From ddff1bfdfe755520494ead56a205f53224a1c4e5 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 25 Sep 2024 10:44:13 +0200 Subject: [PATCH 01/31] feat(SW-214): Setup connection to Contentstack --- lib/graphql/Fragments/Blocks/UspGrid.graphql | 69 ++++++++++++++++ .../contentstack/schemas/blocks/uspGrid.ts | 78 +++++++++++++++++++ types/enums/uspGrid.ts | 5 ++ 3 files changed, 152 insertions(+) create mode 100644 lib/graphql/Fragments/Blocks/UspGrid.graphql create mode 100644 server/routers/contentstack/schemas/blocks/uspGrid.ts create mode 100644 types/enums/uspGrid.ts diff --git a/lib/graphql/Fragments/Blocks/UspGrid.graphql b/lib/graphql/Fragments/Blocks/UspGrid.graphql new file mode 100644 index 000000000..6ccc2a587 --- /dev/null +++ b/lib/graphql/Fragments/Blocks/UspGrid.graphql @@ -0,0 +1,69 @@ +#import "../PageLink/AccountPageLink.graphql" +#import "../PageLink/ContentPageLink.graphql" +#import "../PageLink/HotelPageLink.graphql" +#import "../PageLink/LoyaltyPageLink.graphql" + +#import "../AccountPage/Ref.graphql" +#import "../ContentPage/Ref.graphql" +#import "../HotelPage/Ref.graphql" +#import "../LoyaltyPage/Ref.graphql" + +fragment UspGrid_ContentPage on ContentPageBlocksUspGrid { + __typename + usp_grid { + cardsConnection { + edges { + node { + ... on UspGrid { + usp_card { + icon + text { + embedded_itemsConnection { + totalCount + edges { + node { + __typename + ...AccountPageLink + ...ContentPageLink + ...HotelPageLink + ...LoyaltyPageLink + } + } + } + json + } + } + } + } + } + } + } +} + +fragment UspGrid_ContentPageRefs on ContentPageBlocksUspGrid { + usp_grid { + cardsConnection { + edges { + node { + ... on UspGrid { + usp_card { + text { + embedded_itemsConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...ImageContainerRef + ...LoyaltyPageRef + } + } + } + } + } + } + } + } + } + } +} diff --git a/server/routers/contentstack/schemas/blocks/uspGrid.ts b/server/routers/contentstack/schemas/blocks/uspGrid.ts new file mode 100644 index 000000000..d65a5d0f2 --- /dev/null +++ b/server/routers/contentstack/schemas/blocks/uspGrid.ts @@ -0,0 +1,78 @@ +import { z } from "zod" + +import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" + +import { BlocksEnums } from "@/types/enums/blocks" +import { UspGridEnum } from "@/types/enums/uspGrid" + +export const uspGridSchema = z.object({ + typename: z + .literal(BlocksEnums.block.UspGrid) + .optional() + .default(BlocksEnums.block.UspGrid), + usp_grid: z.object({ + usp_card: z.array( + z.object({ + icon: UspGridEnum.uspIcons, + text: z.object({ + json: z.any(), // JSON + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: z + .discriminatedUnion("__typename", [ + pageLinks.accountPageSchema, + pageLinks.contentPageSchema, + pageLinks.hotelPageSchema, + pageLinks.loyaltyPageSchema, + ]) + .transform((data) => { + const link = pageLinks.transform(data) + if (link) { + return link + } + return data + }), + }) + ), + }), + }), + }) + ), + }), +}) + +const actualRefs = z.discriminatedUnion("__typename", [ + pageLinks.accountPageRefSchema, + pageLinks.contentPageRefSchema, + pageLinks.hotelPageRefSchema, + pageLinks.loyaltyPageRefSchema, +]) + +type Refs = { + node: z.TypeOf +} + +export const uspGridRefsSchema = z.object({ + usp_grid: z + .object({ + usp_card: z.array( + z.object({ + text: z.object({ + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: z.discriminatedUnion("__typename", [ + ...actualRefs.options, + ]), + }) + ), + }), + }), + }) + ), + }) + .transform((data) => { + return data.usp_card.flat() + }), +}) diff --git a/types/enums/uspGrid.ts b/types/enums/uspGrid.ts new file mode 100644 index 000000000..247e46610 --- /dev/null +++ b/types/enums/uspGrid.ts @@ -0,0 +1,5 @@ +import { z } from "zod" + +export namespace UspGridEnum { + export const uspIcons = z.enum(["Snowflake"]) +} From cc7ecff639e6c282179d2512592eac82daf327be Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 25 Sep 2024 10:44:43 +0200 Subject: [PATCH 02/31] feat(SW-214): Setup connection to Contentstack --- .../Query/ContentPage/ContentPage.graphql | 3 + .../contentstack/contentPage/output.ts | 18 +++- .../contentstack/schemas/blocks/textCols.ts | 87 ++++++++++--------- types/enums/blocks.ts | 1 + types/enums/contentPage.ts | 1 + 5 files changed, 64 insertions(+), 46 deletions(-) diff --git a/lib/graphql/Query/ContentPage/ContentPage.graphql b/lib/graphql/Query/ContentPage/ContentPage.graphql index 150429ee0..7b330003b 100644 --- a/lib/graphql/Query/ContentPage/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage/ContentPage.graphql @@ -5,6 +5,7 @@ #import "../../Fragments/Blocks/DynamicContent.graphql" #import "../../Fragments/Blocks/Shortcuts.graphql" #import "../../Fragments/Blocks/TextCols.graphql" +#import "../../Fragments/Blocks/UspGrid.graphql" #import "../../Fragments/Sidebar/Content.graphql" #import "../../Fragments/Sidebar/DynamicContent.graphql" @@ -25,6 +26,7 @@ query GetContentPage($locale: String!, $uid: String!) { ...DynamicContent_ContentPage ...Shortcuts_ContentPage ...TextCols_ContentPage + ...UspGrid_ContentPage } sidebar { __typename @@ -49,6 +51,7 @@ query GetContentPageRefs($locale: String!, $uid: String!) { ...DynamicContent_ContentPageRefs ...Shortcuts_ContentPageRefs ...TextCols_ContentPageRef + ...UspGrid_ContentPageRefs } sidebar { __typename diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 2be699b43..bb2728e63 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -18,6 +18,8 @@ import { shortcutsRefsSchema, shortcutsSchema, } from "../schemas/blocks/shortcuts" +import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols" +import { uspGridSchema } from "../schemas/blocks/uspGrid" import { tempImageVaultAssetSchema } from "../schemas/imageVault" import { contentRefsSchema as sidebarContentRefsSchema, @@ -31,7 +33,6 @@ import { import { systemSchema } from "../schemas/system" import { ContentPageEnum } from "@/types/enums/contentPage" -import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols" // Block schemas export const contentPageCards = z @@ -58,9 +59,17 @@ export const contentPageShortcuts = z }) .merge(shortcutsSchema) -export const contentPageTextCols = z.object({ - __typename: z.literal(ContentPageEnum.ContentStack.blocks.TextCols), -}).merge(textColsSchema) +export const contentPageTextCols = z + .object({ + __typename: z.literal(ContentPageEnum.ContentStack.blocks.TextCols), + }) + .merge(textColsSchema) + +export const contentPageUspGrid = z + .object({ + __typename: z.literal(ContentPageEnum.ContentStack.blocks.UspGrid), + }) + .merge(uspGridSchema) export const blocksSchema = z.discriminatedUnion("__typename", [ contentPageCards, @@ -68,6 +77,7 @@ export const blocksSchema = z.discriminatedUnion("__typename", [ contentPageDynamicContent, contentPageShortcuts, contentPageTextCols, + contentPageUspGrid, ]) export const contentPageSidebarContent = z diff --git a/server/routers/contentstack/schemas/blocks/textCols.ts b/server/routers/contentstack/schemas/blocks/textCols.ts index 01c37953b..6c6a465ff 100644 --- a/server/routers/contentstack/schemas/blocks/textCols.ts +++ b/server/routers/contentstack/schemas/blocks/textCols.ts @@ -1,47 +1,48 @@ import { z } from "zod" import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" + import { imageRefsSchema, imageSchema } from "./image" import { BlocksEnums } from "@/types/enums/blocks" import { ContentEnum } from "@/types/enums/content" -export const textColsSchema = z - .object({ - typename: z - .literal(BlocksEnums.block.TextCols) - .optional() - .default(BlocksEnums.block.TextCols), - text_cols: z.object({ - columns: z.array( - z.object({ - title: z.string().optional().default(""), - text: z.object({ - json: z.any(), // JSON - embedded_itemsConnection: z.object({ - edges: z.array( - z.object({ - node: z.discriminatedUnion("__typename", [ +export const textColsSchema = z.object({ + typename: z + .literal(BlocksEnums.block.TextCols) + .optional() + .default(BlocksEnums.block.TextCols), + text_cols: z.object({ + columns: z.array( + z.object({ + title: z.string().optional().default(""), + text: z.object({ + json: z.any(), // JSON + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: z + .discriminatedUnion("__typename", [ imageSchema, pageLinks.contentPageSchema, pageLinks.hotelPageSchema, pageLinks.loyaltyPageSchema, ]) - .transform((data) => { - const link = pageLinks.transform(data) - if (link) { - return link - } - return data - }), - }) - ), - }), + .transform((data) => { + const link = pageLinks.transform(data) + if (link) { + return link + } + return data + }), + }) + ), }), - }) - ), - }), - }) + }), + }) + ), + }), +}) const actualRefs = z.discriminatedUnion("__typename", [ pageLinks.contentPageRefSchema, @@ -53,9 +54,9 @@ type Refs = { node: z.TypeOf } -export const textColsRefsSchema = z - .object({ - text_cols: z.object({ +export const textColsRefsSchema = z.object({ + text_cols: z + .object({ columns: z.array( z.object({ text: z.object({ @@ -65,20 +66,22 @@ export const textColsRefsSchema = z node: z.discriminatedUnion("__typename", [ imageRefsSchema, ...actualRefs.options, - ]) + ]), }) ), }), }), }) ), - }).transform(data => { - return data.columns.map(column => { - const filtered = column.text.embedded_itemsConnection.edges - .filter( - block => block.node.__typename !== ContentEnum.blocks.SysAsset + }) + .transform((data) => { + return data.columns + .map((column) => { + const filtered = column.text.embedded_itemsConnection.edges.filter( + (block) => block.node.__typename !== ContentEnum.blocks.SysAsset ) as unknown as Refs[] // TS issue with filtered out types - return filtered.map(({ node }) => node.system) - }).flat() + return filtered.map(({ node }) => node.system) + }) + .flat() }), - }) \ No newline at end of file +}) diff --git a/types/enums/blocks.ts b/types/enums/blocks.ts index 88dfe5055..b91989b2e 100644 --- a/types/enums/blocks.ts +++ b/types/enums/blocks.ts @@ -6,5 +6,6 @@ export namespace BlocksEnums { Shortcuts = "Shortcuts", TextCols = "TextCols", TextContent = "TextContent", + UspGrid = "UspGrid", } } diff --git a/types/enums/contentPage.ts b/types/enums/contentPage.ts index 79663d74d..99dd48911 100644 --- a/types/enums/contentPage.ts +++ b/types/enums/contentPage.ts @@ -6,6 +6,7 @@ export namespace ContentPageEnum { DynamicContent = "ContentPageBlocksDynamicContent", Shortcuts = "ContentPageBlocksShortcuts", TextCols = "ContentPageBlocksTextCols", + UspGrid = "ContentPageBlocksUspGrid", } export const enum sidebar { From ee2abb72d2f45c05fa8785893cb3fa2000d104b8 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 25 Sep 2024 10:44:13 +0200 Subject: [PATCH 03/31] feat(SW-214): Setup connection to Contentstack --- lib/graphql/Fragments/Blocks/UspGrid.graphql | 69 ++++++++++++++++ .../contentstack/schemas/blocks/uspGrid.ts | 78 +++++++++++++++++++ types/enums/uspGrid.ts | 5 ++ 3 files changed, 152 insertions(+) create mode 100644 lib/graphql/Fragments/Blocks/UspGrid.graphql create mode 100644 server/routers/contentstack/schemas/blocks/uspGrid.ts create mode 100644 types/enums/uspGrid.ts diff --git a/lib/graphql/Fragments/Blocks/UspGrid.graphql b/lib/graphql/Fragments/Blocks/UspGrid.graphql new file mode 100644 index 000000000..6ccc2a587 --- /dev/null +++ b/lib/graphql/Fragments/Blocks/UspGrid.graphql @@ -0,0 +1,69 @@ +#import "../PageLink/AccountPageLink.graphql" +#import "../PageLink/ContentPageLink.graphql" +#import "../PageLink/HotelPageLink.graphql" +#import "../PageLink/LoyaltyPageLink.graphql" + +#import "../AccountPage/Ref.graphql" +#import "../ContentPage/Ref.graphql" +#import "../HotelPage/Ref.graphql" +#import "../LoyaltyPage/Ref.graphql" + +fragment UspGrid_ContentPage on ContentPageBlocksUspGrid { + __typename + usp_grid { + cardsConnection { + edges { + node { + ... on UspGrid { + usp_card { + icon + text { + embedded_itemsConnection { + totalCount + edges { + node { + __typename + ...AccountPageLink + ...ContentPageLink + ...HotelPageLink + ...LoyaltyPageLink + } + } + } + json + } + } + } + } + } + } + } +} + +fragment UspGrid_ContentPageRefs on ContentPageBlocksUspGrid { + usp_grid { + cardsConnection { + edges { + node { + ... on UspGrid { + usp_card { + text { + embedded_itemsConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...ImageContainerRef + ...LoyaltyPageRef + } + } + } + } + } + } + } + } + } + } +} diff --git a/server/routers/contentstack/schemas/blocks/uspGrid.ts b/server/routers/contentstack/schemas/blocks/uspGrid.ts new file mode 100644 index 000000000..d65a5d0f2 --- /dev/null +++ b/server/routers/contentstack/schemas/blocks/uspGrid.ts @@ -0,0 +1,78 @@ +import { z } from "zod" + +import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" + +import { BlocksEnums } from "@/types/enums/blocks" +import { UspGridEnum } from "@/types/enums/uspGrid" + +export const uspGridSchema = z.object({ + typename: z + .literal(BlocksEnums.block.UspGrid) + .optional() + .default(BlocksEnums.block.UspGrid), + usp_grid: z.object({ + usp_card: z.array( + z.object({ + icon: UspGridEnum.uspIcons, + text: z.object({ + json: z.any(), // JSON + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: z + .discriminatedUnion("__typename", [ + pageLinks.accountPageSchema, + pageLinks.contentPageSchema, + pageLinks.hotelPageSchema, + pageLinks.loyaltyPageSchema, + ]) + .transform((data) => { + const link = pageLinks.transform(data) + if (link) { + return link + } + return data + }), + }) + ), + }), + }), + }) + ), + }), +}) + +const actualRefs = z.discriminatedUnion("__typename", [ + pageLinks.accountPageRefSchema, + pageLinks.contentPageRefSchema, + pageLinks.hotelPageRefSchema, + pageLinks.loyaltyPageRefSchema, +]) + +type Refs = { + node: z.TypeOf +} + +export const uspGridRefsSchema = z.object({ + usp_grid: z + .object({ + usp_card: z.array( + z.object({ + text: z.object({ + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: z.discriminatedUnion("__typename", [ + ...actualRefs.options, + ]), + }) + ), + }), + }), + }) + ), + }) + .transform((data) => { + return data.usp_card.flat() + }), +}) diff --git a/types/enums/uspGrid.ts b/types/enums/uspGrid.ts new file mode 100644 index 000000000..247e46610 --- /dev/null +++ b/types/enums/uspGrid.ts @@ -0,0 +1,5 @@ +import { z } from "zod" + +export namespace UspGridEnum { + export const uspIcons = z.enum(["Snowflake"]) +} From db78a234e4efdf08d6ef94d067fbf382f109a951 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 25 Sep 2024 10:44:43 +0200 Subject: [PATCH 04/31] feat(SW-214): Setup connection to Contentstack --- .../Query/ContentPage/ContentPage.graphql | 3 + .../contentstack/contentPage/output.ts | 18 +++- .../contentstack/schemas/blocks/textCols.ts | 87 ++++++++++--------- types/enums/blocks.ts | 1 + types/enums/contentPage.ts | 1 + 5 files changed, 64 insertions(+), 46 deletions(-) diff --git a/lib/graphql/Query/ContentPage/ContentPage.graphql b/lib/graphql/Query/ContentPage/ContentPage.graphql index 150429ee0..7b330003b 100644 --- a/lib/graphql/Query/ContentPage/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage/ContentPage.graphql @@ -5,6 +5,7 @@ #import "../../Fragments/Blocks/DynamicContent.graphql" #import "../../Fragments/Blocks/Shortcuts.graphql" #import "../../Fragments/Blocks/TextCols.graphql" +#import "../../Fragments/Blocks/UspGrid.graphql" #import "../../Fragments/Sidebar/Content.graphql" #import "../../Fragments/Sidebar/DynamicContent.graphql" @@ -25,6 +26,7 @@ query GetContentPage($locale: String!, $uid: String!) { ...DynamicContent_ContentPage ...Shortcuts_ContentPage ...TextCols_ContentPage + ...UspGrid_ContentPage } sidebar { __typename @@ -49,6 +51,7 @@ query GetContentPageRefs($locale: String!, $uid: String!) { ...DynamicContent_ContentPageRefs ...Shortcuts_ContentPageRefs ...TextCols_ContentPageRef + ...UspGrid_ContentPageRefs } sidebar { __typename diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 2be699b43..bb2728e63 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -18,6 +18,8 @@ import { shortcutsRefsSchema, shortcutsSchema, } from "../schemas/blocks/shortcuts" +import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols" +import { uspGridSchema } from "../schemas/blocks/uspGrid" import { tempImageVaultAssetSchema } from "../schemas/imageVault" import { contentRefsSchema as sidebarContentRefsSchema, @@ -31,7 +33,6 @@ import { import { systemSchema } from "../schemas/system" import { ContentPageEnum } from "@/types/enums/contentPage" -import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols" // Block schemas export const contentPageCards = z @@ -58,9 +59,17 @@ export const contentPageShortcuts = z }) .merge(shortcutsSchema) -export const contentPageTextCols = z.object({ - __typename: z.literal(ContentPageEnum.ContentStack.blocks.TextCols), -}).merge(textColsSchema) +export const contentPageTextCols = z + .object({ + __typename: z.literal(ContentPageEnum.ContentStack.blocks.TextCols), + }) + .merge(textColsSchema) + +export const contentPageUspGrid = z + .object({ + __typename: z.literal(ContentPageEnum.ContentStack.blocks.UspGrid), + }) + .merge(uspGridSchema) export const blocksSchema = z.discriminatedUnion("__typename", [ contentPageCards, @@ -68,6 +77,7 @@ export const blocksSchema = z.discriminatedUnion("__typename", [ contentPageDynamicContent, contentPageShortcuts, contentPageTextCols, + contentPageUspGrid, ]) export const contentPageSidebarContent = z diff --git a/server/routers/contentstack/schemas/blocks/textCols.ts b/server/routers/contentstack/schemas/blocks/textCols.ts index 01c37953b..6c6a465ff 100644 --- a/server/routers/contentstack/schemas/blocks/textCols.ts +++ b/server/routers/contentstack/schemas/blocks/textCols.ts @@ -1,47 +1,48 @@ import { z } from "zod" import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" + import { imageRefsSchema, imageSchema } from "./image" import { BlocksEnums } from "@/types/enums/blocks" import { ContentEnum } from "@/types/enums/content" -export const textColsSchema = z - .object({ - typename: z - .literal(BlocksEnums.block.TextCols) - .optional() - .default(BlocksEnums.block.TextCols), - text_cols: z.object({ - columns: z.array( - z.object({ - title: z.string().optional().default(""), - text: z.object({ - json: z.any(), // JSON - embedded_itemsConnection: z.object({ - edges: z.array( - z.object({ - node: z.discriminatedUnion("__typename", [ +export const textColsSchema = z.object({ + typename: z + .literal(BlocksEnums.block.TextCols) + .optional() + .default(BlocksEnums.block.TextCols), + text_cols: z.object({ + columns: z.array( + z.object({ + title: z.string().optional().default(""), + text: z.object({ + json: z.any(), // JSON + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: z + .discriminatedUnion("__typename", [ imageSchema, pageLinks.contentPageSchema, pageLinks.hotelPageSchema, pageLinks.loyaltyPageSchema, ]) - .transform((data) => { - const link = pageLinks.transform(data) - if (link) { - return link - } - return data - }), - }) - ), - }), + .transform((data) => { + const link = pageLinks.transform(data) + if (link) { + return link + } + return data + }), + }) + ), }), - }) - ), - }), - }) + }), + }) + ), + }), +}) const actualRefs = z.discriminatedUnion("__typename", [ pageLinks.contentPageRefSchema, @@ -53,9 +54,9 @@ type Refs = { node: z.TypeOf } -export const textColsRefsSchema = z - .object({ - text_cols: z.object({ +export const textColsRefsSchema = z.object({ + text_cols: z + .object({ columns: z.array( z.object({ text: z.object({ @@ -65,20 +66,22 @@ export const textColsRefsSchema = z node: z.discriminatedUnion("__typename", [ imageRefsSchema, ...actualRefs.options, - ]) + ]), }) ), }), }), }) ), - }).transform(data => { - return data.columns.map(column => { - const filtered = column.text.embedded_itemsConnection.edges - .filter( - block => block.node.__typename !== ContentEnum.blocks.SysAsset + }) + .transform((data) => { + return data.columns + .map((column) => { + const filtered = column.text.embedded_itemsConnection.edges.filter( + (block) => block.node.__typename !== ContentEnum.blocks.SysAsset ) as unknown as Refs[] // TS issue with filtered out types - return filtered.map(({ node }) => node.system) - }).flat() + return filtered.map(({ node }) => node.system) + }) + .flat() }), - }) \ No newline at end of file +}) diff --git a/types/enums/blocks.ts b/types/enums/blocks.ts index 88dfe5055..b91989b2e 100644 --- a/types/enums/blocks.ts +++ b/types/enums/blocks.ts @@ -6,5 +6,6 @@ export namespace BlocksEnums { Shortcuts = "Shortcuts", TextCols = "TextCols", TextContent = "TextContent", + UspGrid = "UspGrid", } } diff --git a/types/enums/contentPage.ts b/types/enums/contentPage.ts index 79663d74d..99dd48911 100644 --- a/types/enums/contentPage.ts +++ b/types/enums/contentPage.ts @@ -6,6 +6,7 @@ export namespace ContentPageEnum { DynamicContent = "ContentPageBlocksDynamicContent", Shortcuts = "ContentPageBlocksShortcuts", TextCols = "ContentPageBlocksTextCols", + UspGrid = "ContentPageBlocksUspGrid", } export const enum sidebar { From 13c962712c4079b4aa8c5394e9d1055af63d5993 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 20 Sep 2024 11:30:48 +0200 Subject: [PATCH 05/31] fix(SW-432): set max width on amenities list --- .../HotelPage/AmenitiesList/amenitiesList.module.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css b/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css index eb78d72f6..1e6046459 100644 --- a/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css +++ b/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css @@ -6,6 +6,8 @@ display: grid; gap: var(--Spacing-x-one-and-half); height: fit-content; + width: 100%; + max-width: 300px; } .amenityItemList { @@ -26,6 +28,5 @@ @media screen and (min-width: 1367px) { .amenitiesContainer { margin-top: var(--Spacing-x5); - width: 300px; } } From 717a5ef3073cfc1afea66163ae8576c1c9ea885a Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 25 Sep 2024 13:31:13 +0200 Subject: [PATCH 06/31] feat(SW-214): Implement usp component --- components/Blocks/UspGrid/index.tsx | 35 ++++++ components/Blocks/UspGrid/renderOptions.tsx | 73 +++++++++++ components/Blocks/UspGrid/uspgrid.module.css | 28 +++++ components/Blocks/UspGrid/utils.ts | 11 ++ components/Blocks/index.tsx | 3 + components/Icons/Snowflake.tsx | 27 ++++ components/Icons/get-icon-by-icon-name.ts | 3 + components/Icons/index.tsx | 1 + lib/graphql/Fragments/Blocks/UspGrid.graphql | 3 +- .../contentstack/contentPage/output.ts | 9 +- .../routers/contentstack/contentPage/utils.ts | 6 + .../contentstack/schemas/blocks/uspGrid.ts | 118 +++++++++++------- types/components/blocks/uspGrid.ts | 4 + types/components/icon.ts | 1 + types/requests/utils/embeds.ts | 1 + types/trpc/routers/contentstack/blocks.ts | 14 +-- .../trpc/routers/contentstack/contentPage.ts | 8 +- 17 files changed, 286 insertions(+), 59 deletions(-) create mode 100644 components/Blocks/UspGrid/index.tsx create mode 100644 components/Blocks/UspGrid/renderOptions.tsx create mode 100644 components/Blocks/UspGrid/uspgrid.module.css create mode 100644 components/Blocks/UspGrid/utils.ts create mode 100644 components/Icons/Snowflake.tsx create mode 100644 types/components/blocks/uspGrid.ts diff --git a/components/Blocks/UspGrid/index.tsx b/components/Blocks/UspGrid/index.tsx new file mode 100644 index 000000000..26110c622 --- /dev/null +++ b/components/Blocks/UspGrid/index.tsx @@ -0,0 +1,35 @@ +import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" +import JsonToHtml from "@/components/JsonToHtml" + +import { renderOptions } from "./renderOptions" +import { getUspIconName } from "./utils" + +import styles from "./uspgrid.module.css" + +import type { UspGridProps, UspIcon } from "@/types/components/blocks/uspGrid" + +function UspIcon({ icon }: { icon: UspIcon }) { + const iconName = getUspIconName(icon) + const Icon = iconName ? getIconByIconName(iconName) : null + return Icon ? : null +} + +export default function UspGrid({ usp_grid }: UspGridProps) { + return ( +
+ {usp_grid.usp_card.map( + (usp) => + usp.text.json && ( +
+ + +
+ ) + )} +
+ ) +} diff --git a/components/Blocks/UspGrid/renderOptions.tsx b/components/Blocks/UspGrid/renderOptions.tsx new file mode 100644 index 000000000..a4033fec5 --- /dev/null +++ b/components/Blocks/UspGrid/renderOptions.tsx @@ -0,0 +1,73 @@ +import Link from "@/components/TempDesignSystem/Link" +import { removeMultipleSlashes } from "@/utils/url" + +import styles from "./uspgrid.module.css" + +import { EmbedEnum } from "@/types/requests/utils/embeds" +import type { EmbedByUid } from "@/types/transitionTypes/jsontohtml" +import { RTEItemTypeEnum, RTETypeEnum } from "@/types/transitionTypes/rte/enums" +import type { + RTEDefaultNode, + RTENext, + RTENode, + RTERegularNode, +} from "@/types/transitionTypes/rte/node" +import type { RenderOptions } from "@/types/transitionTypes/rte/option" + +export const renderOptions: RenderOptions = { + [RTETypeEnum.p]: ( + node: RTEDefaultNode, + embeds: EmbedByUid, + next: RTENext, + fullRenderOptions: RenderOptions + ) => { + return ( +

+ {next(node.children, embeds, fullRenderOptions)} +

+ ) + }, + [RTETypeEnum.a]: ( + node: RTERegularNode, + embeds: EmbedByUid, + next: RTENext, + fullRenderOptions: RenderOptions + ) => { + if (node.attrs.url) { + return ( + + {next(node.children, embeds, fullRenderOptions)} + + ) + } + return null + }, + [RTETypeEnum.reference]: ( + node: RTENode, + embeds: EmbedByUid, + next: RTENext, + fullRenderOptions: RenderOptions + ) => { + if ("attrs" in node) { + const type = node.attrs.type + if (type !== RTEItemTypeEnum.asset) { + const href = node.attrs?.locale + ? `/${node.attrs.locale}${node.attrs.href}` + : node.attrs.href + + return ( + + {next(node.children, embeds, fullRenderOptions)} + + ) + } + + return null + } + }, +} diff --git a/components/Blocks/UspGrid/uspgrid.module.css b/components/Blocks/UspGrid/uspgrid.module.css new file mode 100644 index 000000000..042acd9b3 --- /dev/null +++ b/components/Blocks/UspGrid/uspgrid.module.css @@ -0,0 +1,28 @@ +.grid { + display: grid; + gap: var(--Spacing-x3); + padding: var(--Spacing-x3) var(--Spacing-x4); +} +@media screen and (min-width: 767px) { + .grid { + grid-template-columns: repeat(3, 1fr); + } + .grid:has(.usp:nth-child(4)) { + grid-template-columns: repeat(2, 1fr); + } +} +.usp { + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); +} +.p { + margin: 0; + font-size: var(--typography-Caption-Regular-fontSize); + color: var(--UI-Text-Medium-contrast); + line-height: 21px; /* Caption variable for line-height is 139.9999976158142%, but it set to 21px in design */ +} +.a { + font-size: var(--typography-Caption-Regular-fontSize); + color: var(--Base-Text-High-contrast); +} diff --git a/components/Blocks/UspGrid/utils.ts b/components/Blocks/UspGrid/utils.ts new file mode 100644 index 000000000..700482909 --- /dev/null +++ b/components/Blocks/UspGrid/utils.ts @@ -0,0 +1,11 @@ +import { UspIcon } from "@/types/components/blocks/uspGrid" +import { IconName } from "@/types/components/icon" + +export function getUspIconName(icon?: UspIcon | null) { + switch (icon) { + case "Snowflake": + return IconName.Snowflake + default: + return IconName.Snowflake + } +} diff --git a/components/Blocks/index.tsx b/components/Blocks/index.tsx index 9946a6fa0..1c6d0bcbc 100644 --- a/components/Blocks/index.tsx +++ b/components/Blocks/index.tsx @@ -2,6 +2,7 @@ import CardsGrid from "@/components/Blocks/CardsGrid" import DynamicContent from "@/components/Blocks/DynamicContent" import Shortcuts from "@/components/Blocks/Shortcuts" import TextCols from "@/components/Blocks/TextCols" +import UspGrid from "@/components/Blocks/UspGrid" import JsonToHtml from "@/components/JsonToHtml" import type { BlocksProps } from "@/types/components/blocks" @@ -57,6 +58,8 @@ export default function Blocks({ blocks }: BlocksProps) { /> ) + case BlocksEnums.block.UspGrid: + return default: return null } diff --git a/components/Icons/Snowflake.tsx b/components/Icons/Snowflake.tsx new file mode 100644 index 000000000..06389343f --- /dev/null +++ b/components/Icons/Snowflake.tsx @@ -0,0 +1,27 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function SnowflakeIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + ) +} diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 117971b3c..92d674299 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -49,6 +49,7 @@ import { SearchIcon, ServiceIcon, ShoppingIcon, + SnowflakeIcon, StarFilledIcon, TrainIcon, TshirtWashIcon, @@ -154,6 +155,8 @@ export function getIconByIconName(icon?: IconName): FC | null { return ServiceIcon case IconName.Shopping: return ShoppingIcon + case IconName.Snowflake: + return SnowflakeIcon case IconName.StarFilled: return StarFilledIcon case IconName.Train: diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index c1806693d..433ef0c89 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -48,6 +48,7 @@ export { default as ScandicLogoIcon } from "./ScandicLogo" export { default as SearchIcon } from "./Search" export { default as ServiceIcon } from "./Service" export { default as ShoppingIcon } from "./Shopping" +export { default as SnowflakeIcon } from "./Snowflake" export { default as StarFilledIcon } from "./StarFilled" export { default as TrainIcon } from "./Train" export { default as TshirtWashIcon } from "./TshirtWash" diff --git a/lib/graphql/Fragments/Blocks/UspGrid.graphql b/lib/graphql/Fragments/Blocks/UspGrid.graphql index 6ccc2a587..ae2df5aa8 100644 --- a/lib/graphql/Fragments/Blocks/UspGrid.graphql +++ b/lib/graphql/Fragments/Blocks/UspGrid.graphql @@ -16,6 +16,7 @@ fragment UspGrid_ContentPage on ContentPageBlocksUspGrid { node { ... on UspGrid { usp_card { + __typename icon text { embedded_itemsConnection { @@ -54,7 +55,7 @@ fragment UspGrid_ContentPageRefs on ContentPageBlocksUspGrid { __typename ...AccountPageRef ...ContentPageRef - ...ImageContainerRef + ...HotelPageRef ...LoyaltyPageRef } } diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index bb2728e63..8048faee6 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -19,7 +19,7 @@ import { shortcutsSchema, } from "../schemas/blocks/shortcuts" import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols" -import { uspGridSchema } from "../schemas/blocks/uspGrid" +import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid" import { tempImageVaultAssetSchema } from "../schemas/imageVault" import { contentRefsSchema as sidebarContentRefsSchema, @@ -157,12 +157,19 @@ const contentPageTextColsRefs = z }) .merge(textColsRefsSchema) +const contentPageUspGridRefs = z + .object({ + __typename: z.literal(ContentPageEnum.ContentStack.blocks.UspGrid), + }) + .merge(uspGridRefsSchema) + const contentPageBlockRefsItem = z.discriminatedUnion("__typename", [ contentPageBlockContentRefs, contentPageShortcutsRefs, contentPageCardsRefs, contentPageDynamicContentRefs, contentPageTextColsRefs, + contentPageUspGridRefs, ]) const contentPageSidebarContentRef = z diff --git a/server/routers/contentstack/contentPage/utils.ts b/server/routers/contentstack/contentPage/utils.ts index c4cc9c5c5..78f5a9d7f 100644 --- a/server/routers/contentstack/contentPage/utils.ts +++ b/server/routers/contentstack/contentPage/utils.ts @@ -147,6 +147,12 @@ export function getConnections({ content_page }: ContentPageRefs) { } break } + case ContentPageEnum.ContentStack.blocks.UspGrid: { + if (block.usp_grid.length) { + connections.push(...block.usp_grid) + } + break + } } }) } diff --git a/server/routers/contentstack/schemas/blocks/uspGrid.ts b/server/routers/contentstack/schemas/blocks/uspGrid.ts index d65a5d0f2..277f84968 100644 --- a/server/routers/contentstack/schemas/blocks/uspGrid.ts +++ b/server/routers/contentstack/schemas/blocks/uspGrid.ts @@ -5,41 +5,57 @@ import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" import { BlocksEnums } from "@/types/enums/blocks" import { UspGridEnum } from "@/types/enums/uspGrid" +const uspCardSchema = z.object({ + icon: UspGridEnum.uspIcons, + text: z.object({ + json: z.any(), // JSON + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: z + .discriminatedUnion("__typename", [ + pageLinks.accountPageSchema, + pageLinks.contentPageSchema, + pageLinks.hotelPageSchema, + pageLinks.loyaltyPageSchema, + ]) + .transform((data) => { + const link = pageLinks.transform(data) + if (link) { + return link + } + return data + }), + }) + ), + }), + }), +}) + export const uspGridSchema = z.object({ typename: z .literal(BlocksEnums.block.UspGrid) .optional() .default(BlocksEnums.block.UspGrid), - usp_grid: z.object({ - usp_card: z.array( - z.object({ - icon: UspGridEnum.uspIcons, - text: z.object({ - json: z.any(), // JSON - embedded_itemsConnection: z.object({ - edges: z.array( - z.object({ - node: z - .discriminatedUnion("__typename", [ - pageLinks.accountPageSchema, - pageLinks.contentPageSchema, - pageLinks.hotelPageSchema, - pageLinks.loyaltyPageSchema, - ]) - .transform((data) => { - const link = pageLinks.transform(data) - if (link) { - return link - } - return data - }), - }) - ), - }), - }), - }) - ), - }), + usp_grid: z + .object({ + cardsConnection: z.object({ + edges: z.array( + z.object({ + node: z.object({ + usp_card: z.array(uspCardSchema), + }), + }) + ), + }), + }) + .transform((data) => { + return { + usp_card: data.cardsConnection.edges.flatMap( + (edge) => edge.node.usp_card + ), + } + }), }) const actualRefs = z.discriminatedUnion("__typename", [ @@ -49,30 +65,40 @@ const actualRefs = z.discriminatedUnion("__typename", [ pageLinks.loyaltyPageRefSchema, ]) -type Refs = { - node: z.TypeOf -} - export const uspGridRefsSchema = z.object({ usp_grid: z .object({ - usp_card: z.array( - z.object({ - text: z.object({ - embedded_itemsConnection: z.object({ - edges: z.array( + cardsConnection: z.object({ + edges: z.array( + z.object({ + node: z.object({ + usp_card: z.array( z.object({ - node: z.discriminatedUnion("__typename", [ - ...actualRefs.options, - ]), + text: z.object({ + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: z.discriminatedUnion("__typename", [ + ...actualRefs.options, + ]), + }) + ), + }), + }), }) ), }), - }), - }) - ), + }) + ), + }), }) .transform((data) => { - return data.usp_card.flat() + return data.cardsConnection.edges.flatMap(({ node }) => + node.usp_card.flatMap((card) => + card.text.embedded_itemsConnection.edges.map( + ({ node }) => node.system + ) + ) + ) }), }) diff --git a/types/components/blocks/uspGrid.ts b/types/components/blocks/uspGrid.ts new file mode 100644 index 000000000..ea6218541 --- /dev/null +++ b/types/components/blocks/uspGrid.ts @@ -0,0 +1,4 @@ +import type { UspGrid } from "@/types/trpc/routers/contentstack/blocks" + +export interface UspGridProps extends Pick {} +export type UspIcon = UspGrid["usp_grid"]["usp_card"][number]["icon"] diff --git a/types/components/icon.ts b/types/components/icon.ts index 8096ef0f7..f9f61a525 100644 --- a/types/components/icon.ts +++ b/types/components/icon.ts @@ -54,6 +54,7 @@ export enum IconName { Search = "Search", Service = "Service", Shopping = "Shopping", + Snowflake = "Snowflake", StarFilled = "StarFilled", Train = "Train", Tripadvisor = "Tripadvisor", diff --git a/types/requests/utils/embeds.ts b/types/requests/utils/embeds.ts index 527564882..0fe39ddbd 100644 --- a/types/requests/utils/embeds.ts +++ b/types/requests/utils/embeds.ts @@ -5,6 +5,7 @@ export enum EmbedEnum { LoyaltyPage = "LoyaltyPage", AccountPage = "AccountPage", ContentPage = "ContentPage", + HotelPage = "HotelPage", } export type Embed = keyof typeof EmbedEnum diff --git a/types/trpc/routers/contentstack/blocks.ts b/types/trpc/routers/contentstack/blocks.ts index 8d8e449ab..6bffdfc90 100644 --- a/types/trpc/routers/contentstack/blocks.ts +++ b/types/trpc/routers/contentstack/blocks.ts @@ -4,13 +4,13 @@ import { cardsGridSchema } from "@/server/routers/contentstack/schemas/blocks/ca 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" - import { textColsSchema } from "@/server/routers/contentstack/schemas/blocks/textCols" +import { uspGridSchema } from "@/server/routers/contentstack/schemas/blocks/uspGrid" -export interface CardsGrid extends z.output { } -export interface Content extends z.output { } -export interface DynamicContent extends z.output { } -export interface Shortcuts extends z.output { } +export interface CardsGrid extends z.output {} +export interface Content extends z.output {} +export interface DynamicContent extends z.output {} +export interface Shortcuts extends z.output {} export type Shortcut = Shortcuts["shortcuts"] -export interface TextCols extends z.output { } - +export interface TextCols extends z.output {} +export interface UspGrid extends z.output {} diff --git a/types/trpc/routers/contentstack/contentPage.ts b/types/trpc/routers/contentstack/contentPage.ts index d95521319..6bc343278 100644 --- a/types/trpc/routers/contentstack/contentPage.ts +++ b/types/trpc/routers/contentstack/contentPage.ts @@ -8,15 +8,15 @@ import { } from "@/server/routers/contentstack/contentPage/output" export interface GetContentPageRefsSchema - extends z.input { } + extends z.input {} export interface ContentPageRefs - extends z.output { } + extends z.output {} export interface GetContentPageSchema - extends z.input { } + extends z.input {} -export interface ContentPage extends z.output { } +export interface ContentPage extends z.output {} export type Block = z.output From 23bc4eb831a57577929a2b6abe992df102658634 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 25 Sep 2024 16:00:50 +0200 Subject: [PATCH 07/31] feat(SW-214): fixing pr comments --- components/Blocks/UspGrid/index.tsx | 2 +- server/routers/contentstack/schemas/blocks/uspGrid.ts | 4 +--- types/enums/uspGrid.ts | 7 ++++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/components/Blocks/UspGrid/index.tsx b/components/Blocks/UspGrid/index.tsx index 26110c622..2f24f1117 100644 --- a/components/Blocks/UspGrid/index.tsx +++ b/components/Blocks/UspGrid/index.tsx @@ -25,7 +25,7 @@ export default function UspGrid({ usp_grid }: UspGridProps) { ) diff --git a/server/routers/contentstack/schemas/blocks/uspGrid.ts b/server/routers/contentstack/schemas/blocks/uspGrid.ts index 277f84968..fc351fbe0 100644 --- a/server/routers/contentstack/schemas/blocks/uspGrid.ts +++ b/server/routers/contentstack/schemas/blocks/uspGrid.ts @@ -78,9 +78,7 @@ export const uspGridRefsSchema = z.object({ embedded_itemsConnection: z.object({ edges: z.array( z.object({ - node: z.discriminatedUnion("__typename", [ - ...actualRefs.options, - ]), + node: actualRefs, }) ), }), diff --git a/types/enums/uspGrid.ts b/types/enums/uspGrid.ts index 247e46610..e5f45dbdc 100644 --- a/types/enums/uspGrid.ts +++ b/types/enums/uspGrid.ts @@ -1,5 +1,10 @@ import { z } from "zod" +import { IconName } from "../components/icon" + export namespace UspGridEnum { - export const uspIcons = z.enum(["Snowflake"]) + export const enum icons { + Snowflake = IconName.Snowflake, + } + export const uspIcons = z.enum([icons.Snowflake]) } From a05131260f38c43f00f7b2180741023f60cf94b5 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 25 Sep 2024 16:08:28 +0200 Subject: [PATCH 08/31] fix: updated enums to match contentstack --- types/components/footer/appDownloadIcons.ts | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/types/components/footer/appDownloadIcons.ts b/types/components/footer/appDownloadIcons.ts index e41eea35e..b591ac280 100644 --- a/types/components/footer/appDownloadIcons.ts +++ b/types/components/footer/appDownloadIcons.ts @@ -1,14 +1,14 @@ export enum AppDownLoadLinks { - apple_da = "/_static/img/store-badges/app-store-badge-da.svg", - apple_de = "/_static/img/store-badges/app-store-badge-de.svg", - apple_en = "/_static/img/store-badges/app-store-badge-en.svg", - apple_fi = "/_static/img/store-badges/app-store-badge-fi.svg", - apple_no = "/_static/img/store-badges/app-store-badge-no.svg", - apple_sv = "/_static/img/store-badges/app-store-badge-sv.svg", - google_da = "/_static/img/store-badges/google-play-badge-da.svg", - google_de = "/_static/img/store-badges/google-play-badge-de.svg", - google_en = "/_static/img/store-badges/google-play-badge-en.svg", - google_fi = "/_static/img/store-badges/google-play-badge-fi.svg", - google_no = "/_static/img/store-badges/google-play-badge-no.svg", - google_sv = "/_static/img/store-badges/google-play-badge-sv.svg", + Apple_da = "/_static/img/store-badges/app-store-badge-da.svg", + Apple_de = "/_static/img/store-badges/app-store-badge-de.svg", + Apple_en = "/_static/img/store-badges/app-store-badge-en.svg", + Apple_fi = "/_static/img/store-badges/app-store-badge-fi.svg", + Apple_no = "/_static/img/store-badges/app-store-badge-no.svg", + Apple_sv = "/_static/img/store-badges/app-store-badge-sv.svg", + Google_da = "/_static/img/store-badges/google-play-badge-da.svg", + Google_de = "/_static/img/store-badges/google-play-badge-de.svg", + Google_en = "/_static/img/store-badges/google-play-badge-en.svg", + Google_fi = "/_static/img/store-badges/google-play-badge-fi.svg", + Google_no = "/_static/img/store-badges/google-play-badge-no.svg", + Google_sv = "/_static/img/store-badges/google-play-badge-sv.svg", } From 4352997322bc550f44d292c710f57a5985da91de Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 19 Sep 2024 15:16:58 +0200 Subject: [PATCH 09/31] feat(SW-368): added link chips component --- .../ContentPage/contentPage.module.css | 18 +++++ components/ContentType/ContentPage/index.tsx | 29 ++++---- components/Icons/ChevronRightSmall.tsx | 40 ++++++++++ components/Icons/get-icon-by-icon-name.ts | 3 + components/Icons/index.tsx | 1 + components/Intro/index.tsx | 11 --- components/Intro/intro.module.css | 16 ---- .../LinkChips/Chip/chip.module.css | 14 ++++ .../TempDesignSystem/LinkChips/Chip/chip.ts | 4 + .../TempDesignSystem/LinkChips/Chip/index.tsx | 19 +++++ .../TempDesignSystem/LinkChips/index.tsx | 20 +++++ .../LinkChips/linkChips.module.css | 8 ++ .../TempDesignSystem/LinkChips/linkChips.ts | 5 ++ .../ContentPage/NavigationLinks.graphql | 19 +++++ .../Query/ContentPage/ContentPage.graphql | 17 +++++ .../contentstack/contentPage/output.ts | 32 +++++++- .../contentstack/schemas/linkConnection.ts | 74 +++++++++++++++++++ types/components/icon.ts | 1 + 18 files changed, 290 insertions(+), 41 deletions(-) create mode 100644 components/Icons/ChevronRightSmall.tsx delete mode 100644 components/Intro/index.tsx delete mode 100644 components/Intro/intro.module.css create mode 100644 components/TempDesignSystem/LinkChips/Chip/chip.module.css create mode 100644 components/TempDesignSystem/LinkChips/Chip/chip.ts create mode 100644 components/TempDesignSystem/LinkChips/Chip/index.tsx create mode 100644 components/TempDesignSystem/LinkChips/index.tsx create mode 100644 components/TempDesignSystem/LinkChips/linkChips.module.css create mode 100644 components/TempDesignSystem/LinkChips/linkChips.ts create mode 100644 lib/graphql/Fragments/ContentPage/NavigationLinks.graphql create mode 100644 server/routers/contentstack/schemas/linkConnection.ts diff --git a/components/ContentType/ContentPage/contentPage.module.css b/components/ContentType/ContentPage/contentPage.module.css index 013b1863a..9ba97d7f9 100644 --- a/components/ContentType/ContentPage/contentPage.module.css +++ b/components/ContentType/ContentPage/contentPage.module.css @@ -11,6 +11,18 @@ padding: var(--Spacing-x4) var(--Spacing-x2); } +.headerContent { + display: grid; + gap: var(--Spacing-x3); + max-width: var(--max-width-content); + margin: 0 auto; +} +.headerIntro { + display: grid; + max-width: var(--max-width-text-block); + gap: var(--Spacing-x3); +} + .content { padding: var(--Spacing-x4) var(--Spacing-x2); display: grid; @@ -21,3 +33,9 @@ width: 100%; max-width: var(--max-width-content); } + +@media (min-width: 768px) { + .headerIntro { + gap: var(--Spacing-x3); + } +} diff --git a/components/ContentType/ContentPage/index.tsx b/components/ContentType/ContentPage/index.tsx index 8e53cafd7..3e7c59abb 100644 --- a/components/ContentType/ContentPage/index.tsx +++ b/components/ContentType/ContentPage/index.tsx @@ -2,8 +2,8 @@ import { serverClient } from "@/lib/trpc/server" import Blocks from "@/components/Blocks" import Hero from "@/components/Hero" -import Intro from "@/components/Intro" import Sidebar from "@/components/Sidebar" +import LinkChips from "@/components/TempDesignSystem/LinkChips" import Preamble from "@/components/TempDesignSystem/Text/Preamble" import Title from "@/components/TempDesignSystem/Text/Title" import TrackingSDK from "@/components/TrackingSDK" @@ -18,31 +18,34 @@ export default async function ContentPage() { } const { tracking, contentPage } = contentPageRes - const heroImage = contentPage.hero_image + const { blocks, hero_image, header, sidebar } = contentPage return ( <>
- {contentPage.sidebar?.length ? ( - - ) : null} + {sidebar?.length ? : null}
- - {contentPage.header.heading} - {contentPage.header.preamble} - +
+
+ {header.heading} + {header.preamble} +
+ {header.navigation_links ? ( + + ) : null} +
- {heroImage ? ( + {hero_image ? ( ) : null} - {contentPage.blocks ? : null} + {blocks ? : null}
diff --git a/components/Icons/ChevronRightSmall.tsx b/components/Icons/ChevronRightSmall.tsx new file mode 100644 index 000000000..25fb29002 --- /dev/null +++ b/components/Icons/ChevronRightSmall.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ChevronRightSmallIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 92d674299..20bf8d9a8 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -17,6 +17,7 @@ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, + ChevronRightSmallIcon, CloseIcon, CloseLarge, CoffeeIcon, @@ -89,6 +90,8 @@ export function getIconByIconName(icon?: IconName): FC | null { return ChevronLeftIcon case IconName.ChevronRight: return ChevronRightIcon + case IconName.ChevronRightSmall: + return ChevronRightSmallIcon case IconName.Close: return CloseIcon case IconName.CloseLarge: diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 433ef0c89..9d7c0bd2c 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -11,6 +11,7 @@ export { default as CheckCircleIcon } from "./CheckCircle" export { default as ChevronDownIcon } from "./ChevronDown" 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 CoffeeIcon } from "./Coffee" diff --git a/components/Intro/index.tsx b/components/Intro/index.tsx deleted file mode 100644 index 6c7a8a30d..000000000 --- a/components/Intro/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { PropsWithChildren } from "react" - -import styles from "./intro.module.css" - -export default async function Intro({ children }: PropsWithChildren) { - return ( -
-
{children}
-
- ) -} diff --git a/components/Intro/intro.module.css b/components/Intro/intro.module.css deleted file mode 100644 index b2290fb9f..000000000 --- a/components/Intro/intro.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.intro { - max-width: var(--max-width-content); - margin: 0 auto; -} - -.content { - display: grid; - max-width: var(--max-width-text-block); - gap: var(--Spacing-x2); -} - -@media (min-width: 768px) { - .content { - gap: var(--Spacing-x3); - } -} diff --git a/components/TempDesignSystem/LinkChips/Chip/chip.module.css b/components/TempDesignSystem/LinkChips/Chip/chip.module.css new file mode 100644 index 000000000..311892c58 --- /dev/null +++ b/components/TempDesignSystem/LinkChips/Chip/chip.module.css @@ -0,0 +1,14 @@ +.linkChip { + display: flex; + align-items: center; + gap: var(--Spacing-x-half); + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); + border-radius: var(--Corner-radius-Small); + background-color: var(--Base-Button-Inverted-Fill-Normal); + transition: background-color 0.3s; + text-decoration: none; +} + +.linkChip:hover { + background-color: var(--Base-Button-Inverted-Fill-Hover-alt); +} diff --git a/components/TempDesignSystem/LinkChips/Chip/chip.ts b/components/TempDesignSystem/LinkChips/Chip/chip.ts new file mode 100644 index 000000000..0a4116686 --- /dev/null +++ b/components/TempDesignSystem/LinkChips/Chip/chip.ts @@ -0,0 +1,4 @@ +export interface LinkChipProps { + url: string + title: string +} diff --git a/components/TempDesignSystem/LinkChips/Chip/index.tsx b/components/TempDesignSystem/LinkChips/Chip/index.tsx new file mode 100644 index 000000000..791037906 --- /dev/null +++ b/components/TempDesignSystem/LinkChips/Chip/index.tsx @@ -0,0 +1,19 @@ +import Link from "next/link" + +import { ChevronRightSmallIcon } from "@/components/Icons" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import { LinkChipProps } from "./chip" + +import styles from "./chip.module.css" + +export default function LinkChip({ url, title }: LinkChipProps) { + return ( + + + {title} + + + + ) +} diff --git a/components/TempDesignSystem/LinkChips/index.tsx b/components/TempDesignSystem/LinkChips/index.tsx new file mode 100644 index 000000000..2d6e12881 --- /dev/null +++ b/components/TempDesignSystem/LinkChips/index.tsx @@ -0,0 +1,20 @@ +import LinkChip from "./Chip" +import { LinkChipsProps } from "./linkChips" + +import styles from "./linkChips.module.css" + +export default function LinkChips({ chips }: LinkChipsProps) { + if (!chips.length) { + return null + } + + return ( +
    + {chips.map(({ url, title }) => ( +
  • + +
  • + ))} +
+ ) +} diff --git a/components/TempDesignSystem/LinkChips/linkChips.module.css b/components/TempDesignSystem/LinkChips/linkChips.module.css new file mode 100644 index 000000000..9d7361824 --- /dev/null +++ b/components/TempDesignSystem/LinkChips/linkChips.module.css @@ -0,0 +1,8 @@ +.linkChips { + list-style: none; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: var(--Spacing-x1); +} diff --git a/components/TempDesignSystem/LinkChips/linkChips.ts b/components/TempDesignSystem/LinkChips/linkChips.ts new file mode 100644 index 000000000..361cd07ed --- /dev/null +++ b/components/TempDesignSystem/LinkChips/linkChips.ts @@ -0,0 +1,5 @@ +import { LinkChipProps } from "./Chip/chip" + +export interface LinkChipsProps { + chips: LinkChipProps[] +} diff --git a/lib/graphql/Fragments/ContentPage/NavigationLinks.graphql b/lib/graphql/Fragments/ContentPage/NavigationLinks.graphql new file mode 100644 index 000000000..b82d68627 --- /dev/null +++ b/lib/graphql/Fragments/ContentPage/NavigationLinks.graphql @@ -0,0 +1,19 @@ +#import "../PageLink/ContentPageLink.graphql" +#import "../PageLink/HotelPageLink.graphql" +#import "../PageLink/LoyaltyPageLink.graphql" + +fragment NavigationLinks on ContentPageHeader { + navigation_links { + title + linkConnection { + edges { + node { + __typename + ...HotelPageLink + ...ContentPageLink + ...LoyaltyPageLink + } + } + } + } +} diff --git a/lib/graphql/Query/ContentPage/ContentPage.graphql b/lib/graphql/Query/ContentPage/ContentPage.graphql index 7b330003b..c1e4decc5 100644 --- a/lib/graphql/Query/ContentPage/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage/ContentPage.graphql @@ -7,6 +7,8 @@ #import "../../Fragments/Blocks/TextCols.graphql" #import "../../Fragments/Blocks/UspGrid.graphql" +#import "../../Fragments/ContentPage/NavigationLinks.graphql" + #import "../../Fragments/Sidebar/Content.graphql" #import "../../Fragments/Sidebar/DynamicContent.graphql" #import "../../Fragments/Sidebar/JoinLoyaltyContact.graphql" @@ -18,6 +20,7 @@ query GetContentPage($locale: String!, $uid: String!) { header { heading preamble + ...NavigationLinks } blocks { __typename @@ -44,6 +47,20 @@ query GetContentPage($locale: String!, $uid: String!) { query GetContentPageRefs($locale: String!, $uid: String!) { content_page(locale: $locale, uid: $uid) { + header { + navigation_links { + linkConnection { + edges { + node { + __typename + ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef + } + } + } + } + } blocks { __typename ...CardsGrid_ContentPageRefs diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 8048faee6..398abb372 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -11,8 +11,8 @@ import { contentSchema as blockContentSchema, } from "../schemas/blocks/content" import { - dynamicContentRefsSchema, dynamicContentSchema as blockDynamicContentSchema, + dynamicContentRefsSchema, } from "../schemas/blocks/dynamicContent" import { shortcutsRefsSchema, @@ -32,7 +32,18 @@ import { } from "../schemas/sidebar/joinLoyaltyContact" import { systemSchema } from "../schemas/system" +import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" import { ContentPageEnum } from "@/types/enums/contentPage" +import { + linkAndTitleSchema, + linkConnectionRefs, +} from "../schemas/linkConnection" + +const linkUnionSchema = z.discriminatedUnion("__typename", [ + pageLinks.contentPageSchema, + pageLinks.hotelPageSchema, + pageLinks.loyaltyPageSchema, +]) // Block schemas export const contentPageCards = z @@ -106,6 +117,19 @@ export const sidebarSchema = z.discriminatedUnion("__typename", [ contentPageJoinLoyaltyContact, ]) +const navigationLinksSchema = z + .array(linkAndTitleSchema) + .nullable() + .transform((data) => { + if (!data) { + return null + } + + return data + .filter((item) => !!item.link) + .map((item) => ({ url: item.link.url, title: item.title })) + }) + // Content Page Schema and types export const contentPageSchema = z.object({ content_page: z.object({ @@ -116,6 +140,7 @@ export const contentPageSchema = z.object({ header: z.object({ heading: z.string(), preamble: z.string(), + navigation_links: navigationLinksSchema, }), system: systemSchema.merge( z.object({ @@ -191,8 +216,13 @@ const contentPageSidebarRefsItem = z.discriminatedUnion("__typename", [ contentPageSidebarJoinLoyaltyContactRef, ]) +const contentPageHeaderRefs = z.object({ + navigation_links: z.array(linkConnectionRefs), +}) + export const contentPageRefsSchema = z.object({ content_page: z.object({ + header: contentPageHeaderRefs, blocks: discriminatedUnionArray( contentPageBlockRefsItem.options ).nullable(), diff --git a/server/routers/contentstack/schemas/linkConnection.ts b/server/routers/contentstack/schemas/linkConnection.ts new file mode 100644 index 000000000..aff9b980c --- /dev/null +++ b/server/routers/contentstack/schemas/linkConnection.ts @@ -0,0 +1,74 @@ +import { z } from "zod" + + + +import { discriminatedUnion } from "@/lib/discriminatedUnion" +import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" + +const linkUnionSchema = z.discriminatedUnion("__typename", [ + pageLinks.contentPageSchema, + pageLinks.hotelPageSchema, + pageLinks.loyaltyPageSchema, +]) + +const titleSchema = z.object({ + title: z.string().optional().default(""), +}) + +export const linkConnectionSchema = z + .object({ + linkConnection: z.object({ + edges: z.array( + z.object({ + node: discriminatedUnion(linkUnionSchema.options), + }) + ), + }), + }) + .transform((data) => { + if (data.linkConnection.edges.length) { + const link = pageLinks.transform(data.linkConnection.edges[0].node) + if (link) { + return { + link, + } + } + } + + return { + link: null, + } + }) + +export const linkAndTitleSchema = z.intersection( + linkConnectionSchema, + titleSchema +) + +const linkRefsUnionSchema = z.discriminatedUnion("__typename", [ + pageLinks.contentPageRefSchema, + pageLinks.hotelPageRefSchema, + pageLinks.loyaltyPageRefSchema, +]) + +export const linkConnectionRefs = z + .object({ + linkConnection: z.object({ + edges: z.array( + z.object({ + node: linkRefsUnionSchema, + }) + ), + }), + }) + .transform((data) => { + if (data.linkConnection.edges.length) { + const link = pageLinks.transformRef(data.linkConnection.edges[0].node) + if (link) { + return { + link, + } + } + } + return { link: null } + }) diff --git a/types/components/icon.ts b/types/components/icon.ts index f9f61a525..6b11649a2 100644 --- a/types/components/icon.ts +++ b/types/components/icon.ts @@ -21,6 +21,7 @@ export enum IconName { ChevronDown = "ChevronDown", ChevronLeft = "ChevronLeft", ChevronRight = "ChevronRight", + ChevronRightSmall = "ChevronRightSmall", Close = "Close", CloseLarge = "CloseLarge", Coffee = "Coffee", From 6a85cfd19c252f2c214ee19949776e101d1c3925 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Wed, 25 Sep 2024 09:41:42 +0200 Subject: [PATCH 10/31] fix(SW-368): import type fixes --- .../TempDesignSystem/LinkChips/Chip/index.tsx | 4 ++-- .../TempDesignSystem/LinkChips/index.tsx | 3 ++- .../TempDesignSystem/LinkChips/linkChips.ts | 2 +- .../contentstack/contentPage/output.ts | 22 ++++++++----------- .../contentstack/schemas/linkConnection.ts | 2 -- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/components/TempDesignSystem/LinkChips/Chip/index.tsx b/components/TempDesignSystem/LinkChips/Chip/index.tsx index 791037906..6ec804629 100644 --- a/components/TempDesignSystem/LinkChips/Chip/index.tsx +++ b/components/TempDesignSystem/LinkChips/Chip/index.tsx @@ -3,10 +3,10 @@ import Link from "next/link" import { ChevronRightSmallIcon } from "@/components/Icons" import Caption from "@/components/TempDesignSystem/Text/Caption" -import { LinkChipProps } from "./chip" - import styles from "./chip.module.css" +import type { LinkChipProps } from "./chip" + export default function LinkChip({ url, title }: LinkChipProps) { return ( diff --git a/components/TempDesignSystem/LinkChips/index.tsx b/components/TempDesignSystem/LinkChips/index.tsx index 2d6e12881..dccefd9db 100644 --- a/components/TempDesignSystem/LinkChips/index.tsx +++ b/components/TempDesignSystem/LinkChips/index.tsx @@ -1,8 +1,9 @@ import LinkChip from "./Chip" -import { LinkChipsProps } from "./linkChips" import styles from "./linkChips.module.css" +import type { LinkChipsProps } from "./linkChips" + export default function LinkChips({ chips }: LinkChipsProps) { if (!chips.length) { return null diff --git a/components/TempDesignSystem/LinkChips/linkChips.ts b/components/TempDesignSystem/LinkChips/linkChips.ts index 361cd07ed..dcc2be2a3 100644 --- a/components/TempDesignSystem/LinkChips/linkChips.ts +++ b/components/TempDesignSystem/LinkChips/linkChips.ts @@ -1,4 +1,4 @@ -import { LinkChipProps } from "./Chip/chip" +import type { LinkChipProps } from "./Chip/chip" export interface LinkChipsProps { chips: LinkChipProps[] diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 398abb372..d4b02d33e 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -11,8 +11,8 @@ import { contentSchema as blockContentSchema, } from "../schemas/blocks/content" import { - dynamicContentSchema as blockDynamicContentSchema, dynamicContentRefsSchema, + dynamicContentSchema as blockDynamicContentSchema, } from "../schemas/blocks/dynamicContent" import { shortcutsRefsSchema, @@ -21,6 +21,10 @@ import { import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols" import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid" import { tempImageVaultAssetSchema } from "../schemas/imageVault" +import { + linkAndTitleSchema, + linkConnectionRefs, +} from "../schemas/linkConnection" import { contentRefsSchema as sidebarContentRefsSchema, contentSchema as sidebarContentSchema, @@ -32,18 +36,7 @@ import { } from "../schemas/sidebar/joinLoyaltyContact" import { systemSchema } from "../schemas/system" -import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" import { ContentPageEnum } from "@/types/enums/contentPage" -import { - linkAndTitleSchema, - linkConnectionRefs, -} from "../schemas/linkConnection" - -const linkUnionSchema = z.discriminatedUnion("__typename", [ - pageLinks.contentPageSchema, - pageLinks.hotelPageSchema, - pageLinks.loyaltyPageSchema, -]) // Block schemas export const contentPageCards = z @@ -127,7 +120,10 @@ const navigationLinksSchema = z return data .filter((item) => !!item.link) - .map((item) => ({ url: item.link.url, title: item.title })) + .map((item) => ({ + url: item.link!.url, + title: item.title || item.link!.title, + })) }) // Content Page Schema and types diff --git a/server/routers/contentstack/schemas/linkConnection.ts b/server/routers/contentstack/schemas/linkConnection.ts index aff9b980c..f70e8dd81 100644 --- a/server/routers/contentstack/schemas/linkConnection.ts +++ b/server/routers/contentstack/schemas/linkConnection.ts @@ -1,7 +1,5 @@ import { z } from "zod" - - import { discriminatedUnion } from "@/lib/discriminatedUnion" import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" From 2127e05870d61932c9103be16e5797296c821709 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 26 Sep 2024 11:07:03 +0200 Subject: [PATCH 11/31] feat(SW-217): Split up Card to two (Card and TeaserCard) --- components/Blocks/CardsGrid.tsx | 40 +++++----- .../{ContentCard => TeaserCard}/index.tsx | 25 ++++--- .../teaserCard.module.css} | 0 .../{ContentCard => TeaserCard}/variants.ts | 4 +- lib/graphql/Fragments/Blocks/Card.graphql | 5 -- .../Fragments/Blocks/CardsGrid.graphql | 6 +- .../Fragments/Blocks/Refs/TeaserCard.graphql | 36 +++++++++ .../Fragments/Blocks/TeaserCard.graphql | 61 ++++++++++++++++ .../contentstack/schemas/blocks/cardsGrid.ts | 73 +++++++++++++++---- .../{contentCard.ts => teaserCard.ts} | 8 +- types/enums/cardsGrid.ts | 1 + types/trpc/routers/contentstack/blocks.ts | 12 ++- 12 files changed, 205 insertions(+), 66 deletions(-) rename components/TempDesignSystem/{ContentCard => TeaserCard}/index.tsx (82%) rename components/TempDesignSystem/{ContentCard/contentCard.module.css => TeaserCard/teaserCard.module.css} (100%) rename components/TempDesignSystem/{ContentCard => TeaserCard}/variants.ts (75%) create mode 100644 lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql create mode 100644 lib/graphql/Fragments/Blocks/TeaserCard.graphql rename types/components/{contentCard.ts => teaserCard.ts} (67%) diff --git a/components/Blocks/CardsGrid.tsx b/components/Blocks/CardsGrid.tsx index 60e654c1a..839690de8 100644 --- a/components/Blocks/CardsGrid.tsx +++ b/components/Blocks/CardsGrid.tsx @@ -1,10 +1,11 @@ import SectionContainer from "@/components/Section/Container" import SectionHeader from "@/components/Section/Header" import Card from "@/components/TempDesignSystem/Card" -import ContentCard from "@/components/TempDesignSystem/ContentCard" import Grids from "@/components/TempDesignSystem/Grids" import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard" +import TeaserCard from "../TempDesignSystem/TeaserCard" + import type { CardsGridProps } from "@/types/components/blocks/cardsGrid" import { CardsGridEnum } from "@/types/enums/cardsGrid" @@ -22,37 +23,38 @@ export default function CardsGrid({ {cards_grid.cards.map((card) => { switch (card.__typename) { - case CardsGridEnum.cards.Card: { - return card.isContentCard ? ( - + ) + case CardsGridEnum.cards.TeaserCard: + return ( + - ) : ( - ) - } case CardsGridEnum.cards.LoyaltyCard: return ( ) diff --git a/components/TempDesignSystem/ContentCard/index.tsx b/components/TempDesignSystem/TeaserCard/index.tsx similarity index 82% rename from components/TempDesignSystem/ContentCard/index.tsx rename to components/TempDesignSystem/TeaserCard/index.tsx index bdde9de6f..a51c04eed 100644 --- a/components/TempDesignSystem/ContentCard/index.tsx +++ b/components/TempDesignSystem/TeaserCard/index.tsx @@ -7,32 +7,32 @@ import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "../Text/Subtitle" -import { contentCardVariants } from "./variants" +import { teaserCardVariants } from "./variants" -import styles from "./contentCard.module.css" +import styles from "./teaserCard.module.css" -import type { ContentCardProps } from "@/types/components/contentCard" +import type { TeaserCardProps } from "@/types/components/teaserCard" -export default function ContentCard({ +export default function TeaserCard({ title, description, primaryButton, secondaryButton, sidePeekButton, - backgroundImage, + image, style = "default", alwaysStack = false, className, -}: ContentCardProps) { - const cardClasses = contentCardVariants({ style, alwaysStack, className }) +}: TeaserCardProps) { + const cardClasses = teaserCardVariants({ style, alwaysStack, className }) return ( -
- {backgroundImage && ( +
+ {image && (
{backgroundImage.meta?.alt {primaryButton.title} @@ -93,6 +94,6 @@ export default function ContentCard({
)}
- + ) } diff --git a/components/TempDesignSystem/ContentCard/contentCard.module.css b/components/TempDesignSystem/TeaserCard/teaserCard.module.css similarity index 100% rename from components/TempDesignSystem/ContentCard/contentCard.module.css rename to components/TempDesignSystem/TeaserCard/teaserCard.module.css diff --git a/components/TempDesignSystem/ContentCard/variants.ts b/components/TempDesignSystem/TeaserCard/variants.ts similarity index 75% rename from components/TempDesignSystem/ContentCard/variants.ts rename to components/TempDesignSystem/TeaserCard/variants.ts index a9cc2b67f..b6ca7744d 100644 --- a/components/TempDesignSystem/ContentCard/variants.ts +++ b/components/TempDesignSystem/TeaserCard/variants.ts @@ -1,8 +1,8 @@ import { cva } from "class-variance-authority" -import styles from "./contentCard.module.css" +import styles from "./teaserCard.module.css" -export const contentCardVariants = cva(styles.card, { +export const teaserCardVariants = cva(styles.card, { variants: { style: { default: styles.default, diff --git a/lib/graphql/Fragments/Blocks/Card.graphql b/lib/graphql/Fragments/Blocks/Card.graphql index 2adc5a75b..39aa63a49 100644 --- a/lib/graphql/Fragments/Blocks/Card.graphql +++ b/lib/graphql/Fragments/Blocks/Card.graphql @@ -9,9 +9,7 @@ fragment CardBlock on Card { body_text has_primary_button has_secondary_button - has_sidepeek_button heading - is_content_card scripted_top_title title primary_button { @@ -52,9 +50,6 @@ fragment CardBlock on Card { } } } - sidepeek_button { - call_to_action_text - } system { ...System } diff --git a/lib/graphql/Fragments/Blocks/CardsGrid.graphql b/lib/graphql/Fragments/Blocks/CardsGrid.graphql index 5697bba22..ce4eb3886 100644 --- a/lib/graphql/Fragments/Blocks/CardsGrid.graphql +++ b/lib/graphql/Fragments/Blocks/CardsGrid.graphql @@ -1,8 +1,10 @@ #import "./Card.graphql" #import "./LoyaltyCard.graphql" +#import "./TeaserCard.graphql" + #import "./Refs/Card.graphql" #import "./Refs/LoyaltyCard.graphql" - +#import "./Refs/TeaserCard.graphql" fragment CardsGrid_ContentPage on ContentPageBlocksCardsGrid { cards_grid { layout @@ -15,6 +17,7 @@ fragment CardsGrid_ContentPage on ContentPageBlocksCardsGrid { __typename ...CardBlock ...LoyaltyCardBlock + ...TeaserCardBlock } } } @@ -29,6 +32,7 @@ fragment CardsGrid_ContentPageRefs on ContentPageBlocksCardsGrid { __typename ...CardBlockRef ...LoyaltyCardBlockRef + ...TeaserCardBlockRef } } } diff --git a/lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql b/lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql new file mode 100644 index 000000000..c922f8adc --- /dev/null +++ b/lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql @@ -0,0 +1,36 @@ +#import "../../AccountPage/Ref.graphql" +#import "../../ContentPage/Ref.graphql" +#import "../../LoyaltyPage/Ref.graphql" +#import "../../HotelPage/Ref.graphql" + +fragment TeaserCardBlockRef on TeaserCard { + secondary_button { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...LoyaltyPageRef + ...HotelPageRef + } + } + } + } + primary_button { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...LoyaltyPageRef + ...HotelPageRef + } + } + } + } + system { + ...System + } +} diff --git a/lib/graphql/Fragments/Blocks/TeaserCard.graphql b/lib/graphql/Fragments/Blocks/TeaserCard.graphql new file mode 100644 index 000000000..5b333797c --- /dev/null +++ b/lib/graphql/Fragments/Blocks/TeaserCard.graphql @@ -0,0 +1,61 @@ +#import "../System.graphql" + +#import "../PageLink/AccountPageLink.graphql" +#import "../PageLink/ContentPageLink.graphql" +#import "../PageLink/LoyaltyPageLink.graphql" +#import "../PageLink/HotelPageLink.graphql" + +fragment TeaserCardBlock on TeaserCard { + heading + body_text + image + title + has_primary_button + has_secondary_button + has_sidepeek_button + primary_button { + is_contentstack_link + cta_text + open_in_new_tab + external_link { + title + href + } + linkConnection { + edges { + node { + __typename + ...LoyaltyPageLink + ...ContentPageLink + ...AccountPageLink + } + } + } + } + secondary_button { + is_contentstack_link + cta_text + open_in_new_tab + external_link { + title + href + } + linkConnection { + edges { + node { + __typename + ...LoyaltyPageLink + ...ContentPageLink + ...AccountPageLink + ...HotelPageLink + } + } + } + } + sidepeek_button { + call_to_action_text + } + system { + ...System + } +} diff --git a/server/routers/contentstack/schemas/blocks/cardsGrid.ts b/server/routers/contentstack/schemas/blocks/cardsGrid.ts index d9c39bcc5..a73eac737 100644 --- a/server/routers/contentstack/schemas/blocks/cardsGrid.ts +++ b/server/routers/contentstack/schemas/blocks/cardsGrid.ts @@ -15,17 +15,10 @@ export const cardBlockSchema = z.object({ body_text: z.string().optional().default(""), has_primary_button: z.boolean().default(false), has_secondary_button: z.boolean().default(false), - has_sidepeek_button: z.boolean().optional().default(false), heading: z.string().optional().default(""), - is_content_card: z.boolean().optional().default(false), primary_button: buttonSchema, scripted_top_title: z.string().optional(), secondary_button: buttonSchema, - sidepeek_button: z - .object({ - call_to_action_text: z.string().optional(), - }) - .optional(), system: systemSchema, title: z.string().optional(), }) @@ -36,23 +29,53 @@ export function transformCardBlock(card: typeof cardBlockSchema._type) { backgroundImage: card.background_image, body_text: card.body_text, heading: card.heading, - isContentCard: card.is_content_card, primaryButton: card.has_primary_button ? card.primary_button : undefined, scripted_top_title: card.scripted_top_title, secondaryButton: card.has_secondary_button ? card.secondary_button : undefined, - sidePeekButton: - card.has_sidepeek_button && card.sidepeek_button?.call_to_action_text - ? { - title: card.sidepeek_button.call_to_action_text, - } - : undefined, system: card.system, title: card.title, } } +export const teaserCardBlockSchema = z.object({ + __typename: z.literal(CardsGridEnum.cards.TeaserCard), + heading: z.string().default(""), + body_text: z.string().default(""), + image: tempImageVaultAssetSchema, + primary_button: buttonSchema, + secondary_button: buttonSchema, + has_primary_button: z.boolean().default(false), + has_secondary_button: z.boolean().default(false), + has_sidepeek_button: z.boolean().optional().default(false), + side_peek_button: z + .object({ + title: z.string().optional().default(""), + }) + .optional(), + system: systemSchema, +}) + +export function transformTeaserCardBlock( + card: typeof teaserCardBlockSchema._type +) { + return { + __typename: card.__typename, + body_text: card.body_text, + heading: card.heading, + primaryButton: card.has_primary_button ? card.primary_button : undefined, + secondaryButton: card.has_secondary_button + ? card.secondary_button + : undefined, + sidePeekButton: card.has_sidepeek_button + ? card.side_peek_button + : undefined, + image: card.image, + system: card.system, + } +} + const loyaltyCardBlockSchema = z.object({ __typename: z.literal(CardsGridEnum.cards.LoyaltyCard), body_text: z.string().optional(), @@ -77,6 +100,7 @@ export const cardsGridSchema = z.object({ node: z.discriminatedUnion("__typename", [ cardBlockSchema, loyaltyCardBlockSchema, + teaserCardBlockSchema, ]), }) ), @@ -95,6 +119,8 @@ export const cardsGridSchema = z.object({ cards: data.cardConnection.edges.map((card) => { if (card.node.__typename === CardsGridEnum.cards.Card) { return transformCardBlock(card.node) + } else if (card.node.__typename === CardsGridEnum.cards.TeaserCard) { + return transformTeaserCardBlock(card.node) } else { return { __typename: card.node.__typename, @@ -118,7 +144,11 @@ export const cardBlockRefsSchema = z.object({ system: systemSchema, }) -export function transformCardBlockRefs(card: typeof cardBlockRefsSchema._type) { +export function transformCardBlockRefs( + card: + | typeof cardBlockRefsSchema._type + | typeof teaserCardBlockRefsSchema._type +) { const cards = [card.system] if (card.primary_button) { cards.push(card.primary_button) @@ -135,6 +165,13 @@ const loyaltyCardBlockRefsSchema = z.object({ system: systemSchema, }) +export const teaserCardBlockRefsSchema = z.object({ + __typename: z.literal(CardsGridEnum.cards.TeaserCard), + primary_button: linkConnectionRefsSchema, + secondary_button: linkConnectionRefsSchema, + system: systemSchema, +}) + export const cardGridRefsSchema = z.object({ cards_grid: z .object({ @@ -144,6 +181,7 @@ export const cardGridRefsSchema = z.object({ node: z.discriminatedUnion("__typename", [ cardBlockRefsSchema, loyaltyCardBlockRefsSchema, + teaserCardBlockRefsSchema, ]), }) ), @@ -152,7 +190,10 @@ export const cardGridRefsSchema = z.object({ .transform((data) => { return data.cardConnection.edges .map(({ node }) => { - if (node.__typename === CardsGridEnum.cards.Card) { + if ( + node.__typename === CardsGridEnum.cards.Card || + node.__typename === CardsGridEnum.cards.TeaserCard + ) { return transformCardBlockRefs(node) } else { const loyaltyCards = [node.system] diff --git a/types/components/contentCard.ts b/types/components/teaserCard.ts similarity index 67% rename from types/components/contentCard.ts rename to types/components/teaserCard.ts index 8c904a2e3..4ca7f56f1 100644 --- a/types/components/contentCard.ts +++ b/types/components/teaserCard.ts @@ -1,6 +1,6 @@ import { VariantProps } from "class-variance-authority" -import { contentCardVariants } from "@/components/TempDesignSystem/ContentCard/variants" +import { teaserCardVariants } from "@/components/TempDesignSystem/TeaserCard/variants" import { ImageVaultAsset } from "@/types/components/imageVault" import type { CardProps } from "@/components/TempDesignSystem/Card/card" @@ -9,13 +9,13 @@ interface SidePeekButton { title: string } -export interface ContentCardProps - extends VariantProps { +export interface TeaserCardProps + extends VariantProps { title: string description: string primaryButton?: CardProps["primaryButton"] secondaryButton?: CardProps["secondaryButton"] sidePeekButton?: SidePeekButton - backgroundImage?: ImageVaultAsset + image?: ImageVaultAsset className?: string } diff --git a/types/enums/cardsGrid.ts b/types/enums/cardsGrid.ts index c6b6b2983..3bb96c684 100644 --- a/types/enums/cardsGrid.ts +++ b/types/enums/cardsGrid.ts @@ -2,5 +2,6 @@ export namespace CardsGridEnum { export const enum cards { Card = "Card", LoyaltyCard = "LoyaltyCard", + TeaserCard = "TeaserCard", } } diff --git a/types/trpc/routers/contentstack/blocks.ts b/types/trpc/routers/contentstack/blocks.ts index 8d8e449ab..6e513e694 100644 --- a/types/trpc/routers/contentstack/blocks.ts +++ b/types/trpc/routers/contentstack/blocks.ts @@ -4,13 +4,11 @@ import { cardsGridSchema } from "@/server/routers/contentstack/schemas/blocks/ca 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" - import { textColsSchema } from "@/server/routers/contentstack/schemas/blocks/textCols" -export interface CardsGrid extends z.output { } -export interface Content extends z.output { } -export interface DynamicContent extends z.output { } -export interface Shortcuts extends z.output { } +export interface CardsGrid extends z.output {} +export interface Content extends z.output {} +export interface DynamicContent extends z.output {} +export interface Shortcuts extends z.output {} export type Shortcut = Shortcuts["shortcuts"] -export interface TextCols extends z.output { } - +export interface TextCols extends z.output {} From bb93bdf69c3180b1e92fe530ec3650bc08b1bd91 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 26 Sep 2024 11:07:03 +0200 Subject: [PATCH 12/31] feat(SW-217): Split up Card to two (Card and TeaserCard) --- components/Blocks/CardsGrid.tsx | 40 +++++----- .../{ContentCard => TeaserCard}/index.tsx | 25 ++++--- .../teaserCard.module.css} | 0 .../{ContentCard => TeaserCard}/variants.ts | 4 +- lib/graphql/Fragments/Blocks/Card.graphql | 5 -- .../Fragments/Blocks/CardsGrid.graphql | 6 +- .../Fragments/Blocks/Refs/TeaserCard.graphql | 36 +++++++++ .../Fragments/Blocks/TeaserCard.graphql | 61 ++++++++++++++++ .../contentstack/schemas/blocks/cardsGrid.ts | 73 +++++++++++++++---- .../{contentCard.ts => teaserCard.ts} | 8 +- types/enums/cardsGrid.ts | 1 + 11 files changed, 200 insertions(+), 59 deletions(-) rename components/TempDesignSystem/{ContentCard => TeaserCard}/index.tsx (82%) rename components/TempDesignSystem/{ContentCard/contentCard.module.css => TeaserCard/teaserCard.module.css} (100%) rename components/TempDesignSystem/{ContentCard => TeaserCard}/variants.ts (75%) create mode 100644 lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql create mode 100644 lib/graphql/Fragments/Blocks/TeaserCard.graphql rename types/components/{contentCard.ts => teaserCard.ts} (67%) diff --git a/components/Blocks/CardsGrid.tsx b/components/Blocks/CardsGrid.tsx index 60e654c1a..839690de8 100644 --- a/components/Blocks/CardsGrid.tsx +++ b/components/Blocks/CardsGrid.tsx @@ -1,10 +1,11 @@ import SectionContainer from "@/components/Section/Container" import SectionHeader from "@/components/Section/Header" import Card from "@/components/TempDesignSystem/Card" -import ContentCard from "@/components/TempDesignSystem/ContentCard" import Grids from "@/components/TempDesignSystem/Grids" import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard" +import TeaserCard from "../TempDesignSystem/TeaserCard" + import type { CardsGridProps } from "@/types/components/blocks/cardsGrid" import { CardsGridEnum } from "@/types/enums/cardsGrid" @@ -22,37 +23,38 @@ export default function CardsGrid({ {cards_grid.cards.map((card) => { switch (card.__typename) { - case CardsGridEnum.cards.Card: { - return card.isContentCard ? ( - + ) + case CardsGridEnum.cards.TeaserCard: + return ( + - ) : ( - ) - } case CardsGridEnum.cards.LoyaltyCard: return ( ) diff --git a/components/TempDesignSystem/ContentCard/index.tsx b/components/TempDesignSystem/TeaserCard/index.tsx similarity index 82% rename from components/TempDesignSystem/ContentCard/index.tsx rename to components/TempDesignSystem/TeaserCard/index.tsx index bdde9de6f..a51c04eed 100644 --- a/components/TempDesignSystem/ContentCard/index.tsx +++ b/components/TempDesignSystem/TeaserCard/index.tsx @@ -7,32 +7,32 @@ import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "../Text/Subtitle" -import { contentCardVariants } from "./variants" +import { teaserCardVariants } from "./variants" -import styles from "./contentCard.module.css" +import styles from "./teaserCard.module.css" -import type { ContentCardProps } from "@/types/components/contentCard" +import type { TeaserCardProps } from "@/types/components/teaserCard" -export default function ContentCard({ +export default function TeaserCard({ title, description, primaryButton, secondaryButton, sidePeekButton, - backgroundImage, + image, style = "default", alwaysStack = false, className, -}: ContentCardProps) { - const cardClasses = contentCardVariants({ style, alwaysStack, className }) +}: TeaserCardProps) { + const cardClasses = teaserCardVariants({ style, alwaysStack, className }) return ( -
- {backgroundImage && ( +
+ {image && (
{backgroundImage.meta?.alt {primaryButton.title} @@ -93,6 +94,6 @@ export default function ContentCard({
)}
- + ) } diff --git a/components/TempDesignSystem/ContentCard/contentCard.module.css b/components/TempDesignSystem/TeaserCard/teaserCard.module.css similarity index 100% rename from components/TempDesignSystem/ContentCard/contentCard.module.css rename to components/TempDesignSystem/TeaserCard/teaserCard.module.css diff --git a/components/TempDesignSystem/ContentCard/variants.ts b/components/TempDesignSystem/TeaserCard/variants.ts similarity index 75% rename from components/TempDesignSystem/ContentCard/variants.ts rename to components/TempDesignSystem/TeaserCard/variants.ts index a9cc2b67f..b6ca7744d 100644 --- a/components/TempDesignSystem/ContentCard/variants.ts +++ b/components/TempDesignSystem/TeaserCard/variants.ts @@ -1,8 +1,8 @@ import { cva } from "class-variance-authority" -import styles from "./contentCard.module.css" +import styles from "./teaserCard.module.css" -export const contentCardVariants = cva(styles.card, { +export const teaserCardVariants = cva(styles.card, { variants: { style: { default: styles.default, diff --git a/lib/graphql/Fragments/Blocks/Card.graphql b/lib/graphql/Fragments/Blocks/Card.graphql index 2adc5a75b..39aa63a49 100644 --- a/lib/graphql/Fragments/Blocks/Card.graphql +++ b/lib/graphql/Fragments/Blocks/Card.graphql @@ -9,9 +9,7 @@ fragment CardBlock on Card { body_text has_primary_button has_secondary_button - has_sidepeek_button heading - is_content_card scripted_top_title title primary_button { @@ -52,9 +50,6 @@ fragment CardBlock on Card { } } } - sidepeek_button { - call_to_action_text - } system { ...System } diff --git a/lib/graphql/Fragments/Blocks/CardsGrid.graphql b/lib/graphql/Fragments/Blocks/CardsGrid.graphql index 5697bba22..ce4eb3886 100644 --- a/lib/graphql/Fragments/Blocks/CardsGrid.graphql +++ b/lib/graphql/Fragments/Blocks/CardsGrid.graphql @@ -1,8 +1,10 @@ #import "./Card.graphql" #import "./LoyaltyCard.graphql" +#import "./TeaserCard.graphql" + #import "./Refs/Card.graphql" #import "./Refs/LoyaltyCard.graphql" - +#import "./Refs/TeaserCard.graphql" fragment CardsGrid_ContentPage on ContentPageBlocksCardsGrid { cards_grid { layout @@ -15,6 +17,7 @@ fragment CardsGrid_ContentPage on ContentPageBlocksCardsGrid { __typename ...CardBlock ...LoyaltyCardBlock + ...TeaserCardBlock } } } @@ -29,6 +32,7 @@ fragment CardsGrid_ContentPageRefs on ContentPageBlocksCardsGrid { __typename ...CardBlockRef ...LoyaltyCardBlockRef + ...TeaserCardBlockRef } } } diff --git a/lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql b/lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql new file mode 100644 index 000000000..c922f8adc --- /dev/null +++ b/lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql @@ -0,0 +1,36 @@ +#import "../../AccountPage/Ref.graphql" +#import "../../ContentPage/Ref.graphql" +#import "../../LoyaltyPage/Ref.graphql" +#import "../../HotelPage/Ref.graphql" + +fragment TeaserCardBlockRef on TeaserCard { + secondary_button { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...LoyaltyPageRef + ...HotelPageRef + } + } + } + } + primary_button { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...LoyaltyPageRef + ...HotelPageRef + } + } + } + } + system { + ...System + } +} diff --git a/lib/graphql/Fragments/Blocks/TeaserCard.graphql b/lib/graphql/Fragments/Blocks/TeaserCard.graphql new file mode 100644 index 000000000..5b333797c --- /dev/null +++ b/lib/graphql/Fragments/Blocks/TeaserCard.graphql @@ -0,0 +1,61 @@ +#import "../System.graphql" + +#import "../PageLink/AccountPageLink.graphql" +#import "../PageLink/ContentPageLink.graphql" +#import "../PageLink/LoyaltyPageLink.graphql" +#import "../PageLink/HotelPageLink.graphql" + +fragment TeaserCardBlock on TeaserCard { + heading + body_text + image + title + has_primary_button + has_secondary_button + has_sidepeek_button + primary_button { + is_contentstack_link + cta_text + open_in_new_tab + external_link { + title + href + } + linkConnection { + edges { + node { + __typename + ...LoyaltyPageLink + ...ContentPageLink + ...AccountPageLink + } + } + } + } + secondary_button { + is_contentstack_link + cta_text + open_in_new_tab + external_link { + title + href + } + linkConnection { + edges { + node { + __typename + ...LoyaltyPageLink + ...ContentPageLink + ...AccountPageLink + ...HotelPageLink + } + } + } + } + sidepeek_button { + call_to_action_text + } + system { + ...System + } +} diff --git a/server/routers/contentstack/schemas/blocks/cardsGrid.ts b/server/routers/contentstack/schemas/blocks/cardsGrid.ts index d9c39bcc5..a73eac737 100644 --- a/server/routers/contentstack/schemas/blocks/cardsGrid.ts +++ b/server/routers/contentstack/schemas/blocks/cardsGrid.ts @@ -15,17 +15,10 @@ export const cardBlockSchema = z.object({ body_text: z.string().optional().default(""), has_primary_button: z.boolean().default(false), has_secondary_button: z.boolean().default(false), - has_sidepeek_button: z.boolean().optional().default(false), heading: z.string().optional().default(""), - is_content_card: z.boolean().optional().default(false), primary_button: buttonSchema, scripted_top_title: z.string().optional(), secondary_button: buttonSchema, - sidepeek_button: z - .object({ - call_to_action_text: z.string().optional(), - }) - .optional(), system: systemSchema, title: z.string().optional(), }) @@ -36,23 +29,53 @@ export function transformCardBlock(card: typeof cardBlockSchema._type) { backgroundImage: card.background_image, body_text: card.body_text, heading: card.heading, - isContentCard: card.is_content_card, primaryButton: card.has_primary_button ? card.primary_button : undefined, scripted_top_title: card.scripted_top_title, secondaryButton: card.has_secondary_button ? card.secondary_button : undefined, - sidePeekButton: - card.has_sidepeek_button && card.sidepeek_button?.call_to_action_text - ? { - title: card.sidepeek_button.call_to_action_text, - } - : undefined, system: card.system, title: card.title, } } +export const teaserCardBlockSchema = z.object({ + __typename: z.literal(CardsGridEnum.cards.TeaserCard), + heading: z.string().default(""), + body_text: z.string().default(""), + image: tempImageVaultAssetSchema, + primary_button: buttonSchema, + secondary_button: buttonSchema, + has_primary_button: z.boolean().default(false), + has_secondary_button: z.boolean().default(false), + has_sidepeek_button: z.boolean().optional().default(false), + side_peek_button: z + .object({ + title: z.string().optional().default(""), + }) + .optional(), + system: systemSchema, +}) + +export function transformTeaserCardBlock( + card: typeof teaserCardBlockSchema._type +) { + return { + __typename: card.__typename, + body_text: card.body_text, + heading: card.heading, + primaryButton: card.has_primary_button ? card.primary_button : undefined, + secondaryButton: card.has_secondary_button + ? card.secondary_button + : undefined, + sidePeekButton: card.has_sidepeek_button + ? card.side_peek_button + : undefined, + image: card.image, + system: card.system, + } +} + const loyaltyCardBlockSchema = z.object({ __typename: z.literal(CardsGridEnum.cards.LoyaltyCard), body_text: z.string().optional(), @@ -77,6 +100,7 @@ export const cardsGridSchema = z.object({ node: z.discriminatedUnion("__typename", [ cardBlockSchema, loyaltyCardBlockSchema, + teaserCardBlockSchema, ]), }) ), @@ -95,6 +119,8 @@ export const cardsGridSchema = z.object({ cards: data.cardConnection.edges.map((card) => { if (card.node.__typename === CardsGridEnum.cards.Card) { return transformCardBlock(card.node) + } else if (card.node.__typename === CardsGridEnum.cards.TeaserCard) { + return transformTeaserCardBlock(card.node) } else { return { __typename: card.node.__typename, @@ -118,7 +144,11 @@ export const cardBlockRefsSchema = z.object({ system: systemSchema, }) -export function transformCardBlockRefs(card: typeof cardBlockRefsSchema._type) { +export function transformCardBlockRefs( + card: + | typeof cardBlockRefsSchema._type + | typeof teaserCardBlockRefsSchema._type +) { const cards = [card.system] if (card.primary_button) { cards.push(card.primary_button) @@ -135,6 +165,13 @@ const loyaltyCardBlockRefsSchema = z.object({ system: systemSchema, }) +export const teaserCardBlockRefsSchema = z.object({ + __typename: z.literal(CardsGridEnum.cards.TeaserCard), + primary_button: linkConnectionRefsSchema, + secondary_button: linkConnectionRefsSchema, + system: systemSchema, +}) + export const cardGridRefsSchema = z.object({ cards_grid: z .object({ @@ -144,6 +181,7 @@ export const cardGridRefsSchema = z.object({ node: z.discriminatedUnion("__typename", [ cardBlockRefsSchema, loyaltyCardBlockRefsSchema, + teaserCardBlockRefsSchema, ]), }) ), @@ -152,7 +190,10 @@ export const cardGridRefsSchema = z.object({ .transform((data) => { return data.cardConnection.edges .map(({ node }) => { - if (node.__typename === CardsGridEnum.cards.Card) { + if ( + node.__typename === CardsGridEnum.cards.Card || + node.__typename === CardsGridEnum.cards.TeaserCard + ) { return transformCardBlockRefs(node) } else { const loyaltyCards = [node.system] diff --git a/types/components/contentCard.ts b/types/components/teaserCard.ts similarity index 67% rename from types/components/contentCard.ts rename to types/components/teaserCard.ts index 8c904a2e3..4ca7f56f1 100644 --- a/types/components/contentCard.ts +++ b/types/components/teaserCard.ts @@ -1,6 +1,6 @@ import { VariantProps } from "class-variance-authority" -import { contentCardVariants } from "@/components/TempDesignSystem/ContentCard/variants" +import { teaserCardVariants } from "@/components/TempDesignSystem/TeaserCard/variants" import { ImageVaultAsset } from "@/types/components/imageVault" import type { CardProps } from "@/components/TempDesignSystem/Card/card" @@ -9,13 +9,13 @@ interface SidePeekButton { title: string } -export interface ContentCardProps - extends VariantProps { +export interface TeaserCardProps + extends VariantProps { title: string description: string primaryButton?: CardProps["primaryButton"] secondaryButton?: CardProps["secondaryButton"] sidePeekButton?: SidePeekButton - backgroundImage?: ImageVaultAsset + image?: ImageVaultAsset className?: string } diff --git a/types/enums/cardsGrid.ts b/types/enums/cardsGrid.ts index c6b6b2983..3bb96c684 100644 --- a/types/enums/cardsGrid.ts +++ b/types/enums/cardsGrid.ts @@ -2,5 +2,6 @@ export namespace CardsGridEnum { export const enum cards { Card = "Card", LoyaltyCard = "LoyaltyCard", + TeaserCard = "TeaserCard", } } From 56cd02f90bb5d51308b25a60c7cd1dbf7b72d286 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 25 Sep 2024 15:59:16 +0200 Subject: [PATCH 13/31] feat(SW-353): dynamic rewards --- app/api/web/revalidate/loyaltyConfig/route.ts | 103 ++++ .../Benefits/CurrentLevel/index.tsx | 58 -- .../DynamicContent/LoyaltyLevels/index.tsx | 93 +-- .../Overview/Friend/MembershipLevel/index.tsx | 36 -- .../membershipLevel.module.css | 4 - .../DynamicContent/Overview/Friend/index.tsx | 23 +- .../Overview/Stats/Points/index.tsx | 13 +- .../OverviewTable/BenefitCard/index.tsx | 53 -- .../OverviewTable/BenefitList/index.tsx | 31 - .../OverviewTable/BenefitValue/index.tsx | 26 - .../DynamicContent/OverviewTable/Client.tsx | 189 +++--- .../LargeTable/DesktopHeader/index.tsx | 18 +- .../OverviewTable/LargeTable/index.tsx | 45 +- .../LargeTable/largeTable.module.css | 6 +- .../OverviewTable/LevelSummary/index.tsx | 14 +- .../OverviewTable/RewardList/Card/index.tsx | 48 ++ .../Card/rewardCard.module.css} | 12 +- .../OverviewTable/RewardList/index.tsx | 38 ++ .../rewardList.module.css} | 6 +- .../OverviewTable/RewardValue/index.tsx | 21 + .../rewardValue.module.css} | 6 +- .../DynamicContent/OverviewTable/data/DA.json | 538 ------------------ .../DynamicContent/OverviewTable/data/DE.json | 538 ------------------ .../DynamicContent/OverviewTable/data/EN.json | 538 ------------------ .../DynamicContent/OverviewTable/data/FI.json | 538 ------------------ .../DynamicContent/OverviewTable/data/NO.json | 538 ------------------ .../DynamicContent/OverviewTable/data/SV.json | 538 ------------------ .../DynamicContent/OverviewTable/index.tsx | 8 +- .../Points/Overview/Points/index.tsx | 13 +- .../Rewards/CurrentLevel/Client.tsx | 70 +++ .../CurrentLevel/current.module.css | 0 .../Rewards/CurrentLevel/index.tsx | 32 ++ .../{Benefits => Rewards}/NextLevel/index.tsx | 37 +- .../NextLevel/next.module.css | 0 components/Blocks/DynamicContent/index.tsx | 8 +- .../Header/MainMenu/MyPagesMenu/index.tsx | 3 +- .../MainMenu/MyPagesMenuContent/index.tsx | 22 +- components/Levels/Icon.tsx | 38 ++ components/Levels/Level/BestFriend.tsx | 12 +- components/Levels/Level/CloseFriend.tsx | 6 +- components/Levels/Level/DearFriend.tsx | 12 +- components/Levels/Level/GoodFriend.tsx | 12 +- components/Levels/Level/LoyalFriend.tsx | 6 +- components/Levels/Level/NewFriend.tsx | 12 +- components/Levels/Level/TrueFriend.tsx | 12 +- components/Levels/levels.ts | 5 +- .../ShowMoreButton/button.module.css | 4 + .../TempDesignSystem/ShowMoreButton/index.tsx | 32 ++ components/Webviews/AccountPage/Blocks.tsx | 8 +- data/loyaltyLevels/DA.json | 117 ---- data/loyaltyLevels/DE.json | 117 ---- data/loyaltyLevels/EN.json | 117 ---- data/loyaltyLevels/FI.json | 117 ---- data/loyaltyLevels/NO.json | 117 ---- data/loyaltyLevels/SV.json | 117 ---- data/loyaltyLevels/index.ts | 19 - lib/api/endpoints.ts | 2 + lib/graphql/Query/LoyaltyLevels.graphql | 25 + lib/graphql/Query/Rewards.graphql | 15 + server/routers/contentstack/index.ts | 4 + .../contentstack/loyaltyLevel/index.ts | 5 + .../contentstack/loyaltyLevel/input.ts | 7 + .../contentstack/loyaltyLevel/output.ts | 24 + .../contentstack/loyaltyLevel/query.ts | 147 +++++ server/routers/contentstack/reward/index.ts | 5 + server/routers/contentstack/reward/input.ts | 19 + server/routers/contentstack/reward/output.ts | 82 +++ server/routers/contentstack/reward/query.ts | 371 ++++++++++++ server/routers/hotels/query.ts | 64 +-- server/tokenManager.ts | 6 +- server/trpc.ts | 42 +- types/components/header/myPagesMenu.ts | 4 +- types/components/myPages/membership.ts | 10 +- types/components/overviewTable.ts | 65 +-- utils/generateTag.ts | 16 + utils/loyaltyTable.ts | 50 +- utils/membershipLevel.ts | 16 - utils/user.ts | 32 +- 78 files changed, 1568 insertions(+), 4587 deletions(-) create mode 100644 app/api/web/revalidate/loyaltyConfig/route.ts delete mode 100644 components/Blocks/DynamicContent/Benefits/CurrentLevel/index.tsx delete mode 100644 components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/index.tsx delete mode 100644 components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/membershipLevel.module.css delete mode 100644 components/Blocks/DynamicContent/OverviewTable/BenefitCard/index.tsx delete mode 100644 components/Blocks/DynamicContent/OverviewTable/BenefitList/index.tsx delete mode 100644 components/Blocks/DynamicContent/OverviewTable/BenefitValue/index.tsx create mode 100644 components/Blocks/DynamicContent/OverviewTable/RewardList/Card/index.tsx rename components/Blocks/DynamicContent/OverviewTable/{BenefitCard/benefitCard.module.css => RewardList/Card/rewardCard.module.css} (87%) create mode 100644 components/Blocks/DynamicContent/OverviewTable/RewardList/index.tsx rename components/Blocks/DynamicContent/OverviewTable/{BenefitList/benefitList.module.css => RewardList/rewardList.module.css} (79%) create mode 100644 components/Blocks/DynamicContent/OverviewTable/RewardValue/index.tsx rename components/Blocks/DynamicContent/OverviewTable/{BenefitValue/benefitValue.module.css => RewardValue/rewardValue.module.css} (85%) delete mode 100644 components/Blocks/DynamicContent/OverviewTable/data/DA.json delete mode 100644 components/Blocks/DynamicContent/OverviewTable/data/DE.json delete mode 100644 components/Blocks/DynamicContent/OverviewTable/data/EN.json delete mode 100644 components/Blocks/DynamicContent/OverviewTable/data/FI.json delete mode 100644 components/Blocks/DynamicContent/OverviewTable/data/NO.json delete mode 100644 components/Blocks/DynamicContent/OverviewTable/data/SV.json create mode 100644 components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx rename components/Blocks/DynamicContent/{Benefits => Rewards}/CurrentLevel/current.module.css (100%) create mode 100644 components/Blocks/DynamicContent/Rewards/CurrentLevel/index.tsx rename components/Blocks/DynamicContent/{Benefits => Rewards}/NextLevel/index.tsx (65%) rename components/Blocks/DynamicContent/{Benefits => Rewards}/NextLevel/next.module.css (100%) create mode 100644 components/Levels/Icon.tsx create mode 100644 components/TempDesignSystem/ShowMoreButton/button.module.css create mode 100644 components/TempDesignSystem/ShowMoreButton/index.tsx delete mode 100644 data/loyaltyLevels/DA.json delete mode 100644 data/loyaltyLevels/DE.json delete mode 100644 data/loyaltyLevels/EN.json delete mode 100644 data/loyaltyLevels/FI.json delete mode 100644 data/loyaltyLevels/NO.json delete mode 100644 data/loyaltyLevels/SV.json delete mode 100644 data/loyaltyLevels/index.ts create mode 100644 lib/graphql/Query/LoyaltyLevels.graphql create mode 100644 lib/graphql/Query/Rewards.graphql create mode 100644 server/routers/contentstack/loyaltyLevel/index.ts create mode 100644 server/routers/contentstack/loyaltyLevel/input.ts create mode 100644 server/routers/contentstack/loyaltyLevel/output.ts create mode 100644 server/routers/contentstack/loyaltyLevel/query.ts create mode 100644 server/routers/contentstack/reward/index.ts create mode 100644 server/routers/contentstack/reward/input.ts create mode 100644 server/routers/contentstack/reward/output.ts create mode 100644 server/routers/contentstack/reward/query.ts delete mode 100644 utils/membershipLevel.ts diff --git a/app/api/web/revalidate/loyaltyConfig/route.ts b/app/api/web/revalidate/loyaltyConfig/route.ts new file mode 100644 index 000000000..da036cbd6 --- /dev/null +++ b/app/api/web/revalidate/loyaltyConfig/route.ts @@ -0,0 +1,103 @@ +import { revalidateTag } from "next/cache" +import { headers } from "next/headers" +import { NextRequest } from "next/server" +import { z } from "zod" + +import { Lang } from "@/constants/languages" +import { env } from "@/env/server" +import { internalServerError } from "@/server/errors/next" + +import { generateLoyaltyConfigTag } from "@/utils/generateTag" + +enum LoyaltyConfigContentTypes { + loyalty_level = "loyalty_level", + rewards = "rewards", +} + +const validateJsonBody = z.object({ + data: z.object({ + content_type: z.object({ + uid: z.nativeEnum(LoyaltyConfigContentTypes), + }), + entry: z.object({ + reward_id: z.string().optional(), + level_id: z.string().optional(), + system: z.object({ + locale: z.nativeEnum(Lang), + }), + }), + }), +}) + +export async function POST(request: NextRequest) { + try { + const headersList = headers() + const secret = headersList.get("x-revalidate-secret") + + if (secret !== env.REVALIDATE_SECRET) { + console.error(`Invalid Secret`) + console.error({ secret }) + return Response.json( + { + now: Date.now(), + revalidated: false, + }, + { + status: 400, + } + ) + } + + const data = await request.json() + const validatedData = validateJsonBody.safeParse(data) + if (!validatedData.success) { + console.error( + "Bad validation for `validatedData` in loyaltyConfig revalidation" + ) + console.error(validatedData.error) + return internalServerError({ revalidated: false, now: Date.now() }) + } + + const { + data: { + data: { content_type, entry }, + }, + } = validatedData + + let tag = "" + if ( + content_type.uid === LoyaltyConfigContentTypes.loyalty_level && + entry.level_id + ) { + tag = generateLoyaltyConfigTag( + entry.system.locale, + content_type.uid, + entry.level_id + ) + } else if ( + content_type.uid === LoyaltyConfigContentTypes.rewards && + entry.reward_id + ) { + tag = generateLoyaltyConfigTag( + entry.system.locale, + content_type.uid, + entry.reward_id + ) + } else { + console.error("Invalid content_type") + return Response.json( + { revalidated: false, now: Date.now() }, + { status: 404 } + ) + } + + console.info(`Revalidating loyalty config tag: ${tag}`) + revalidateTag(tag) + + return Response.json({ revalidated: true, now: Date.now() }) + } catch (error) { + console.error("Failed to revalidate tag(s) for loyalty config") + console.error(error) + return internalServerError({ revalidated: false, now: Date.now() }) + } +} diff --git a/components/Blocks/DynamicContent/Benefits/CurrentLevel/index.tsx b/components/Blocks/DynamicContent/Benefits/CurrentLevel/index.tsx deleted file mode 100644 index 31a920f12..000000000 --- a/components/Blocks/DynamicContent/Benefits/CurrentLevel/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { MembershipLevelEnum } from "@/constants/membershipLevels" -import { getProfile } from "@/lib/trpc/memoizedRequests" - -import SectionContainer from "@/components/Section/Container" -import SectionHeader from "@/components/Section/Header" -import SectionLink from "@/components/Section/Link" -import Grids from "@/components/TempDesignSystem/Grids" -import Title from "@/components/TempDesignSystem/Text/Title" -import { getLang } from "@/i18n/serverContext" -import { getMembershipLevelObject } from "@/utils/membershipLevel" - -import styles from "./current.module.css" - -import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage" - -export default async function CurrentBenefitsBlock({ - title, - subtitle, - link, -}: AccountPageComponentProps) { - const user = await getProfile() - // TAKE NOTE: we need clarification on how benefits stack from different levels - // in order to determine if a benefit is specific to a level or if it is a cumulative benefit - // we might have to add a new boolean property "exclusive" or similar - if (!user || "error" in user || !user.membership) { - return null - } - - const currentLevel = getMembershipLevelObject( - user.membership.membershipLevel as MembershipLevelEnum, - getLang() - ) - if (!currentLevel) { - // TODO: handle this case? - return null - } - - return ( - - - - {currentLevel.benefits.map((benefit, idx) => ( -
- - {benefit.title} - -
- ))} -
- -
- ) -} diff --git a/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx b/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx index 1b3c30154..f23130e7d 100644 --- a/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx +++ b/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx @@ -1,88 +1,43 @@ -"use client" - -import { notFound, useParams } from "next/navigation" -import { useIntl } from "react-intl" - -import { Lang } from "@/constants/languages" +import { serverClient } from "@/lib/trpc/server" import { CheckIcon } from "@/components/Icons" -import { - BestFriend, - CloseFriend, - DearFriend, - GoodFriend, - LoyalFriend, - NewFriend, - TrueFriend, -} from "@/components/Levels" +import MembershipLevelIcon from "@/components/Levels/Icon" import BiroScript from "@/components/TempDesignSystem/Text/BiroScript" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" -import levelsData from "@/data/loyaltyLevels" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" import SectionWrapper from "../SectionWrapper" import styles from "./loyaltyLevels.module.css" -import type { LoyaltyLevelsProps } from "@/types/components/blocks/dynamicContent" -import type { Level, LevelCardProps } from "@/types/components/overviewTable" +import { LoyaltyLevelsProps } from "@/types/components/blocks/dynamicContent" +import type { LevelCardProps } from "@/types/components/overviewTable" -export default function LoyaltyLevels({ +export default async function LoyaltyLevels({ dynamic_content, firstItem, }: LoyaltyLevelsProps) { - const params = useParams() - const lang = params.lang as Lang - const { formatMessage } = useIntl() + const uniqueLevels = await serverClient().contentstack.rewards.all({ + unique: true, + }) - const { levels } = levelsData[lang] return (
- {levels.map((level: Level) => ( - + {uniqueLevels.map((level) => ( + ))}
) } -function LevelCard({ formatMessage, lang, level }: LevelCardProps) { - let Level = null - switch (level.level) { - case 1: - Level = NewFriend - break - case 2: - Level = GoodFriend - break - case 3: - Level = CloseFriend - break - case 4: - Level = DearFriend - break - case 5: - Level = LoyalFriend - break - case 6: - Level = TrueFriend - break - case 7: - Level = BestFriend - break - default: { - const loyaltyLevel = level.level as never - console.error(`Unsupported loyalty level given: ${loyaltyLevel}`) - notFound() - } - } - const pointsString = `${level.requiredPoints.toLocaleString(lang)} ${formatMessage({ id: "points" })} ` +async function LevelCard({ level }: LevelCardProps) { + const lang = getLang() + const intl = await getIntl() + const pointsString = `${level.required_points.toLocaleString(lang)} ${intl.formatMessage({ id: "points" })} ` return (
@@ -92,24 +47,24 @@ function LevelCard({ formatMessage, lang, level }: LevelCardProps) { color="primaryLightOnSurfaceAccent" tilted="large" > - {formatMessage({ id: "Level" })} {level.level} + {intl.formatMessage({ id: "Level" })} {level.user_facing_tag} - + {pointsString} - {level.requiredNights ? ( + {level.required_nights ? ( <span className={styles.redText}> - {formatMessage({ id: "or" })} {level.requiredNights}{" "} - {formatMessage({ id: "nights" })} + {intl.formatMessage({ id: "or" })} {level.required_nights}{" "} + {intl.formatMessage({ id: "nights" })} </span> ) : null}
- {level.benefits.map((benefit) => ( + {level.rewards.map((reward) => ( @@ -117,7 +72,7 @@ function LevelCard({ formatMessage, lang, level }: LevelCardProps) { className={styles.checkIcon} color="primaryLightOnSurfaceAccent" /> - {benefit.title} + {reward.label} ))}
diff --git a/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/index.tsx b/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/index.tsx deleted file mode 100644 index 07d4743ee..000000000 --- a/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { membershipLevels } from "@/constants/membershipLevels" - -import { - BestFriend, - CloseFriend, - DearFriend, - GoodFriend, - LoyalFriend, - NewFriend, - TrueFriend, -} from "@/components/Levels" - -import styles from "./membershipLevel.module.css" - -import type { MembershipLevelProps } from "@/types/components/myPages/membership" - -export default function MembershipLevel({ level }: MembershipLevelProps) { - switch (level) { - case membershipLevels.L1: - return - case membershipLevels.L2: - return - case membershipLevels.L3: - return - case membershipLevels.L4: - return - case membershipLevels.L5: - return - case membershipLevels.L6: - return - case membershipLevels.L7: - return - default: - return null - } -} diff --git a/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/membershipLevel.module.css b/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/membershipLevel.module.css deleted file mode 100644 index 25b922864..000000000 --- a/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/membershipLevel.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.level { - height: 105px; - width: 219px; -} diff --git a/components/Blocks/DynamicContent/Overview/Friend/index.tsx b/components/Blocks/DynamicContent/Overview/Friend/index.tsx index c581765b6..3f1b30ed1 100644 --- a/components/Blocks/DynamicContent/Overview/Friend/index.tsx +++ b/components/Blocks/DynamicContent/Overview/Friend/index.tsx @@ -1,12 +1,14 @@ -import { membershipLevels } from "@/constants/membershipLevels" +import { + MembershipLevelEnum, + membershipLevels, +} from "@/constants/membershipLevels" +import MembershipLevelIcon from "@/components/Levels/Icon" import Body from "@/components/TempDesignSystem/Text/Body" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { isHighestMembership } from "@/utils/user" -import MembershipLevel from "./MembershipLevel" - import styles from "./friend.module.css" import type { FriendProps } from "@/types/components/myPages/friend" @@ -20,7 +22,6 @@ export default async function Friend({ if (!membership?.membershipLevel) { return null } - // @ts-expect-error: membershiplevel needs proper fix const isHighestLevel = isHighestMembership(membership.membershipLevel) return ( @@ -30,16 +31,14 @@ export default async function Friend({ {formatMessage( isHighestLevel ? { id: "Highest level" } - : // @ts-expect-error: membershiplevel needs proper fix - { id: `Level ${membershipLevels[membership.membershipLevel]}` } + : { id: `Level ${membershipLevels[membership.membershipLevel]}` } )} - {membership ? ( - - ) : null} +
diff --git a/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx b/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx index 09784bfe1..a17e8d0ee 100644 --- a/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx +++ b/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx @@ -1,8 +1,7 @@ import { MembershipLevelEnum } from "@/constants/membershipLevels" +import { serverClient } from "@/lib/trpc/server" import { getIntl } from "@/i18n" -import { getLang } from "@/i18n/serverContext" -import { getMembershipLevelObject } from "@/utils/membershipLevel" import { getMembership } from "@/utils/user" import PointsContainer from "./Container" @@ -14,10 +13,12 @@ export default async function Points({ user }: UserProps) { const { formatMessage } = await getIntl() const membership = getMembership(user.memberships) - const nextLevel = getMembershipLevelObject( - membership?.nextLevel as MembershipLevelEnum, - getLang() - ) + + const nextLevel = membership?.nextLevel + ? await serverClient().contentstack.loyaltyLevels.byLevel({ + level: MembershipLevelEnum[membership.nextLevel], + }) + : null return ( <PointsContainer> diff --git a/components/Blocks/DynamicContent/OverviewTable/BenefitCard/index.tsx b/components/Blocks/DynamicContent/OverviewTable/BenefitCard/index.tsx deleted file mode 100644 index 719f2d2c1..000000000 --- a/components/Blocks/DynamicContent/OverviewTable/BenefitCard/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { ChevronDown } from "react-feather" - -import Title from "@/components/TempDesignSystem/Text/Title" - -import BenefitValue from "../BenefitValue" - -import styles from "./benefitCard.module.css" - -import type { BenefitCardProps } from "@/types/components/overviewTable" - -export default function BenefitCard({ - comparedValues, - title, - description, -}: BenefitCardProps) { - return ( - <div className={styles.benefitCard}> - <div className={styles.benefitInfo}> - <details className={styles.details}> - <summary className={styles.summary}> - <hgroup className={styles.benefitCardHeader}> - <Title - as="h5" - level="h2" - textTransform={"regular"} - className={styles.benefitCardTitle} - > - {title} - - - - - - -

- -

-
- {comparedValues.map((benefit, idx) => ( -
- -
- ))} -
- - ) -} diff --git a/components/Blocks/DynamicContent/OverviewTable/BenefitList/index.tsx b/components/Blocks/DynamicContent/OverviewTable/BenefitList/index.tsx deleted file mode 100644 index 4756ea7d2..000000000 --- a/components/Blocks/DynamicContent/OverviewTable/BenefitList/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { findBenefit, getUnlockedBenefits } from "@/utils/loyaltyTable" - -import BenefitCard from "../BenefitCard" - -import styles from "./benefitList.module.css" - -import type { BenefitListProps } from "@/types/components/overviewTable" - -export default function BenefitList({ levels }: BenefitListProps) { - return getUnlockedBenefits(levels).map((benefit) => { - const levelBenefits = levels.map((level) => { - return findBenefit(benefit, level) - }) - return ( -
- { - return { - key: `${benefit.name}-${idx}`, - value: benefit.value, - unlocked: benefit.unlocked, - valueDetails: benefit.valueDetails, - } - })} - /> -
- ) - }) -} diff --git a/components/Blocks/DynamicContent/OverviewTable/BenefitValue/index.tsx b/components/Blocks/DynamicContent/OverviewTable/BenefitValue/index.tsx deleted file mode 100644 index 22f4313a3..000000000 --- a/components/Blocks/DynamicContent/OverviewTable/BenefitValue/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Minus } from "react-feather" - -import CheckCircle from "@/components/Icons/CheckCircle" - -import styles from "./benefitValue.module.css" - -import type { BenefitValueProps } from "@/types/components/overviewTable" - -export default function BenefitValue({ benefit }: BenefitValueProps) { - if (!benefit.unlocked) { - return - } - if (!benefit.value) { - return - } - return ( -
- {benefit.value} - {benefit.valueDetails && ( - - {benefit.valueDetails} - - )} -
- ) -} diff --git a/components/Blocks/DynamicContent/OverviewTable/Client.tsx b/components/Blocks/DynamicContent/OverviewTable/Client.tsx index d21db20df..b7d82badf 100644 --- a/components/Blocks/DynamicContent/OverviewTable/Client.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/Client.tsx @@ -3,23 +3,18 @@ import { useReducer } from "react" import { useIntl } from "react-intl" -import { Lang } from "@/constants/languages" -import { membershipLevels } from "@/constants/membershipLevels" +import { + MembershipLevel, + MembershipLevelEnum, +} from "@/constants/membershipLevels" -import Image from "@/components/Image" +import MembershipLevelIcon from "@/components/Levels/Icon" import Select from "@/components/TempDesignSystem/Select" -import useLang from "@/hooks/useLang" -import { getMembership } from "@/utils/user" +import { getSteppedUpLevel } from "@/utils/user" -import DA from "./data/DA.json" -import DE from "./data/DE.json" -import EN from "./data/EN.json" -import FI from "./data/FI.json" -import NO from "./data/NO.json" -import SV from "./data/SV.json" -import BenefitList from "./BenefitList" import LargeTable from "./LargeTable" import LevelSummary from "./LevelSummary" +import RewardList from "./RewardList" import YourLevel from "./YourLevelScript" import styles from "./overviewTable.module.css" @@ -29,94 +24,91 @@ import type { Key } from "react-aria-components" import { ComparisonLevel, DesktopSelectColumns, + LevelWithRewards, type MobileColumnHeaderProps, - overviewTableActionsEnum, + OverviewTableActionsEnum, type OverviewTableClientProps, OverviewTableReducerAction, } from "@/types/components/overviewTable" -import type { User } from "@/types/user" -const levelsTranslations = { - [Lang.en]: EN, - [Lang.sv]: SV, - [Lang.no]: NO, - [Lang.da]: DA, - [Lang.fi]: FI, - [Lang.de]: DE, +function getLevelNamesForSelect(level: MembershipLevel, levelName: string) { + const levelToNumber = MembershipLevelEnum[level] + return [levelToNumber, levelName].join(" - ") } -function getTranslatedLevel(membershipLevel: membershipLevels, lang: Lang) { - return levelsTranslations[lang].levels.find( - (level) => level.level === membershipLevel - ) as ComparisonLevel +function getLevel( + membershipLevel: MembershipLevel, + levels: LevelWithRewards[] +) { + return levels.find((level) => level.level_id === membershipLevel)! } -function getInitialState({ user, lang }: { user?: User; lang: Lang }) { - const membership = user?.memberships ? getMembership(user.memberships) : null - if (!membership?.membershipLevel) { +function getInitialState({ + activeMembership, + levels, +}: OverviewTableClientProps) { + if (!activeMembership) { return { - selectedLevelAMobile: getTranslatedLevel(1, lang), - selectedLevelBMobile: getTranslatedLevel(2, lang), - selectedLevelADesktop: getTranslatedLevel(1, lang), - selectedLevelBDesktop: getTranslatedLevel(2, lang), - selectedLevelCDesktop: getTranslatedLevel(3, lang), + selectedLevelAMobile: getLevel(MembershipLevelEnum.L1, levels), + selectedLevelBMobile: getLevel(MembershipLevelEnum.L2, levels), + selectedLevelADesktop: getLevel(MembershipLevelEnum.L1, levels), + selectedLevelBDesktop: getLevel(MembershipLevelEnum.L2, levels), + selectedLevelCDesktop: getLevel(MembershipLevelEnum.L3, levels), } } - if (!membership.membershipLevel) return null - // @ts-expect-error: membership levels needs proper fix - const level = membershipLevels[membership.membershipLevel] + const level = MembershipLevelEnum[activeMembership] switch (level) { - case 6: + case MembershipLevelEnum.L6: return { - selectedLevelAMobile: getTranslatedLevel(6, lang), - selectedLevelBMobile: getTranslatedLevel(7, lang), - selectedLevelADesktop: getTranslatedLevel(5, lang), - selectedLevelBDesktop: getTranslatedLevel(6, lang), - selectedLevelCDesktop: getTranslatedLevel(7, lang), + selectedLevelAMobile: getLevel(MembershipLevelEnum.L6, levels), + selectedLevelBMobile: getLevel(MembershipLevelEnum.L7, levels), + selectedLevelADesktop: getLevel(MembershipLevelEnum.L5, levels), + selectedLevelBDesktop: getLevel(MembershipLevelEnum.L6, levels), + selectedLevelCDesktop: getLevel(MembershipLevelEnum.L7, levels), } - case 7: + case MembershipLevelEnum.L7: return { - selectedLevelAMobile: getTranslatedLevel(6, lang), - selectedLevelBMobile: getTranslatedLevel(7, lang), - selectedLevelADesktop: getTranslatedLevel(6, lang), - selectedLevelBDesktop: getTranslatedLevel(7, lang), - selectedLevelCDesktop: getTranslatedLevel(1, lang), + selectedLevelAMobile: getLevel(MembershipLevelEnum.L6, levels), + selectedLevelBMobile: getLevel(MembershipLevelEnum.L7, levels), + selectedLevelADesktop: getLevel(MembershipLevelEnum.L6, levels), + selectedLevelBDesktop: getLevel(MembershipLevelEnum.L7, levels), + selectedLevelCDesktop: getLevel(MembershipLevelEnum.L1, levels), } default: return { - selectedLevelAMobile: getTranslatedLevel(level, lang), - selectedLevelBMobile: getTranslatedLevel(level + 1, lang), - selectedLevelADesktop: getTranslatedLevel(level, lang), - selectedLevelBDesktop: getTranslatedLevel(level + 1, lang), - selectedLevelCDesktop: getTranslatedLevel(level + 2, lang), + selectedLevelAMobile: getLevel(level, levels), + selectedLevelBMobile: getLevel(getSteppedUpLevel(level, 1), levels), + selectedLevelADesktop: getLevel(level, levels), + selectedLevelBDesktop: getLevel(getSteppedUpLevel(level, 1), levels), + selectedLevelCDesktop: getLevel(getSteppedUpLevel(level, 2), levels), } } } function reducer(state: any, action: OverviewTableReducerAction) { switch (action.type) { - case overviewTableActionsEnum.SET_SELECTED_LEVEL_A_MOBILE: + case OverviewTableActionsEnum.SET_SELECTED_LEVEL_A_MOBILE: return { ...state, selectedLevelAMobile: action.payload, } - case overviewTableActionsEnum.SET_SELECTED_LEVEL_B_MOBILE: + case OverviewTableActionsEnum.SET_SELECTED_LEVEL_B_MOBILE: return { ...state, selectedLevelBMobile: action.payload, } - case overviewTableActionsEnum.SET_SELECTED_LEVEL_A_DESKTOP: + case OverviewTableActionsEnum.SET_SELECTED_LEVEL_A_DESKTOP: return { ...state, selectedLevelADesktop: action.payload, } - case overviewTableActionsEnum.SET_SELECTED_LEVEL_B_DESKTOP: + case OverviewTableActionsEnum.SET_SELECTED_LEVEL_B_DESKTOP: return { ...state, selectedLevelBDesktop: action.payload, } - case overviewTableActionsEnum.SET_SELECTED_LEVEL_C_DESKTOP: + case OverviewTableActionsEnum.SET_SELECTED_LEVEL_C_DESKTOP: return { ...state, selectedLevelCDesktop: action.payload, @@ -126,51 +118,45 @@ function reducer(state: any, action: OverviewTableReducerAction) { } } -export default function OverviewTableClient({ +export default function OverviewTable({ activeMembership, + levels, }: OverviewTableClientProps) { const intl = useIntl() - const lang = useLang() - const levelsData = levelsTranslations[lang] + const [selectionState, dispatch] = useReducer( reducer, - { activeMembership, lang }, + { activeMembership, levels }, getInitialState ) - function handleSelectChange(actionType: overviewTableActionsEnum) { + function handleSelectChange(actionType: OverviewTableActionsEnum) { return (key: Key) => { - if (typeof key === "number") { - dispatch({ - payload: getTranslatedLevel(key, lang), - type: actionType, - }) - } + dispatch({ + payload: getLevel(key as MembershipLevel, levels), + type: actionType, + }) } } - const levelOptions = levelsData.levels.map((level) => ({ - label: [level.level, level.name].join(" "), - value: level.level, + const levelOptions = levels.map((level) => ({ + label: getLevelNamesForSelect(level.level_id, level.name), + value: level.level_id, })) - let activeMembershipLevel: membershipLevels | null = null - if (activeMembership?.membershipLevel) { - // @ts-expect-error: membershiplevel needs proper fix - activeMembershipLevel = membershipLevels[activeMembership?.membershipLevel] - } + const activeMembershipLevel = activeMembership ?? null function MobileColumnHeader({ column }: MobileColumnHeaderProps) { let selectedLevelMobile: ComparisonLevel - let actionEnumMobile: overviewTableActionsEnum + let actionEnumMobile: OverviewTableActionsEnum switch (column) { case "A": selectedLevelMobile = selectionState.selectedLevelAMobile - actionEnumMobile = overviewTableActionsEnum.SET_SELECTED_LEVEL_A_MOBILE + actionEnumMobile = OverviewTableActionsEnum.SET_SELECTED_LEVEL_A_MOBILE break case "B": selectedLevelMobile = selectionState.selectedLevelBMobile - actionEnumMobile = overviewTableActionsEnum.SET_SELECTED_LEVEL_B_MOBILE + actionEnumMobile = OverviewTableActionsEnum.SET_SELECTED_LEVEL_B_MOBILE break default: return null @@ -178,30 +164,30 @@ export default function OverviewTableClient({ return (
- {activeMembershipLevel === selectedLevelMobile.level ? ( + {activeMembershipLevel === selectedLevelMobile.level_id ? ( ) : null} - {selectedLevelMobile.name}
level.level === selectedLevelMobile.level - ) as ComparisonLevel + levels.find( + (level) => level.level_id === selectedLevelMobile.level_id + )! } showDescription={false} /> ) } - return (
@@ -249,7 +234,7 @@ export default function OverviewTableClient({
-
- {/* Remove `as` once we have real data */} - +
) diff --git a/components/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/index.tsx b/components/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/index.tsx index 154484a79..6cc06874f 100644 --- a/components/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/index.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/index.tsx @@ -1,4 +1,4 @@ -import Image from "@/components/Image" +import MembershipLevelIcon from "@/components/Levels/Icon" import LevelSummary from "../../LevelSummary" import YourLevel from "../../YourLevelScript" @@ -21,13 +21,13 @@ export default function DesktopHeader({ {levels.map((level, idx) => { return ( - - {activeLevel === level.level ? : null} - {level.name} + {activeLevel === level.level_id ? : null} + ) @@ -38,7 +38,7 @@ export default function DesktopHeader({ {levels.map((level, idx) => { return ( diff --git a/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx b/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx index 1117aa275..1b1780787 100644 --- a/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx @@ -1,16 +1,20 @@ import { ChevronDown } from "react-feather" import Title from "@/components/TempDesignSystem/Text/Title" -import { findBenefit, getUnlockedBenefits } from "@/utils/loyaltyTable" +import { + findAvailableRewards, + getGroupedLabelAndDescription, + getGroupedRewards, +} from "@/utils/loyaltyTable" -import BenefitValue from "../BenefitValue" +import RewardValue from "../RewardValue" import DesktopHeader from "./DesktopHeader" import styles from "./largeTable.module.css" import type { - BenefitTableHeaderProps, LargeTableProps, + RewardTableHeaderProps, } from "@/types/components/overviewTable" export default function LargeTable({ @@ -18,6 +22,8 @@ export default function LargeTable({ activeLevel, Select, }: LargeTableProps) { + const groupedRewards = getGroupedRewards(levels) + return ( - {getUnlockedBenefits(levels).map((benefit) => { + {Object.entries(groupedRewards).map(([key, groupedRewards], idx) => { + const { label, description } = + getGroupedLabelAndDescription(groupedRewards) + return ( - - + {levels.map((level, idx) => { + const rewardIdsInGroup = groupedRewards.map((b) => b.reward_id) + const reward = findAvailableRewards(rewardIdsInGroup, level) return ( - ) })} @@ -50,17 +58,12 @@ export default function LargeTable({ ) } -function BenefitTableHeader({ name, description }: BenefitTableHeaderProps) { +function RewardTableHeader({ name, description }: RewardTableHeaderProps) { return (
-
- + <hgroup className={styles.rewardHeader}> + <Title as="h5" level="h2" textTransform={"regular"}> {name} @@ -69,7 +72,7 @@ function BenefitTableHeader({ name, description }: BenefitTableHeaderProps) {

diff --git a/components/Blocks/DynamicContent/OverviewTable/LargeTable/largeTable.module.css b/components/Blocks/DynamicContent/OverviewTable/LargeTable/largeTable.module.css index 4b27568bd..462f1475e 100644 --- a/components/Blocks/DynamicContent/OverviewTable/LargeTable/largeTable.module.css +++ b/components/Blocks/DynamicContent/OverviewTable/LargeTable/largeTable.module.css @@ -19,7 +19,7 @@ text-align: center; } -.benefitTh { +.rewardTh { padding: var(--Spacing-x3) var(--Spacing-x2); font-size: var(--typography-Caption-Regular-fontSize); font-weight: var(--typography-Caption-Regular-fontWeight); @@ -29,14 +29,14 @@ transform: rotate(180deg); } -.benefitHeader { +.rewardHeader { display: grid; gap: var(--Spacing-x1); grid-template-columns: 1fr auto; text-align: start; } -.benefitDescription { +.rewardDescription { margin: 0; padding-top: var(--Spacing-x1); text-align: start; diff --git a/components/Blocks/DynamicContent/OverviewTable/LevelSummary/index.tsx b/components/Blocks/DynamicContent/OverviewTable/LevelSummary/index.tsx index 519c62ef1..88e349b90 100644 --- a/components/Blocks/DynamicContent/OverviewTable/LevelSummary/index.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/LevelSummary/index.tsx @@ -1,3 +1,7 @@ +import { useIntl } from "react-intl" + +import useLang from "@/hooks/useLang" + import styles from "./levelSummary.module.css" import type { LevelSummaryProps } from "@/types/components/overviewTable" @@ -6,9 +10,17 @@ export default function LevelSummary({ level, showDescription = true, }: LevelSummaryProps) { + const lang = useLang() + const intl = useIntl() return (
- {level.requirement} + + {level.required_points.toLocaleString(lang)} + {"p "} + {level.required_nights + ? `${intl.formatMessage({ id: "or" })} ${level.required_nights} ${intl.formatMessage({ id: "nights" })}` + : ""} + {showDescription && (

{level.description}

)} diff --git a/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/index.tsx b/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/index.tsx new file mode 100644 index 000000000..b54d9722d --- /dev/null +++ b/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/index.tsx @@ -0,0 +1,48 @@ +import { ChevronDown } from "react-feather" + +import Title from "@/components/TempDesignSystem/Text/Title" + +import RewardValue from "../../RewardValue" + +import styles from "./rewardCard.module.css" + +import type { RewardCardProps } from "@/types/components/overviewTable" + +export default function RewardCard({ + comparedValues, + title, + description, +}: RewardCardProps) { + return ( +
+
+
+ +
+ + {title} + + + + +
+
+

+

+
+
+ {comparedValues.map((reward, idx) => ( +
+ +
+ ))} +
+
+ ) +} diff --git a/components/Blocks/DynamicContent/OverviewTable/BenefitCard/benefitCard.module.css b/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/rewardCard.module.css similarity index 87% rename from components/Blocks/DynamicContent/OverviewTable/BenefitCard/benefitCard.module.css rename to components/Blocks/DynamicContent/OverviewTable/RewardList/Card/rewardCard.module.css index ae643de29..02984b0f1 100644 --- a/components/Blocks/DynamicContent/OverviewTable/BenefitCard/benefitCard.module.css +++ b/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/rewardCard.module.css @@ -1,25 +1,25 @@ -.benefitCard { +.rewardCard { padding-bottom: var(--Spacing-x-one-and-half); z-index: 2; grid-column: 1/3; } -.benefitCardHeader { +.rewardCardHeader { display: grid; grid-template-columns: 1fr auto; } -.benefitCardDescription { +.rewardCardDescription { font-size: var(--typography-Caption-Regular-fontSize); line-height: 150%; padding-right: var(--Spacing-x4); } -.benefitInfo { +.rewardInfo { padding-bottom: var(--Spacing-x-one-and-half); } -.benefitComparison { +.rewardComparison { display: grid; grid-template-columns: 1fr 1fr; } @@ -50,7 +50,7 @@ } @media screen and (min-width: 950px) { - .benefitComparison { + .rewardComparison { grid-template-columns: 1fr 1fr 1fr; } } diff --git a/components/Blocks/DynamicContent/OverviewTable/RewardList/index.tsx b/components/Blocks/DynamicContent/OverviewTable/RewardList/index.tsx new file mode 100644 index 000000000..b0a72a3bd --- /dev/null +++ b/components/Blocks/DynamicContent/OverviewTable/RewardList/index.tsx @@ -0,0 +1,38 @@ +import { + findAvailableRewards, + getGroupedLabelAndDescription, + getGroupedRewards, +} from "@/utils/loyaltyTable" + +import RewardCard from "./Card" + +import styles from "./rewardList.module.css" + +import type { RewardListProps } from "@/types/components/overviewTable" + +export default function RewardList({ levels }: RewardListProps) { + const groupedRewards = getGroupedRewards(levels) + + return Object.values(groupedRewards).map((groupedRewards) => { + const rewardIdsInGroup = groupedRewards.map((b) => b.reward_id) + + const { label, description } = getGroupedLabelAndDescription(groupedRewards) + + const levelRewards = levels.map((level) => { + return findAvailableRewards(rewardIdsInGroup, level) + }) + + return ( +
+ +
+ ) + }) +} diff --git a/components/Blocks/DynamicContent/OverviewTable/BenefitList/benefitList.module.css b/components/Blocks/DynamicContent/OverviewTable/RewardList/rewardList.module.css similarity index 79% rename from components/Blocks/DynamicContent/OverviewTable/BenefitList/benefitList.module.css rename to components/Blocks/DynamicContent/OverviewTable/RewardList/rewardList.module.css index dba0dd085..3407e1599 100644 --- a/components/Blocks/DynamicContent/OverviewTable/BenefitList/benefitList.module.css +++ b/components/Blocks/DynamicContent/OverviewTable/RewardList/rewardList.module.css @@ -1,4 +1,4 @@ -.benefitCardWrapper { +.rewardCardWrapper { border-bottom: 1px solid var(--Base-Border-Subtle); position: relative; display: grid; @@ -8,12 +8,12 @@ margin: var(--Spacing-x1) var(--Spacing-x2); } -.benefitCardWrapper:last-child { +.rewardCardWrapper:last-child { border: none; } @media screen and (min-width: 950px) { - .benefitCardWrapper { + .rewardCardWrapper { grid-column: 1/4; } } diff --git a/components/Blocks/DynamicContent/OverviewTable/RewardValue/index.tsx b/components/Blocks/DynamicContent/OverviewTable/RewardValue/index.tsx new file mode 100644 index 000000000..e7efee15e --- /dev/null +++ b/components/Blocks/DynamicContent/OverviewTable/RewardValue/index.tsx @@ -0,0 +1,21 @@ +import { Minus } from "react-feather" + +import CheckCircle from "@/components/Icons/CheckCircle" + +import styles from "./rewardValue.module.css" + +import type { RewardValueProps } from "@/types/components/overviewTable" + +export default function RewardValue({ reward }: RewardValueProps) { + if (!reward) { + return + } + if (!reward.value) { + return + } + return ( +
+ {reward.value} +
+ ) +} diff --git a/components/Blocks/DynamicContent/OverviewTable/BenefitValue/benefitValue.module.css b/components/Blocks/DynamicContent/OverviewTable/RewardValue/rewardValue.module.css similarity index 85% rename from components/Blocks/DynamicContent/OverviewTable/BenefitValue/benefitValue.module.css rename to components/Blocks/DynamicContent/OverviewTable/RewardValue/rewardValue.module.css index c72e3dd7f..de9108cf1 100644 --- a/components/Blocks/DynamicContent/OverviewTable/BenefitValue/benefitValue.module.css +++ b/components/Blocks/DynamicContent/OverviewTable/RewardValue/rewardValue.module.css @@ -1,4 +1,4 @@ -.benefitValueContainer { +.rewardValueContainer { display: flex; flex-direction: column; align-items: center; @@ -7,12 +7,12 @@ text-wrap: balance; } -.benefitValue { +.rewardValue { font-size: var(--typography-Body-Bold-fontSize); font-weight: var(--typography-Body-Bold-fontWeight); } -.benefitValueDetails { +.rewardValueDetails { font-size: var(--typography-Footnote-Regular-fontSize); text-align: center; color: var(--UI-Grey-80); diff --git a/components/Blocks/DynamicContent/OverviewTable/data/DA.json b/components/Blocks/DynamicContent/OverviewTable/data/DA.json deleted file mode 100644 index 551d46b58..000000000 --- a/components/Blocks/DynamicContent/OverviewTable/data/DA.json +++ /dev/null @@ -1,538 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requirement": "0p", - "description": "Det her er starten på noget smukt. Som New Friend kan du godt glæde dig til at opdage alt det skønne ved Scandic.", - "icon": "/_static/icons/loyaltylevels/new-friend.svg", - "benefits": [ - { - "name": "Venlige priser", - "description": "Som vores ven får du altid de bedste tilbud, uanset tid og sted. Her er der ingen hemmelige aftaler eller bookingkoder – du skal bare booke som normalt.", - "unlocked": true - }, - { - "name": "Rabat på mad", - "description": "Mums! I weekenderne får du en lækker rabat på 10% i vores restaurant og hotelshop. Tilbuddet gælder, uanset om du overnatter eller bare kigger forbi for at få en bid mad. Så kom i gang med at forkæle dig selv og bestille ekstra roomservice.", - "unlocked": true, - "value": "10%" - }, - { - "name": "Gratis mocktail til børn under opholdet", - "description": "Vi vil så gerne have, at børnene føler sig som de VIPs, de er, så vi giver en forfriskende mocktail under hvert ophold.", - "unlocked": true - }, - { - "name": "Sen check ud, når tilgængeligt", - "description": "Vi ved godt, at muligheden for at checke sent ud virkelig kan redde turen, og nu skal du ikke længere skynde dig ud af sengen. Check ud en time senere uden ekstra omkostninger, og nyd at kunne sove lidt længere.", - "unlocked": false - }, - { - "name": "Restaurantvoucher", - "description": "", - "unlocked": false - }, - { - "name": "Venskabsboost", - "description": "", - "unlocked": false - }, - { - "name": "Tidlig check ind, når tilgængeligt", - "description": "Vil du tage hul på opholdet lidt hurtigere? Intet problem. Check ind en time før uden ekstra omkostninger, og nyd at kunne slappe en smule mere af.", - "unlocked": false - }, - { - "name": "Gratis opgraderinger, når tilgængelige", - "description": "På det her venskabsniveau opgraderer vi dit værelse, hvis det er muligt, så du får et endnu mere behageligt ophold.", - "unlocked": false - }, - { - "name": "2-for-1 morgenmad", - "description": "Uanset om du overnatter hos os eller ej, kan du invitere en ven på morgenmad, for I kan spise for to personer og kun betale for én! Men husk, at tjekke detaljerne først, så alt er klar til et hyggeligt morgenmøde.", - "unlocked": false - }, - { - "name": "48-timers værelsesgaranti", - "description": "Kun få af jer får denne særlige fordel! Selv hvis vi er fuldt bookede, er du garanteret et værelse, så længe du booker mindst 48 timer i forvejen.", - "unlocked": false - }, - { - "name": "Gratis morgenmad – altid", - "description": "Klar til noget lækkert om morgenen? Nu kan du starte dagen med gratis morgenmad – og ved du hvad? Det gælder uanset om du overnatter hos os eller ej.", - "unlocked": false - }, - { - "name": "En fantastisk gave hvert år", - "description": "Som vores Best Friend fortjener du virkelig en særlig behandling, og derfor har vi en eksklusiv og ret fantastisk gave klar til dig en gang om året. Hvad det er? Det er jo en overraskelse!", - "unlocked": false - }, - { - "name": "Børneboost", - "description": "Dit barn er også vores ven, og det betyder en særlig boostgave, når I overnatter hos os. Hvorfor? Fordi børn er de bedste!", - "unlocked": false - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requirement": "5 000p", - "description": "Du har været her meget på det sidste! Lad os tage venskabet videre ét behageligt ophold ad gangen.", - "icon": "/_static/icons/loyaltylevels/good-friend.svg", - "benefits": [ - { - "name": "Venlige priser", - "description": "Som vores ven får du altid de bedste tilbud, uanset tid og sted. Her er der ingen hemmelige aftaler eller bookingkoder – du skal bare booke som normalt.", - "unlocked": true - }, - { - "name": "Rabat på mad", - "description": "Hvad er mere lækkert end en rabat? Som vores ven får du i weekenderne og på udvalgte helligdage 10-15% rabat i vores restaurant og på maden i vores hotel shop – og det er uanset om du overnatter hos os eller ej. Så kom i gang med at forkæle dig selv: Bestil noget roomservice!", - "unlocked": true, - "value": "15%" - }, - { - "name": "Gratis mocktail til børn under opholdet", - "description": "Vi vil så gerne have, at børnenes føler sig som de VIPs, de er, så vi giver en forfriskende mocktail under hvert ophold.", - "unlocked": true - }, - { - "name": "Sen check ud, når tilgængeligt", - "description": "Vi ved godt, at muligheden for at checke sent ud virkelig kan redde turen, og nu skal du ikke længere skynde dig ud af sengen. Check ud en time senere uden ekstra omkostninger, og nyd at kunne sove lidt længere.", - "unlocked": false - }, - { - "name": "Restaurantvoucher", - "description": "", - "unlocked": false - }, - { - "name": "Venskabsboost", - "description": "", - "unlocked": false - }, - { - "name": "Tidlig check ind, når tilgængeligt", - "description": "Vil du tage hul på opholdet lidt hurtigere? Intet problem. Check ind en time før uden ekstra omkostninger, og nyd at kunne slappe en smule mere af.", - "unlocked": false - }, - { - "name": "Gratis opgraderinger, når tilgængelige", - "description": "På det her venskabsniveau opgraderer vi dit værelse, hvis det er muligt, så du får et endnu mere behageligt ophold.", - "unlocked": false - }, - { - "name": "2-for-1 morgenmad", - "description": "Uanset om du overnatter hos os eller ej, kan du invitere en ven på morgenmad, for I kan spise for to personer og kun betale for én! Men husk, at tjekke detaljerne først, så alt er klar til et hyggeligt morgenmøde.", - "unlocked": false - }, - { - "name": "48-timers værelsesgaranti", - "description": "Kun få af jer får denne særlige fordel! Selv hvis vi er fuldt bookede, er du garanteret et værelse, så længe du booker mindst 48 timer i forvejen.", - "unlocked": false - }, - { - "name": "Gratis morgenmad – altid", - "description": "Klar til noget lækkert om morgenen? Nu kan du starte dagen med gratis morgenmad – og ved du hvad? Det gælder uanset om du overnatter hos os eller ej.", - "unlocked": false - }, - { - "name": "En fantastisk gave hvert år", - "description": "Som vores Best Friend fortjener du virkelig en særlig behandling, og derfor har vi en eksklusiv og ret fantastisk gave klar til dig en gang om året. Hvad det er? Det er jo en overraskelse!", - "unlocked": false - }, - { - "name": "Børneboost", - "description": "Dit barn er også vores ven, og det betyder en særlig boostgave, når I overnatter hos os. Hvorfor? Fordi børn er de bedste!", - "unlocked": false - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requirement": "10 000p", - "description": "Vi har lært hinanden bedre at kende, og det gør din oplevelse hos Scandic meget mere personlig.", - "icon": "/_static/icons/loyaltylevels/close-friend.svg", - "benefits": [ - { - "name": "Venlige priser", - "description": "Som vores ven får du altid de bedste tilbud, uanset tid og sted. Her er der ingen hemmelige aftaler eller bookingkoder – du skal bare booke som normalt.", - "unlocked": true - }, - { - "name": "Rabat på mad", - "description": "Hvad er mere lækkert end en rabat? Som vores ven får du i weekenderne og på udvalgte helligdage 10-15% rabat i vores restaurant og på maden i vores hotel shop – og det er uanset om du overnatter hos os eller ej. Så kom i gang med at forkæle dig selv: Bestil noget roomservice!", - "unlocked": true, - "value": "15%" - }, - { - "name": "Gratis mocktail til børn under opholdet", - "description": "Vi vil så gerne have, at børnenes føler sig som de VIPs, de er, så vi giver en forfriskende mocktail under hvert ophold.", - "unlocked": true - }, - { - "name": "Sen check ud, når tilgængeligt", - "description": "Vi ved godt, at muligheden for at checke sent ud virkelig kan redde turen, og nu skal du ikke længere skynde dig ud af sengen. Check ud en time senere uden ekstra omkostninger, og nyd at kunne sove lidt længere.", - "unlocked": true - }, - { - "name": "Restaurantvoucher", - "description": "For hver pointgivende overnatning hos os giver vi dig en restaurantvoucher på DKK 50,-.", - "unlocked": true, - "value": "DKK 50,-" - }, - { - "name": "Venskabsboost", - "description": "", - "unlocked": false - }, - { - "name": "Tidlig check ind, når tilgængeligt", - "description": "Vil du tage hul på opholdet lidt hurtigere? Intet problem. Check ind en time før uden ekstra omkostninger, og nyd at kunne slappe en smule mere af.", - "unlocked": false - }, - { - "name": "Gratis opgraderinger, når tilgængelige", - "description": "På det her venskabsniveau opgraderer vi dit værelse, hvis det er muligt, så du får et endnu mere behageligt ophold.", - "unlocked": false - }, - { - "name": "2-for-1 morgenmad", - "description": "Uanset om du overnatter hos os eller ej, kan du invitere en ven på morgenmad, for I kan spise for to personer og kun betale for én! Men husk, at tjekke detaljerne først, så alt er klar til et hyggeligt morgenmøde.", - "unlocked": false - }, - { - "name": "48-timers værelsesgaranti", - "description": "Kun få af jer får denne særlige fordel! Selv hvis vi er fuldt bookede, er du garanteret et værelse, så længe du booker mindst 48 timer i forvejen.", - "unlocked": false - }, - { - "name": "Gratis morgenmad – altid", - "description": "Klar til noget lækkert om morgenen? Nu kan du starte dagen med gratis morgenmad – og ved du hvad? Det gælder uanset om du overnatter hos os eller ej.", - "unlocked": false - }, - { - "name": "En fantastisk gave hvert år", - "description": "Som vores Best Friend fortjener du virkelig en særlig behandling, og derfor har vi en eksklusiv og ret fantastisk gave klar til dig en gang om året. Hvad det er? Det er jo en overraskelse!", - "unlocked": false - }, - { - "name": "Børneboost", - "description": "Dit barn er også vores ven, og det betyder en særlig boostgave, når I overnatter hos os. Hvorfor? Fordi børn er de bedste!", - "unlocked": false - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requirement": "25 000p", - "description": "Her er vist grobund for et livslangt venskab, og det betyder, at du får adgang til meget mere med Scandic.", - "icon": "/_static/icons/loyaltylevels/dear-friend.svg", - "benefits": [ - { - "name": "Venlige priser", - "description": "Som vores ven får du altid de bedste tilbud, uanset tid og sted. Her er der ingen hemmelige aftaler eller bookingkoder – du skal bare booke som normalt.", - "unlocked": true - }, - { - "name": "Rabat på mad", - "description": "Hvad er mere lækkert end en rabat? Som vores ven får du i weekenderne og på udvalgte helligdage 10-15% rabat i vores restaurant og på maden i vores hotelshop – og det er uanset om du overnatter hos os eller ej. Så kom i gang med at forkæle dig selv: Bestil noget roomservice!", - "unlocked": true, - "value": "15%" - }, - { - "name": "Gratis mocktail til børn under opholdet", - "description": "Vi vil så gerne have, at børnenes føler sig som de VIPs, de er, så vi giver en forfriskende mocktail under hvert ophold.", - "unlocked": true - }, - { - "name": "Sen check ud, når tilgængeligt", - "description": "Vi ved godt, at muligheden for at checke sent ud virkelig kan redde turen, og nu skal du ikke længere skynde dig ud af sengen. Check ud en time senere uden ekstra omkostninger, og nyd at kunne sove lidt længere.", - "unlocked": true - }, - { - "name": "Restaurantvoucher", - "description": "Vores venskab blev lige en smule bedre! Få en restaurantvoucher på DKK 75,- for hver pointgivende overnatning.", - "unlocked": true, - "value": "DKK 75,-" - }, - { - "name": "Venskabsboost", - "description": "Hver gang du booster dine venskabspoint, får du 25% ekstra! Så kom i gang med at optjene point på ophold, måltider m.m., og pludselig har du en gratis overnatning.", - "unlocked": true, - "value": "25%" - }, - { - "name": "Tidlig check ind, når tilgængeligt", - "description": "Vil du tage hul på opholdet lidt hurtigere? Intet problem. Check ind en time før uden ekstra omkostninger, og nyd at kunne slappe en smule mere af.", - "unlocked": true - }, - { - "name": "Gratis opgraderinger, når tilgængelige", - "description": "På det her venskabsniveau opgraderer vi dit værelse, hvis det er muligt, så du får et endnu mere behageligt ophold.", - "unlocked": false - }, - { - "name": "2-for-1 morgenmad", - "description": "Uanset om du overnatter hos os eller ej, kan du invitere en ven på morgenmad, for I kan spise for to personer og kun betale for én! Men husk, at tjekke detaljerne først, så alt er klar til et hyggeligt morgenmøde.", - "unlocked": false - }, - { - "name": "48-timers værelsesgaranti", - "description": "Kun få af jer får denne særlige fordel! Selv hvis vi er fuldt bookede, er du garanteret et værelse, så længe du booker mindst 48 timer i forvejen.", - "unlocked": false - }, - { - "name": "Gratis morgenmad – altid", - "description": "Klar til noget lækkert om morgenen? Nu kan du starte dagen med gratis morgenmad – og ved du hvad? Det gælder uanset om du overnatter hos os eller ej.", - "unlocked": false - }, - { - "name": "En fantastisk gave hvert år", - "description": "Som vores Best Friend fortjener du virkelig en særlig behandling, og derfor har vi en eksklusiv og ret fantastisk gave klar til dig en gang om året. Hvad det er? Det er jo en overraskelse!", - "unlocked": false - }, - { - "name": "Børneboost", - "description": "Dit barn er også vores ven, og det betyder en særlig boostgave, når I overnatter hos os. Hvorfor? Fordi børn er de bedste!", - "unlocked": false - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requirement": "100 000p", - "description": "Du har været hos os på mange ophold, så derfor vil vi gerne give dig nogle af vores bedste fordele.", - "icon": "/_static/icons/loyaltylevels/loyal-friend.svg", - "benefits": [ - { - "name": "Venlige priser", - "description": "Som vores ven får du altid de bedste tilbud, uanset tid og sted. Her er der ingen hemmelige aftaler eller bookingkoder – du skal bare booke som normalt.", - "unlocked": true - }, - { - "name": "Rabat på mad", - "description": "Hvad er mere lækkert end en rabat? Som vores ven får du i weekenderne og på udvalgte helligdage 10-15% rabat i vores restaurant og på maden i vores hotel shop – og det er uanset om du overnatter hos os eller ej. Så kom i gang med at forkæle dig selv: Bestil noget roomservice!", - "unlocked": true, - "value": "15%" - }, - { - "name": "Gratis mocktail til børn under opholdet", - "description": "Vi vil så gerne have, at børnenes føler sig som de VIPs, de er, så vi giver en forfriskende mocktail under hvert ophold.", - "unlocked": true - }, - { - "name": "Sen check ud, når tilgængeligt", - "description": "Vi ved godt, at muligheden for at checke sent ud virkelig kan redde turen, og nu skal du ikke længere skynde dig ud af sengen. Check ud en time senere uden ekstra omkostninger, og nyd at kunne sove lidt længere.", - "unlocked": true - }, - { - "name": "Restaurantvoucher", - "description": "Vi giver dig en restaurantvoucher på DKK 100,- for hver pointgivende overnatning. Det kan blive til mange croissanter!", - "unlocked": true, - "value": "DKK 100,-" - }, - { - "name": "Venskabsboost", - "description": "Hver gang du booster dine venskabspoint, får du 25% ekstra! Så kom i gang med at optjene point på ophold, måltider m.m., og pludselig har du en gratis overnatning.", - "unlocked": true, - "value": "25%" - }, - { - "name": "Tidlig check ind, når tilgængeligt", - "description": "Vil du tage hul på opholdet lidt hurtigere? Intet problem. Check ind en time før uden ekstra omkostninger, og nyd at kunne slappe en smule mere af.", - "unlocked": true - }, - { - "name": "Gratis opgraderinger, når tilgængelige", - "description": "På det her venskabsniveau opgraderer vi dit værelse, hvis det er muligt, så du får et endnu mere behageligt ophold.", - "unlocked": true - }, - { - "name": "2-for-1 morgenmad", - "description": "Uanset om du overnatter hos os eller ej, kan du invitere en ven på morgenmad, for I kan spise for to personer og kun betale for én! Men husk, at tjekke detaljerne først, så alt er klar til et hyggeligt morgenmøde.", - "unlocked": true - }, - { - "name": "48-timers værelsesgaranti", - "description": "Kun få af jer får denne særlige fordel! Selv hvis vi er fuldt bookede, er du garanteret et værelse, så længe du booker mindst 48 timer i forvejen.", - "unlocked": false - }, - { - "name": "Gratis morgenmad – altid", - "description": "Klar til noget lækkert om morgenen? Nu kan du starte dagen med gratis morgenmad – og ved du hvad? Det gælder uanset om du overnatter hos os eller ej.", - "unlocked": false - }, - { - "name": "En fantastisk gave hvert år", - "description": "Som vores Best Friend fortjener du virkelig en særlig behandling, og derfor har vi en eksklusiv og ret fantastisk gave klar til dig en gang om året. Hvad det er? Det er jo en overraskelse!", - "unlocked": false - }, - { - "name": "Børneboost", - "description": "Dit barn er også vores ven, og det betyder en særlig boostgave, når I overnatter hos os. Hvorfor? Fordi børn er de bedste!", - "unlocked": false - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requirement": "250 000p", - "description": "Vi er glade for, at du besøger os, uanset om det er høj- eller lavsæson. Derfor får du endnu flere skræddersyede fordele.", - "icon": "/_static/icons/loyaltylevels/true-friend.svg", - "benefits": [ - { - "name": "Venlige priser", - "description": "Som vores ven får du altid de bedste tilbud, uanset tid og sted. Her er der ingen hemmelige aftaler eller bookingkoder – du skal bare booke som normalt.", - "unlocked": true - }, - { - "name": "Rabat på mad", - "description": "Hvad er mere lækkert end en rabat? Som vores ven får du i weekenderne og på udvalgte helligdage 10-15% rabat i vores restaurant og på maden i vores hotel shop – og det er uanset om du overnatter hos os eller ej. Så kom i gang med at forkæle dig selv: Bestil noget roomservice!", - "unlocked": true, - "value": "15%" - }, - { - "name": "Gratis mocktail til børn under opholdet", - "description": "Vi vil så gerne have, at børnenes føler sig som de VIPs, de er, så vi giver en forfriskende mocktail under hvert ophold.", - "unlocked": true - }, - { - "name": "Sen check ud, når tilgængeligt", - "description": "Vi ved godt, at muligheden for at checke sent ud virkelig kan redde turen, og nu skal du ikke længere skynde dig ud af sengen. Check ud en time senere uden ekstra omkostninger, og nyd at kunne sove lidt længere.", - "unlocked": true - }, - { - "name": "Restaurantvoucher", - "description": "Det går ret godt, så nu giver hver pointgivende overnatning en restaurantvoucher. Den er helt sikkert god at have ved hånden, næste gang du vil spise en god middag!", - "unlocked": true, - "value": "DKK 150,-" - }, - { - "name": "Venskabsboost", - "description": "Du kan godt glæde dig. Hver gang du booster dine venskabspoint, får du 25-50% ekstra! Så kom i gang med at optjene point på ophold, måltider m.m., og pludselig har du en gratis overnatning.", - "unlocked": true, - "value": "50%" - }, - { - "name": "Tidlig check ind, når tilgængeligt", - "description": "Vil du tage hul på opholdet lidt hurtigere? Intet problem. Check ind en time før uden ekstra omkostninger, og nyd at kunne slappe en smule mere af.", - "unlocked": true - }, - { - "name": "Gratis opgraderinger, når tilgængelige", - "description": "På det her venskabsniveau opgraderer vi dit værelse, hvis det er muligt, så du får et endnu mere behageligt ophold.", - "unlocked": true - }, - { - "name": "2-for-1 morgenmad", - "description": "Uanset om du overnatter hos os eller ej, kan du invitere en ven på morgenmad, for I kan spise for to personer og kun betale for én! Men husk, at tjekke detaljerne først, så alt er klar til et hyggeligt morgenmøde.", - "unlocked": false - }, - { - "name": "48-timers værelsesgaranti", - "description": "Kun få af jer får denne særlige fordel! Selv hvis vi er fuldt bookede, er du garanteret et værelse, så længe du booker mindst 48 timer i forvejen.", - "unlocked": true - }, - { - "name": "Gratis morgenmad – altid", - "description": "Klar til noget lækkert om morgenen? Nu kan du starte dagen med gratis morgenmad – og ved du hvad? Det gælder uanset om du overnatter hos os eller ej.", - "unlocked": true - }, - { - "name": "En fantastisk gave hvert år", - "description": "Som vores Best Friend fortjener du virkelig en særlig behandling, og derfor har vi en eksklusiv og ret fantastisk gave klar til dig en gang om året. Hvad det er? Det er jo en overraskelse!", - "unlocked": false - }, - { - "name": "Børneboost", - "description": "Dit barn er også vores ven, og det betyder en særlig boostgave, når I overnatter hos os. Hvorfor? Fordi børn er de bedste!", - "unlocked": false - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requirement": "400 000p eller 100 nætter", - "description": "Det bliver simpelthen ikke bedre end det her, når det kommer til de helt eksklusive oplevelser!", - "icon": "/_static/icons/loyaltylevels/best-friend.svg", - "benefits": [ - { - "name": "Venlige priser", - "description": "Som vores ven får du altid de bedste tilbud, uanset tid og sted. Her er der ingen hemmelige aftaler eller bookingkoder – du skal bare booke som normalt.", - "unlocked": true - }, - { - "name": "Rabat på mad", - "description": "Hvad er mere lækkert end en rabat? Som vores ven får du i weekenderne og på udvalgte helligdage 10-15% rabat i vores restaurant og på maden i vores hotel shop – og det er uanset om du overnatter hos os eller ej. Så kom i gang med at forkæle dig selv: Bestil noget roomservice!", - "unlocked": true, - "value": "15%" - }, - { - "name": "Gratis mocktail til børn under opholdet", - "description": "Vi vil så gerne have, at børnenes føler sig som de VIPs, de er, så vi giver en forfriskende mocktail under hvert ophold.", - "unlocked": true - }, - { - "name": "Sen check ud, når tilgængeligt", - "description": "Vi ved godt, at muligheden for at checke sent ud virkelig kan redde turen, og nu skal du ikke længere skynde dig ud af sengen. Check ud en time senere uden ekstra omkostninger, og nyd at kunne sove lidt længere.", - "unlocked": true - }, - { - "name": "Restaurantvoucher", - "description": "Nu giver hver pointgivende overnatning en restaurantvoucher. Forestil dig lige, hvor mange croissanter du kan få!", - "unlocked": true, - "value": "DKK 200,-" - }, - { - "name": "Venskabsboost", - "description": "Du kan godt glæde dig. Hver gang du booster dine venskabspoint, får du 25-50% ekstra! Så kom i gang med at optjene point på ophold, måltider m.m., og pludselig har du en gratis overnatning.", - "unlocked": true, - "value": "50%" - }, - { - "name": "Tidlig check ind, når tilgængeligt", - "description": "Vil du tage hul på opholdet lidt hurtigere? Intet problem. Check ind en time før uden ekstra omkostninger, og nyd at kunne slappe en smule mere af.", - "unlocked": true - }, - { - "name": "Gratis opgraderinger, når tilgængelige", - "description": "På det her venskabsniveau opgraderer vi dit værelse, hvis det er muligt, så du får et endnu mere behageligt ophold.", - "unlocked": true - }, - { - "name": "2-for-1 morgenmad", - "description": "Uanset om du overnatter hos os eller ej, kan du invitere en ven på morgenmad, for I kan spise for to personer og kun betale for én! Men husk, at tjekke detaljerne først, så alt er klar til et hyggeligt morgenmøde.", - "unlocked": false - }, - { - "name": "48-timers værelsesgaranti", - "description": "Kun få af jer får denne særlige fordel! Selv hvis vi er fuldt bookede, er du garanteret et værelse, så længe du booker mindst 48 timer i forvejen.", - "unlocked": true - }, - { - "name": "Gratis morgenmad – altid", - "description": "Klar til noget lækkert om morgenen? Nu kan du starte dagen med gratis morgenmad – og ved du hvad? Det gælder uanset om du overnatter hos os eller ej.", - "unlocked": true - }, - { - "name": "En fantastisk gave hvert år", - "description": "Som vores Best Friend fortjener du virkelig en særlig behandling, og derfor har vi en eksklusiv og ret fantastisk gave klar til dig en gang om året. Hvad det er? Det er jo en overraskelse!", - "unlocked": true - }, - { - "name": "Børneboost", - "description": "Dit barn er også vores ven, og det betyder en særlig boostgave, når I overnatter hos os. Hvorfor? Fordi børn er de bedste!", - "unlocked": true - } - ] - } - ] -} diff --git a/components/Blocks/DynamicContent/OverviewTable/data/DE.json b/components/Blocks/DynamicContent/OverviewTable/data/DE.json deleted file mode 100644 index 5d11c6a36..000000000 --- a/components/Blocks/DynamicContent/OverviewTable/data/DE.json +++ /dev/null @@ -1,538 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requirement": "0 Punkte", - "description": "Dies ist der Beginn von etwas Wunderbarem: Als New Friend können Sie sich auf eine Reise voller herrlicher Scandic-Entdeckungen freuen.", - "icon": "/_static/icons/loyaltylevels/new-friend.svg", - "benefits": [ - { - "name": "Freundschaftspreise", - "description": "Als unser Freund erhalten Sie immer das beste Angebot: an jedem Tag und an jedem Ort. Und dafür ist weder ein Handschlag noch ein Buchungscode erforderlich – stürzen Sie sich einfach ins Buchungserlebnis und überzeugen Sie sich selbst.", - "unlocked": true - }, - { - "name": "Rabatt auf Speisen", - "description": "Mmmh, lecker! Genießen Sie an den Wochenenden einen verlockenden Preisnachlass von 10 % in unserem Restaurant und Hotelshop. Dieses Angebot gilt unabhängig davon, ob Sie bei uns übernachten oder nur auf einen Happen zu essen bei uns vorbeischauen. Worauf warten Sie also noch? Gönnen Sie sich etwas und bestellen Sie den zusätzlichen Zimmerservice.", - "unlocked": true, - "value": "10%" - }, - { - "name": "Kostenloser Kinder-Mocktail während des Aufenthalts", - "description": "Oh, Sie haben ja die Kinder dabei! Wie schön – hallo junger Freund! Wir möchten, dass sich Kinder bei uns wie VIPs fühlen, und deshalb bekommen sie von uns bei jedem Aufenthalt einen erfrischenden Mocktail spendiert.", - "unlocked": true - }, - { - "name": "Später Check-out, wenn verfügbar", - "description": "Wir wissen, dass ein später Check-out manchmal eine große Erleichterung sein kann. Und an diesem Punkt unserer Freundschaft gibt es keinen Grund mehr, sich aus dem Bett zu quälen: Wir kommen Ihnen gerne entgegen. Checken Sie kostenlos 1 Stunde später aus und schlafen Sie einfach etwas länger.", - "unlocked": false - }, - { - "name": "Restaurantgutschein", - "description": "", - "unlocked": false - }, - { - "name": "Freundschaftsbonus", - "description": "", - "unlocked": false - }, - { - "name": "Früher Check-in, wenn verfügbar", - "description": "Sie treffen schon früher bei uns ein? Kein Problem! Checken Sie einfach kostenlos eine Stunde früher ein – so können Sie sich gleich ein wenig ausruhen und haben noch mehr von Ihrem Aufenthalt.", - "unlocked": false - }, - { - "name": "Kostenlose Upgrades wenn verfügbar", - "description": "Um auf den Punkt zu kommen – wir setzen noch einen drauf. Auf dieser Stufe unserer Freundschaft bieten wir Ihnen, wann immer es möglich ist, ein Zimmer-Upgrade an – so wird Ihr Aufenthalt noch komfortabler. Wenn das nicht großartig klingt …", - "unlocked": false - }, - { - "name": "Frühstück für zwei zum Preis von einem", - "description": "Wenn das kein delikater Deal ist: Egal, ob Sie bei uns übernachten oder nicht, schnappen Sie sich einen Frühstückspartner, denn jetzt können Sie zu zweit zum Preis von einem bei uns essen! Informieren Sie sich einfach vorher über alle Details und genießen Sie ein gemütliches Frühstück zu zweit.", - "unlocked": false - }, - { - "name": "48-Stunden-Zimmergarantie", - "description": "Pssst! Diesen ganz besonderen Leckerbissen bekommen bei uns nur die Wenigsten! Also aufgepasst: Selbst wenn wir völlig ausgebucht sind, erhalten Sie bei uns garantiert ein Zimmer, wenn Sie 48 Stunden im Voraus buchen. Ist das nicht einfach unglaublich?", - "unlocked": false - }, - { - "name": "Ein kostenloses Frühstück – jederzeit", - "description": "Lust auf einen leckeren Start in den Tag? Dann schauen Sie doch einfach bei uns vorbei! Denn jetzt können Sie Ihren Tag mit einem kostenlosen Frühstück beginnen – und zwar ganz unabhängig davon, ob Sie bei uns übernachten oder nicht.", - "unlocked": false - }, - { - "name": "Ein exklusives Geschenk pro Jahr", - "description": "Als Best Friend haben Sie natürlich eine königliche Behandlung verdient – deshalb haben wir einmal im Jahr ein exklusives und ziemlich großartiges Geschenk für Sie. Neugierig? Tja, das bleibt leider eine Überraschung. Es wird noch nichts verraten!", - "unlocked": false - }, - { - "name": "Ein Geschenk für Kinder", - "description": "An diesem Punkt unserer Freundschaft betrachten wir natürlich auch Ihr Kind als Freund – was bedeutet, dass es bei jeder Übernachtung ein ganz spezielles Geschenk von uns erhält. Und warum? Weil Kinder einfach cool sind! Sie haben eine VIP-Behandlung verdient.", - "unlocked": false - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requirement": "5 000 Punkte", - "description": "Sie waren in letzter Zeit viel bei uns! Und ehrlich gesagt haben wir das Gefühl, dass wir auf einer Wellenlänge sind – die vielen angenehmen Aufenthalte und lustigen Überraschungen sprechen für sich.", - "icon": "/_static/icons/loyaltylevels/good-friend.svg", - "benefits": [ - { - "name": "Freundschaftspreise", - "description": "Als unser Freund erhalten Sie immer das beste Angebot: an jedem Tag und an jedem Ort. Und dafür ist weder ein Handschlag noch ein Buchungscode erforderlich – stürzen Sie sich einfach ins Buchungserlebnis und überzeugen Sie sich selbst.", - "unlocked": true - }, - { - "name": "Rabatt auf Speisen", - "description": "Was gibt es Schöneres als einen Preisnachlass? Als Freund erhalten Sie an Wochenenden und ausgewählten Feiertagen 10-15% Rabatt auf die Speisen in unseren Restaurants und Hotelshops – und zwar unabhängig davon, ob Sie bei uns übernachten oder nicht. Also kommen Sie vorbei und tun Sie sich etwas Gutes! Zum Beispiel mit unserem zusätzlichen Zimmerservice.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Kostenloser Kinder-Mocktail während des Aufenthalts", - "description": "Oh, Sie haben ja die Kinder dabei! Wie schön – hallo junger Freund! Wir möchten, dass sich Kinder bei uns wie VIPs fühlen, und deshalb bekommen sie von uns bei jedem Aufenthalt einen erfrischenden Mocktail spendiert.", - "unlocked": true - }, - { - "name": "Später Check-out, wenn verfügbar", - "description": "Wir wissen, dass ein später Check-out manchmal eine große Erleichterung sein kann. Und an diesem Punkt unserer Freundschaft gibt es keinen Grund mehr, sich aus dem Bett zu quälen: Wir kommen Ihnen gerne entgegen. Checken Sie kostenlos 1 Stunde später aus und schlafen Sie einfach etwas länger.", - "unlocked": false - }, - { - "name": "Restaurantgutschein", - "description": "", - "unlocked": false - }, - { - "name": "Freundschaftsbonus", - "description": "", - "unlocked": false - }, - { - "name": "Früher Check-in, wenn verfügbar", - "description": "Sie treffen schon früher bei uns ein? Kein Problem! Checken Sie einfach kostenlos eine Stunde früher ein – so können Sie sich gleich ein wenig ausruhen und haben noch mehr von Ihrem Aufenthalt.", - "unlocked": false - }, - { - "name": "Kostenlose Upgrades wenn verfügbar", - "description": "Um auf den Punkt zu kommen – wir setzen noch einen drauf. Auf dieser Stufe unserer Freundschaft bieten wir Ihnen, wann immer es möglich ist, ein Zimmer-Upgrade an – so wird Ihr Aufenthalt noch komfortabler. Wenn das nicht großartig klingt …", - "unlocked": false - }, - { - "name": "Frühstück für zwei zum Preis von einem", - "description": "Wenn das kein delikater Deal ist: Egal, ob Sie bei uns übernachten oder nicht, schnappen Sie sich einen Frühstückspartner, denn jetzt können Sie zu zweit zum Preis von einem bei uns essen! Informieren Sie sich einfach vorher über alle Details und genießen Sie ein gemütliches Frühstück zu zweit.", - "unlocked": false - }, - { - "name": "48-Stunden-Zimmergarantie", - "description": "Pssst! Diesen ganz besonderen Leckerbissen bekommen bei uns nur die Wenigsten! Also aufgepasst: Selbst wenn wir völlig ausgebucht sind, erhalten Sie bei uns garantiert ein Zimmer, wenn Sie 48 Stunden im Voraus buchen. Ist das nicht einfach unglaublich?", - "unlocked": false - }, - { - "name": "Ein kostenloses Frühstück – jederzeit", - "description": "Lust auf einen leckeren Start in den Tag? Dann schauen Sie doch einfach bei uns vorbei! Denn jetzt können Sie Ihren Tag mit einem kostenlosen Frühstück beginnen – und zwar ganz unabhängig davon, ob Sie bei uns übernachten oder nicht.", - "unlocked": false - }, - { - "name": "Ein exklusives Geschenk pro Jahr", - "description": "Als Best Friend haben Sie natürlich eine königliche Behandlung verdient – deshalb haben wir einmal im Jahr ein exklusives und ziemlich großartiges Geschenk für Sie. Neugierig? Tja, das bleibt leider eine Überraschung. Es wird noch nichts verraten!", - "unlocked": false - }, - { - "name": "Ein Geschenk für Kinder", - "description": "An diesem Punkt unserer Freundschaft betrachten wir natürlich auch Ihr Kind als Freund – was bedeutet, dass es bei jeder Übernachtung ein ganz spezielles Geschenk von uns erhält. Und warum? Weil Kinder einfach cool sind! Sie haben eine VIP-Behandlung verdient.", - "unlocked": false - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requirement": "10 000 Punkte", - "description": "Jetzt wird es ernst: Wir lernen uns wirklich besser kennen, was bedeutet, dass Ihre Zeit mit Scandic noch viel persönlicher wird.", - "icon": "/_static/icons/loyaltylevels/close-friend.svg", - "benefits": [ - { - "name": "Freundschaftspreise", - "description": "Als unser Freund erhalten Sie immer das beste Angebot: an jedem Tag und an jedem Ort. Und dafür ist weder ein Handschlag noch ein Buchungscode erforderlich – stürzen Sie sich einfach ins Buchungserlebnis und überzeugen Sie sich selbst.", - "unlocked": true - }, - { - "name": "Rabatt auf Speisen", - "description": "Was gibt es Schöneres als einen Preisnachlass? Als Freund erhalten Sie an Wochenenden und ausgewählten Feiertagen 10-15% Rabatt auf die Speisen in unseren Restaurants und Hotelshops – und zwar unabhängig davon, ob Sie bei uns übernachten oder nicht. Also kommen Sie vorbei und tun Sie sich etwas Gutes! Zum Beispiel mit unserem zusätzlichen Zimmerservice.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Kostenloser Kinder-Mocktail während des Aufenthalts", - "description": "Oh, Sie haben ja die Kinder dabei! Wie schön – hallo junger Freund! Wir möchten, dass sich Kinder bei uns wie VIPs fühlen, und deshalb bekommen sie von uns bei jedem Aufenthalt einen erfrischenden Mocktail spendiert.", - "unlocked": true - }, - { - "name": "Später Check-out, wenn verfügbar", - "description": "Wir wissen, dass ein später Check-out manchmal eine große Erleichterung sein kann. Und an diesem Punkt unserer Freundschaft gibt es keinen Grund mehr, sich aus dem Bett zu quälen: Wir kommen Ihnen gerne entgegen. Checken Sie kostenlos 1 Stunde später aus und schlafen Sie einfach etwas länger.", - "unlocked": true - }, - { - "name": "Restaurantgutschein", - "description": "Und jetzt kommt der Clou: Für jede Übernachtung, mit der Sie bei uns Freundschaftspunkte sammeln, schenken wir Ihnen zusätzlich einen Restaurantgutschein über 5 €.", - "unlocked": true, - "value": "5 €" - }, - { - "name": "Freundschaftsbonus", - "description": "", - "unlocked": false - }, - { - "name": "Früher Check-in, wenn verfügbar", - "description": "Sie treffen schon früher bei uns ein? Kein Problem! Checken Sie einfach kostenlos eine Stunde früher ein – so können Sie sich gleich ein wenig ausruhen und haben noch mehr von Ihrem Aufenthalt.", - "unlocked": false - }, - { - "name": "Kostenlose Upgrades wenn verfügbar", - "description": "Um auf den Punkt zu kommen – wir setzen noch einen drauf. Auf dieser Stufe unserer Freundschaft bieten wir Ihnen, wann immer es möglich ist, ein Zimmer-Upgrade an – so wird Ihr Aufenthalt noch komfortabler. Wenn das nicht großartig klingt …", - "unlocked": false - }, - { - "name": "Frühstück für zwei zum Preis von einem", - "description": "Wenn das kein delikater Deal ist: Egal, ob Sie bei uns übernachten oder nicht, schnappen Sie sich einen Frühstückspartner, denn jetzt können Sie zu zweit zum Preis von einem bei uns essen! Informieren Sie sich einfach vorher über alle Details und genießen Sie ein gemütliches Frühstück zu zweit.", - "unlocked": false - }, - { - "name": "48-Stunden-Zimmergarantie", - "description": "Pssst! Diesen ganz besonderen Leckerbissen bekommen bei uns nur die Wenigsten! Also aufgepasst: Selbst wenn wir völlig ausgebucht sind, erhalten Sie bei uns garantiert ein Zimmer, wenn Sie 48 Stunden im Voraus buchen. Ist das nicht einfach unglaublich?", - "unlocked": false - }, - { - "name": "Ein kostenloses Frühstück – jederzeit", - "description": "Lust auf einen leckeren Start in den Tag? Dann schauen Sie doch einfach bei uns vorbei! Denn jetzt können Sie Ihren Tag mit einem kostenlosen Frühstück beginnen – und zwar ganz unabhängig davon, ob Sie bei uns übernachten oder nicht.", - "unlocked": false - }, - { - "name": "Ein exklusives Geschenk pro Jahr", - "description": "Als Best Friend haben Sie natürlich eine königliche Behandlung verdient – deshalb haben wir einmal im Jahr ein exklusives und ziemlich großartiges Geschenk für Sie. Neugierig? Tja, das bleibt leider eine Überraschung. Es wird noch nichts verraten!", - "unlocked": false - }, - { - "name": "Ein Geschenk für Kinder", - "description": "An diesem Punkt unserer Freundschaft betrachten wir natürlich auch Ihr Kind als Freund – was bedeutet, dass es bei jeder Übernachtung ein ganz spezielles Geschenk von uns erhält. Und warum? Weil Kinder einfach cool sind! Sie haben eine VIP-Behandlung verdient.", - "unlocked": false - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requirement": "25 000 Punkte", - "description": "Ein Hoch auf uns! Unser Verhältnis scheint sich in Richtung Freunde fürs Leben zu entwickeln – was auch bedeutet, dass Sie Zugang zu einer ganzen Menge mehr Scandic bekommen.", - "icon": "/_static/icons/loyaltylevels/dear-friend.svg", - "benefits": [ - { - "name": "Freundschaftspreise", - "description": "Als unser Freund erhalten Sie immer das beste Angebot: an jedem Tag und an jedem Ort. Und dafür ist weder ein Handschlag noch ein Buchungscode erforderlich – stürzen Sie sich einfach ins Buchungserlebnis und überzeugen Sie sich selbst.", - "unlocked": true - }, - { - "name": "Rabatt auf Speisen", - "description": "Was gibt es Schöneres als einen Preisnachlass? Als Freund erhalten Sie an Wochenenden und ausgewählten Feiertagen 10-15% Rabatt auf die Speisen in unseren Restaurants und Hotelshops – und zwar unabhängig davon, ob Sie bei uns übernachten oder nicht. Also kommen Sie vorbei und tun Sie sich etwas Gutes! Zum Beispiel mit unserem zusätzlichen Zimmerservice.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Kostenloser Kinder-Mocktail während des Aufenthalts", - "description": "Oh, Sie haben ja die Kinder dabei! Wie schön – hallo junger Freund! Wir möchten, dass sich Kinder bei uns wie VIPs fühlen, und deshalb bekommen sie von uns bei jedem Aufenthalt einen erfrischenden Mocktail spendiert.", - "unlocked": true - }, - { - "name": "Später Check-out, wenn verfügbar", - "description": "Wir wissen, dass ein später Check-out manchmal eine große Erleichterung sein kann. Und an diesem Punkt unserer Freundschaft gibt es keinen Grund mehr, sich aus dem Bett zu quälen: Wir kommen Ihnen gerne entgegen. Checken Sie kostenlos 1 Stunde später aus und schlafen Sie einfach etwas länger.", - "unlocked": true - }, - { - "name": "Restaurantgutschein", - "description": "Unsere Freundschaft wird jetzt noch schöner! Genießen Sie einen Restaurantgutschein im Wert von 7,50 € für jede Übernachtung, mit der Sie bei uns Punkte sammeln. Ist das nicht aufregend?", - "unlocked": true, - "value": "7,50 €" - }, - { - "name": "Freundschaftsbonus", - "description": "Hier haben wir etwas wirklich Großartiges für Sie: Jedes Mal, wenn Sie einen neuen Schub an Freundschaftspunkten erhalten, gibt es von uns 25 % dazu – ein ordentlicher Extra-Schub! Wenn Sie also anfangen, diese Punkte für Aufenthalte, Mahlzeiten und anderes zu sammeln, erhalten Sie im Handumdrehen eine kostenlose Übernachtung.", - "unlocked": true, - "value": "25%" - }, - { - "name": "Früher Check-in, wenn verfügbar", - "description": "Sie treffen schon früher bei uns ein? Kein Problem! Checken Sie einfach kostenlos eine Stunde früher ein – so können Sie sich gleich ein wenig ausruhen und haben noch mehr von Ihrem Aufenthalt.", - "unlocked": true - }, - { - "name": "Kostenlose Upgrades wenn verfügbar", - "description": "Um auf den Punkt zu kommen – wir setzen noch einen drauf. Auf dieser Stufe unserer Freundschaft bieten wir Ihnen, wann immer es möglich ist, ein Zimmer-Upgrade an – so wird Ihr Aufenthalt noch komfortabler. Wenn das nicht großartig klingt …", - "unlocked": false - }, - { - "name": "Frühstück für zwei zum Preis von einem", - "description": "Wenn das kein delikater Deal ist: Egal, ob Sie bei uns übernachten oder nicht, schnappen Sie sich einen Frühstückspartner, denn jetzt können Sie zu zweit zum Preis von einem bei uns essen! Informieren Sie sich einfach vorher über alle Details und genießen Sie ein gemütliches Frühstück zu zweit.", - "unlocked": false - }, - { - "name": "48-Stunden-Zimmergarantie", - "description": "Pssst! Diesen ganz besonderen Leckerbissen bekommen bei uns nur die Wenigsten! Also aufgepasst: Selbst wenn wir völlig ausgebucht sind, erhalten Sie bei uns garantiert ein Zimmer, wenn Sie 48 Stunden im Voraus buchen. Ist das nicht einfach unglaublich?", - "unlocked": false - }, - { - "name": "Ein kostenloses Frühstück – jederzeit", - "description": "Lust auf einen leckeren Start in den Tag? Dann schauen Sie doch einfach bei uns vorbei! Denn jetzt können Sie Ihren Tag mit einem kostenlosen Frühstück beginnen – und zwar ganz unabhängig davon, ob Sie bei uns übernachten oder nicht.", - "unlocked": false - }, - { - "name": "Ein exklusives Geschenk pro Jahr", - "description": "Als Best Friend haben Sie natürlich eine königliche Behandlung verdient – deshalb haben wir einmal im Jahr ein exklusives und ziemlich großartiges Geschenk für Sie. Neugierig? Tja, das bleibt leider eine Überraschung. Es wird noch nichts verraten!", - "unlocked": false - }, - { - "name": "Ein Geschenk für Kinder", - "description": "An diesem Punkt unserer Freundschaft betrachten wir natürlich auch Ihr Kind als Freund – was bedeutet, dass es bei jeder Übernachtung ein ganz spezielles Geschenk von uns erhält. Und warum? Weil Kinder einfach cool sind! Sie haben eine VIP-Behandlung verdient.", - "unlocked": false - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requirement": "100 000 Punkte", - "description": "Sie haben uns während zahlreicher Aufenthalte, Happy Hours und Workouts im Fitnessstudio die Treue gehalten – deshalb wollen wir uns mit einigen unserer großartigsten Belohnungen bei Ihnen revanchieren.", - "icon": "/_static/icons/loyaltylevels/loyal-friend.svg", - "benefits": [ - { - "name": "Freundschaftspreise", - "description": "Als unser Freund erhalten Sie immer das beste Angebot: an jedem Tag und an jedem Ort. Und dafür ist weder ein Handschlag noch ein Buchungscode erforderlich – stürzen Sie sich einfach ins Buchungserlebnis und überzeugen Sie sich selbst.", - "unlocked": true - }, - { - "name": "Rabatt auf Speisen", - "description": "Was gibt es Schöneres als einen Preisnachlass? Als Freund erhalten Sie an Wochenenden und ausgewählten Feiertagen 10-15% Rabatt auf die Speisen in unseren Restaurants und Hotelshops – und zwar unabhängig davon, ob Sie bei uns übernachten oder nicht. Also kommen Sie vorbei und tun Sie sich etwas Gutes! Zum Beispiel mit unserem zusätzlichen Zimmerservice.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Kostenloser Kinder-Mocktail während des Aufenthalts", - "description": "Oh, Sie haben ja die Kinder dabei! Wie schön – hallo junger Freund! Wir möchten, dass sich Kinder bei uns wie VIPs fühlen, und deshalb bekommen sie von uns bei jedem Aufenthalt einen erfrischenden Mocktail spendiert.", - "unlocked": true - }, - { - "name": "Später Check-out, wenn verfügbar", - "description": "Wir wissen, dass ein später Check-out manchmal eine große Erleichterung sein kann. Und an diesem Punkt unserer Freundschaft gibt es keinen Grund mehr, sich aus dem Bett zu quälen: Wir kommen Ihnen gerne entgegen. Checken Sie kostenlos 1 Stunde später aus und schlafen Sie einfach etwas länger.", - "unlocked": true - }, - { - "name": "Restaurantgutschein", - "description": "Wir schenken Ihnen einen zusätzlichen Restaurantgutschein über 10 € für jede Übernachtung, mit der Sie bei uns Freundschaftspunkte sammeln. Dafür bekommen Sie schon einen ganzen Berg von Croissants! Klingt das nicht verlockend?", - "unlocked": true, - "value": "10 €" - }, - { - "name": "Freundschaftsbonus", - "description": "Hier haben wir etwas wirklich Großartiges für Sie: Jedes Mal, wenn Sie einen neuen Schub an Freundschaftspunkten erhalten, gibt es von uns 25 % dazu – ein ordentlicher Extra-Schub! Wenn Sie also anfangen, diese Punkte für Aufenthalte, Mahlzeiten und anderes zu sammeln, erhalten Sie im Handumdrehen eine kostenlose Übernachtung.", - "unlocked": true, - "value": "25%" - }, - { - "name": "Früher Check-in, wenn verfügbar", - "description": "Sie treffen schon früher bei uns ein? Kein Problem! Checken Sie einfach kostenlos eine Stunde früher ein – so können Sie sich gleich ein wenig ausruhen und haben noch mehr von Ihrem Aufenthalt.", - "unlocked": true - }, - { - "name": "Kostenlose Upgrades wenn verfügbar", - "description": "Um auf den Punkt zu kommen – wir setzen noch einen drauf. Auf dieser Stufe unserer Freundschaft bieten wir Ihnen, wann immer es möglich ist, ein Zimmer-Upgrade an – so wird Ihr Aufenthalt noch komfortabler. Wenn das nicht großartig klingt …", - "unlocked": true - }, - { - "name": "Frühstück für zwei zum Preis von einem", - "description": "Wenn das kein delikater Deal ist: Egal, ob Sie bei uns übernachten oder nicht, schnappen Sie sich einen Frühstückspartner, denn jetzt können Sie zu zweit zum Preis von einem bei uns essen! Informieren Sie sich einfach vorher über alle Details und genießen Sie ein gemütliches Frühstück zu zweit.", - "unlocked": true - }, - { - "name": "48-Stunden-Zimmergarantie", - "description": "Pssst! Diesen ganz besonderen Leckerbissen bekommen bei uns nur die Wenigsten! Also aufgepasst: Selbst wenn wir völlig ausgebucht sind, erhalten Sie bei uns garantiert ein Zimmer, wenn Sie 48 Stunden im Voraus buchen. Ist das nicht einfach unglaublich?", - "unlocked": false - }, - { - "name": "Ein kostenloses Frühstück – jederzeit", - "description": "Lust auf einen leckeren Start in den Tag? Dann schauen Sie doch einfach bei uns vorbei! Denn jetzt können Sie Ihren Tag mit einem kostenlosen Frühstück beginnen – und zwar ganz unabhängig davon, ob Sie bei uns übernachten oder nicht.", - "unlocked": false - }, - { - "name": "Ein exklusives Geschenk pro Jahr", - "description": "Als Best Friend haben Sie natürlich eine königliche Behandlung verdient – deshalb haben wir einmal im Jahr ein exklusives und ziemlich großartiges Geschenk für Sie. Neugierig? Tja, das bleibt leider eine Überraschung. Es wird noch nichts verraten!", - "unlocked": false - }, - { - "name": "Ein Geschenk für Kinder", - "description": "An diesem Punkt unserer Freundschaft betrachten wir natürlich auch Ihr Kind als Freund – was bedeutet, dass es bei jeder Übernachtung ein ganz spezielles Geschenk von uns erhält. Und warum? Weil Kinder einfach cool sind! Sie haben eine VIP-Behandlung verdient.", - "unlocked": false - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requirement": "250 000 Punkte", - "description": "Es spielt keine Rolle, ob Haupt- oder Nebensaison: Sie sind immer für uns da. Genießen Sie noch mehr individuelle Vorteile – genau nach Ihrem Geschmack.", - "icon": "/_static/icons/loyaltylevels/true-friend.svg", - "benefits": [ - { - "name": "Freundschaftspreise", - "description": "Als unser Freund erhalten Sie immer das beste Angebot: an jedem Tag und an jedem Ort. Und dafür ist weder ein Handschlag noch ein Buchungscode erforderlich – stürzen Sie sich einfach ins Buchungserlebnis und überzeugen Sie sich selbst.", - "unlocked": true - }, - { - "name": "Rabatt auf Speisen", - "description": "Was gibt es Schöneres als einen Preisnachlass? Als Freund erhalten Sie an Wochenenden und ausgewählten Feiertagen 10-15% Rabatt auf die Speisen in unseren Restaurants und Hotelshops – und zwar unabhängig davon, ob Sie bei uns übernachten oder nicht. Also kommen Sie vorbei und tun Sie sich etwas Gutes! Zum Beispiel mit unserem zusätzlichen Zimmerservice.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Kostenloser Kinder-Mocktail während des Aufenthalts", - "description": "Oh, Sie haben ja die Kinder dabei! Wie schön – hallo junger Freund! Wir möchten, dass sich Kinder bei uns wie VIPs fühlen, und deshalb bekommen sie von uns bei jedem Aufenthalt einen erfrischenden Mocktail spendiert.", - "unlocked": true - }, - { - "name": "Später Check-out, wenn verfügbar", - "description": "Wir wissen, dass ein später Check-out manchmal eine große Erleichterung sein kann. Und an diesem Punkt unserer Freundschaft gibt es keinen Grund mehr, sich aus dem Bett zu quälen: Wir kommen Ihnen gerne entgegen. Checken Sie kostenlos 1 Stunde später aus und schlafen Sie einfach etwas länger.", - "unlocked": true - }, - { - "name": "Restaurantgutschein", - "description": "Zwischen uns läuft es inzwischen so gut, dass Sie für jede Nacht, in der Sie bei uns Freundschaftspunkte sammeln, zusätzlich noch einen Restaurantgutschein im Wert von 15 € erhalten. Perfekt für Ihr nächstes köstliches Frühstück oder Abendessen!", - "unlocked": true, - "value": "15 €" - }, - { - "name": "Freundschaftsbonus", - "description": "Was für ein Spaß! Jedes Mal, wenn Sie einen neuen Schub an Freundschaftspunkten erhalten, gibt es von uns 25-50 % dazu – ein ordentlicher Extraschub! Je früher Sie also anfangen, Punkte für Ihre Aufenthalte, Mahlzeiten und vieles mehr zu sammeln, desto schneller erhalten Sie Ihre erste kostenlose Übernachtung.", - "unlocked": true, - "value": "50%" - }, - { - "name": "Früher Check-in, wenn verfügbar", - "description": "Sie treffen schon früher bei uns ein? Kein Problem! Checken Sie einfach kostenlos eine Stunde früher ein – so können Sie sich gleich ein wenig ausruhen und haben noch mehr von Ihrem Aufenthalt.", - "unlocked": true - }, - { - "name": "Kostenlose Upgrades wenn verfügbar", - "description": "Um auf den Punkt zu kommen – wir setzen noch einen drauf. Auf dieser Stufe unserer Freundschaft bieten wir Ihnen, wann immer es möglich ist, ein Zimmer-Upgrade an – so wird Ihr Aufenthalt noch komfortabler. Wenn das nicht großartig klingt …", - "unlocked": true - }, - { - "name": "Frühstück für zwei zum Preis von einem", - "description": "Wenn das kein delikater Deal ist: Egal, ob Sie bei uns übernachten oder nicht, schnappen Sie sich einen Frühstückspartner, denn jetzt können Sie zu zweit zum Preis von einem bei uns essen! Informieren Sie sich einfach vorher über alle Details und genießen Sie ein gemütliches Frühstück zu zweit.", - "unlocked": false - }, - { - "name": "48-Stunden-Zimmergarantie", - "description": "Pssst! Diesen ganz besonderen Leckerbissen bekommen bei uns nur die Wenigsten! Also aufgepasst: Selbst wenn wir völlig ausgebucht sind, erhalten Sie bei uns garantiert ein Zimmer, wenn Sie 48 Stunden im Voraus buchen. Ist das nicht einfach unglaublich?", - "unlocked": true - }, - { - "name": "Ein kostenloses Frühstück – jederzeit", - "description": "Lust auf einen leckeren Start in den Tag? Dann schauen Sie doch einfach bei uns vorbei! Denn jetzt können Sie Ihren Tag mit einem kostenlosen Frühstück beginnen – und zwar ganz unabhängig davon, ob Sie bei uns übernachten oder nicht.", - "unlocked": true - }, - { - "name": "Ein exklusives Geschenk pro Jahr", - "description": "Als Best Friend haben Sie natürlich eine königliche Behandlung verdient – deshalb haben wir einmal im Jahr ein exklusives und ziemlich großartiges Geschenk für Sie. Neugierig? Tja, das bleibt leider eine Überraschung. Es wird noch nichts verraten!", - "unlocked": false - }, - { - "name": "Ein Geschenk für Kinder", - "description": "An diesem Punkt unserer Freundschaft betrachten wir natürlich auch Ihr Kind als Freund – was bedeutet, dass es bei jeder Übernachtung ein ganz spezielles Geschenk von uns erhält. Und warum? Weil Kinder einfach cool sind! Sie haben eine VIP-Behandlung verdient.", - "unlocked": false - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requirement": "400 000 Punkte oder 100 Nächte", - "description": "Für eine Freundschaft wie diese gibt es im Grunde keine passenden Worte, aber wir versuchen es trotzdem: Denn es könnte gar nichts Besseres geben, wenn es um sehr, sehr exklusive Erlebnisse geht!", - "icon": "/_static/icons/loyaltylevels/best-friend.svg", - "benefits": [ - { - "name": "Freundschaftspreise", - "description": "Als unser Freund erhalten Sie immer das beste Angebot: an jedem Tag und an jedem Ort. Und dafür ist weder ein Handschlag noch ein Buchungscode erforderlich – stürzen Sie sich einfach ins Buchungserlebnis und überzeugen Sie sich selbst.", - "unlocked": true - }, - { - "name": "Rabatt auf Speisen", - "description": "Was gibt es Schöneres als einen Preisnachlass? Als Freund erhalten Sie an Wochenenden und ausgewählten Feiertagen 10-15% Rabatt auf die Speisen in unseren Restaurants und Hotelshops – und zwar unabhängig davon, ob Sie bei uns übernachten oder nicht. Also kommen Sie vorbei und tun Sie sich etwas Gutes! Zum Beispiel mit unserem zusätzlichen Zimmerservice.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Kostenloser Kinder-Mocktail während des Aufenthalts", - "description": "Oh, Sie haben ja die Kinder dabei! Wie schön – hallo junger Freund! Wir möchten, dass sich Kinder bei uns wie VIPs fühlen, und deshalb bekommen sie von uns bei jedem Aufenthalt einen erfrischenden Mocktail spendiert.", - "unlocked": true - }, - { - "name": "Später Check-out, wenn verfügbar", - "description": "Wir wissen, dass ein später Check-out manchmal eine große Erleichterung sein kann. Und an diesem Punkt unserer Freundschaft gibt es keinen Grund mehr, sich aus dem Bett zu quälen: Wir kommen Ihnen gerne entgegen. Checken Sie kostenlos 1 Stunde später aus und schlafen Sie einfach etwas länger.", - "unlocked": true - }, - { - "name": "Restaurantgutschein", - "description": "Für jede Nacht, in der Sie bei uns Freundschaftspunkte sammeln, erhalten Sie einen Restaurantgutschein in Höhe von 20 €. Stellen Sie sich vor, wie viele Croissants Sie dafür essen könnten! Wenn das nicht verlockend ist?", - "unlocked": true, - "value": "20 €" - }, - { - "name": "Freundschaftsbonus", - "description": "Was für ein Spaß! Jedes Mal, wenn Sie einen neuen Schub an Freundschaftspunkten erhalten, gibt es von uns 25-50 % dazu – ein ordentlicher Extraschub! Je früher Sie also anfangen, Punkte für Ihre Aufenthalte, Mahlzeiten und vieles mehr zu sammeln, desto schneller erhalten Sie Ihre erste kostenlose Übernachtung.", - "unlocked": true, - "value": "50%" - }, - { - "name": "Früher Check-in, wenn verfügbar", - "description": "Sie treffen schon früher bei uns ein? Kein Problem! Checken Sie einfach kostenlos eine Stunde früher ein – so können Sie sich gleich ein wenig ausruhen und haben noch mehr von Ihrem Aufenthalt.", - "unlocked": true - }, - { - "name": "Kostenlose Upgrades wenn verfügbar", - "description": "Um auf den Punkt zu kommen – wir setzen noch einen drauf. Auf dieser Stufe unserer Freundschaft bieten wir Ihnen, wann immer es möglich ist, ein Zimmer-Upgrade an – so wird Ihr Aufenthalt noch komfortabler. Wenn das nicht großartig klingt …", - "unlocked": true - }, - { - "name": "Frühstück für zwei zum Preis von einem", - "description": "Wenn das kein delikater Deal ist: Egal, ob Sie bei uns übernachten oder nicht, schnappen Sie sich einen Frühstückspartner, denn jetzt können Sie zu zweit zum Preis von einem bei uns essen! Informieren Sie sich einfach vorher über alle Details und genießen Sie ein gemütliches Frühstück zu zweit.", - "unlocked": false - }, - { - "name": "48-Stunden-Zimmergarantie", - "description": "Pssst! Diesen ganz besonderen Leckerbissen bekommen bei uns nur die Wenigsten! Also aufgepasst: Selbst wenn wir völlig ausgebucht sind, erhalten Sie bei uns garantiert ein Zimmer, wenn Sie 48 Stunden im Voraus buchen. Ist das nicht einfach unglaublich?", - "unlocked": true - }, - { - "name": "Ein kostenloses Frühstück – jederzeit", - "description": "Lust auf einen leckeren Start in den Tag? Dann schauen Sie doch einfach bei uns vorbei! Denn jetzt können Sie Ihren Tag mit einem kostenlosen Frühstück beginnen – und zwar ganz unabhängig davon, ob Sie bei uns übernachten oder nicht.", - "unlocked": true - }, - { - "name": "Ein exklusives Geschenk pro Jahr", - "description": "Als Best Friend haben Sie natürlich eine königliche Behandlung verdient – deshalb haben wir einmal im Jahr ein exklusives und ziemlich großartiges Geschenk für Sie. Neugierig? Tja, das bleibt leider eine Überraschung. Es wird noch nichts verraten!", - "unlocked": true - }, - { - "name": "Ein Geschenk für Kinder", - "description": "An diesem Punkt unserer Freundschaft betrachten wir natürlich auch Ihr Kind als Freund – was bedeutet, dass es bei jeder Übernachtung ein ganz spezielles Geschenk von uns erhält. Und warum? Weil Kinder einfach cool sind! Sie haben eine VIP-Behandlung verdient.", - "unlocked": true - } - ] - } - ] -} diff --git a/components/Blocks/DynamicContent/OverviewTable/data/EN.json b/components/Blocks/DynamicContent/OverviewTable/data/EN.json deleted file mode 100644 index b791a6e60..000000000 --- a/components/Blocks/DynamicContent/OverviewTable/data/EN.json +++ /dev/null @@ -1,538 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requirement": "0p", - "description": "This is the start of something beautiful: as a New Friend, get ready for a journey of delightful Scandic discoveries.", - "icon": "/_static/icons/loyaltylevels/new-friend.svg", - "benefits": [ - { - "name": "Friendly rates", - "description": "Being our friend means you’re always scoring the best deal: any day, any place. No secret handshake or booking code needed – just dive into the booking and see for yourself.", - "unlocked": true - }, - { - "name": "Discount on food", - "description": "Yumm! Enjoy a savory 10% or more off in our restaurant and hotel shop on weekends. This offer stands whether you're our guest for the night or just dropping by for a bite. So, go ahead, treat yourself and order that extra room service.", - "unlocked": true, - "value": "10%" - }, - { - "name": "Free kids mocktail during stay", - "description": "Got the kiddos with you? That’s amazing – hello there, buddy! We wanna make sure that kids feel like the VIPs they really are, so every stay they get a refreshing mocktail on us.", - "unlocked": true - }, - { - "name": "Late checkout when available", - "description": "We get it – late checkout can be a total trip-saver at times. At this point in our friendship, there’s no need to rush out of bed: we’ve got you covered. Check out 1 hour later at no cost and get those last, extra refreshing minutes of sleep.", - "unlocked": false - }, - { - "name": "Restaurant voucher", - "description": "Yeah, that’s right: for each friendship point-boosting night, you get a voucher to redeem when you dine or drink at our restaurants and bars.", - "unlocked": false - }, - { - "name": "Friendship point boost", - "description": "Now here’s something pretty sweet: every time you get a boost of friendship points, you get 25% or 50% extra – a boost! So you know, start racking up those points on stays, meals and more and you’ll get a free night in no time.", - "unlocked": false - }, - { - "name": "Early check-in when available", - "description": "Wanna get a head start on your stay? No problem, no problem. Check in 1 hour earlier at no cost and take the highway to relaxation and bliss.", - "unlocked": false - }, - { - "name": "Free upgrades when available", - "description": "Let's cut to the chase – we’re taking things up a notch. At this level in our friendship, we'll upgrade your room whenever possible for an even comfier stay. Sounds good, right?", - "unlocked": false - }, - { - "name": "2-for-1 breakfast", - "description": "Now, this is a delicious deal: regardless if you’re staying with us or not, go grab a breakfast buddy because you're eating two for the price of one! Just make sure to check the details first so everything is in place for your yummy get-together.", - "unlocked": false - }, - { - "name": "48hr room guarantee", - "description": "Hush – this one's a special treat just for a few of you! So listen up: even if we're fully booked, you're guaranteed a room as long as you book 48 hours in advance. Pretty incredible!", - "unlocked": false - }, - { - "name": "Free breakfast – always", - "description": "Care for a tasty morning treat? Come on over! You now get to kickstart your day with free breakfast – and guess what: that goes regardless if you’re staying with us or not.", - "unlocked": false - }, - { - "name": "Yearly awesome gift", - "description": "As our Best Friend, you totally deserve princess treatment – so we’ve got an exclusive, annual and pretty awesome gift all lined up. What it is? Well, that’s a surprise. No peeking!", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "At this friendship level, your kid is our friend, too – and that means a special kid’s boost gift when you stay with us. Why? Because kids are cool! They totally deserve VIP treatment.", - "unlocked": false - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requirement": "5 000p", - "description": "You've been around a lot lately! And honestly, we feel like we're vibing - one enjoyable stay at a time.", - "icon": "/_static/icons/loyaltylevels/good-friend.svg", - "benefits": [ - { - "name": "Friendly rates", - "description": "Being our friend means you’re always scoring the best deal: any day, any place. No secret handshake or booking code needed – just dive into the booking and see for yourself.", - "unlocked": true - }, - { - "name": "Discount on food", - "description": "What’s more delicious than a discount? As our friend, you get 10-15% off on our restaurant and hotel shop food on weekends and selected holidays – and that goes for when you’re staying with us and when you’re not. So come on, spoil yourself! Get some room service.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Free kids mocktail during stay", - "description": "Got the kiddos with you? That’s amazing – hello there, buddy! We wanna make sure that kids feel like the VIPs they really are, so every stay they get a refreshing mocktail on us.", - "unlocked": true - }, - { - "name": "Late checkout when available", - "description": "We get it – late checkout can be a total trip-saver at times. At this point in our friendship, there’s no need to rush out of bed: we’ve got you covered. Check out 1 hour later at no cost and get those last, extra refreshing minutes of sleep.", - "unlocked": false - }, - { - "name": "Restaurant voucher", - "description": "Yeah, that’s right: for each friendship point-boosting night, you get a voucher to redeem when you dine or drink at our restaurants and bars.", - "unlocked": false - }, - { - "name": "Friendship point boost", - "description": "Now here’s something pretty sweet: every time you get a boost of friendship points, you get 25% or 50% extra – a boost! So you know, start racking up those points on stays, meals and more and you’ll get a free night in no time.", - "unlocked": false - }, - { - "name": "Early check-in when available", - "description": "Wanna get a head start on your stay? No problem, no problem. Check in 1 hour earlier at no cost and take the highway to relaxation and bliss.", - "unlocked": false - }, - { - "name": "Free upgrades when available", - "description": "Let's cut to the chase – we’re taking things up a notch. At this level in our friendship, we'll upgrade your room whenever possible for an even comfier stay. Sounds good, right?", - "unlocked": false - }, - { - "name": "2-for-1 breakfast", - "description": "Now, this is a delicious deal: regardless if you’re staying with us or not, go grab a breakfast buddy because you're eating two for the price of one! Just make sure to check the details first so everything is in place for your yummy get-together.", - "unlocked": false - }, - { - "name": "48hr room guarantee", - "description": "Hush – this one's a special treat just for a few of you! So listen up: even if we're fully booked, you're guaranteed a room as long as you book 48 hours in advance. Pretty incredible!", - "unlocked": false - }, - { - "name": "Free breakfast – always", - "description": "Care for a tasty morning treat? Come on over! You now get to kickstart your day with free breakfast – and guess what: that goes regardless if you’re staying with us or not.", - "unlocked": false - }, - { - "name": "Yearly awesome gift", - "description": "As our Best Friend, you totally deserve princess treatment – so we’ve got an exclusive, annual and pretty awesome gift all lined up. What it is? Well, that’s a surprise. No peeking!", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "At this friendship level, your kid is our friend, too – and that means a special kid’s boost gift when you stay with us. Why? Because kids are cool! They totally deserve VIP treatment.", - "unlocked": false - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requirement": "10 000p", - "description": "It's serious now: we're really getting to know each other which makes your Scandic experiences a lot more personal.", - "icon": "/_static/icons/loyaltylevels/close-friend.svg", - "benefits": [ - { - "name": "Friendly rates", - "description": "Being our friend means you’re always scoring the best deal: any day, any place. No secret handshake or booking code needed – just dive into the booking and see for yourself.", - "unlocked": true - }, - { - "name": "Discount on food", - "description": "What’s more delicious than a discount? As our friend, you get 10-15% off on our restaurant and hotel shop food on weekends and selected holidays – and that goes for when you’re staying with us and when you’re not. So come on, spoil yourself! Get some room service.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Free kids mocktail during stay", - "description": "Got the kiddos with you? That’s amazing – hello there, buddy! We wanna make sure that kids feel like the VIPs they really are, so every stay they get a refreshing mocktail on us.", - "unlocked": true - }, - { - "name": "Late checkout when available", - "description": "We get it – late checkout can be a total trip-saver at times. At this point in our friendship, there’s no need to rush out of bed: we’ve got you covered. Check out 1 hour later at no cost and get those last, extra refreshing minutes of sleep.", - "unlocked": true - }, - { - "name": "Restaurant voucher", - "description": "Yeah, that’s right: for each friendship point-boosting night, you get a voucher to redeem when you dine or drink at our restaurants and bars.", - "unlocked": true, - "value": "€5" - }, - { - "name": "Friendship point boost", - "description": "Now here’s something pretty sweet: every time you get a boost of friendship points, you get 25% or 50% extra – a boost! So you know, start racking up those points on stays, meals and more and you’ll get a free night in no time.", - "unlocked": false - }, - { - "name": "Early check-in when available", - "description": "Wanna get a head start on your stay? No problem, no problem. Check in 1 hour earlier at no cost and take the highway to relaxation and bliss.", - "unlocked": false - }, - { - "name": "Free upgrades when available", - "description": "Let's cut to the chase – we’re taking things up a notch. At this level in our friendship, we'll upgrade your room whenever possible for an even comfier stay. Sounds good, right?", - "unlocked": false - }, - { - "name": "2-for-1 breakfast", - "description": "Now, this is a delicious deal: regardless if you’re staying with us or not, go grab a breakfast buddy because you're eating two for the price of one! Just make sure to check the details first so everything is in place for your yummy get-together.", - "unlocked": false - }, - { - "name": "48hr room guarantee", - "description": "Hush – this one's a special treat just for a few of you! So listen up: even if we're fully booked, you're guaranteed a room as long as you book 48 hours in advance. Pretty incredible!", - "unlocked": false - }, - { - "name": "Free breakfast – always", - "description": "Care for a tasty morning treat? Come on over! You now get to kickstart your day with free breakfast – and guess what: that goes regardless if you’re staying with us or not.", - "unlocked": false - }, - { - "name": "Yearly awesome gift", - "description": "As our Best Friend, you totally deserve princess treatment – so we’ve got an exclusive, annual and pretty awesome gift all lined up. What it is? Well, that’s a surprise. No peeking!", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "At this friendship level, your kid is our friend, too – and that means a special kid’s boost gift when you stay with us. Why? Because kids are cool! They totally deserve VIP treatment.", - "unlocked": false - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requirement": "25 000p", - "description": "Cheers to us! This seems to be going in the direction of friends for life - and that comes with access to a-whole-lotta-more of Scandic.", - "icon": "/_static/icons/loyaltylevels/dear-friend.svg", - "benefits": [ - { - "name": "Friendly rates", - "description": "Being our friend means you’re always scoring the best deal: any day, any place. No secret handshake or booking code needed – just dive into the booking and see for yourself.", - "unlocked": true - }, - { - "name": "Discount on food", - "description": "What’s more delicious than a discount? As our friend, you get 10-15% off on our restaurant and hotel shop food on weekends and selected holidays – and that goes for when you’re staying with us and when you’re not. So come on, spoil yourself! Get some room service.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Free kids mocktail during stay", - "description": "Got the kiddos with you? That’s amazing – hello there, buddy! We wanna make sure that kids feel like the VIPs they really are, so every stay they get a refreshing mocktail on us.", - "unlocked": true - }, - { - "name": "Late checkout when available", - "description": "We get it – late checkout can be a total trip-saver at times. At this point in our friendship, there’s no need to rush out of bed: we’ve got you covered. Check out 1 hour later at no cost and get those last, extra refreshing minutes of sleep.", - "unlocked": true - }, - { - "name": "Restaurant voucher", - "description": "Yeah, that’s right: for each friendship point-boosting night, you get a voucher to redeem when you dine or drink at our restaurants and bars.", - "unlocked": true, - "value": "€7.5" - }, - { - "name": "Friendship point boost", - "description": "Now here’s something pretty sweet: every time you get a boost of friendship points, you get 25% or 50% extra – a boost! So you know, start racking up those points on stays, meals and more and you’ll get a free night in no time.", - "unlocked": true, - "value": "+25%" - }, - { - "name": "Early check-in when available", - "description": "Wanna get a head start on your stay? No problem, no problem. Check in 1 hour earlier at no cost and take the highway to relaxation and bliss.", - "unlocked": true - }, - { - "name": "Free upgrades when available", - "description": "Let's cut to the chase – we’re taking things up a notch. At this level in our friendship, we'll upgrade your room whenever possible for an even comfier stay. Sounds good, right?", - "unlocked": false - }, - { - "name": "2-for-1 breakfast", - "description": "Now, this is a delicious deal: regardless if you’re staying with us or not, go grab a breakfast buddy because you're eating two for the price of one! Just make sure to check the details first so everything is in place for your yummy get-together.", - "unlocked": false - }, - { - "name": "48hr room guarantee", - "description": "Hush – this one's a special treat just for a few of you! So listen up: even if we're fully booked, you're guaranteed a room as long as you book 48 hours in advance. Pretty incredible!", - "unlocked": false - }, - { - "name": "Free breakfast – always", - "description": "Care for a tasty morning treat? Come on over! You now get to kickstart your day with free breakfast – and guess what: that goes regardless if you’re staying with us or not.", - "unlocked": false - }, - { - "name": "Yearly awesome gift", - "description": "As our Best Friend, you totally deserve princess treatment – so we’ve got an exclusive, annual and pretty awesome gift all lined up. What it is? Well, that’s a surprise. No peeking!", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "At this friendship level, your kid is our friend, too – and that means a special kid’s boost gift when you stay with us. Why? Because kids are cool! They totally deserve VIP treatment.", - "unlocked": false - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requirement": "100 000p", - "description": "You've stuck with us through stays, after works and gym sessions - so we'll stick with you through some of our very best rewards.", - "icon": "/_static/icons/loyaltylevels/loyal-friend.svg", - "benefits": [ - { - "name": "Friendly rates", - "description": "Being our friend means you’re always scoring the best deal: any day, any place. No secret handshake or booking code needed – just dive into the booking and see for yourself.", - "unlocked": true - }, - { - "name": "Discount on food", - "description": "What’s more delicious than a discount? As our friend, you get 10-15% off on our restaurant and hotel shop food on weekends and selected holidays – and that goes for when you’re staying with us and when you’re not. So come on, spoil yourself! Get some room service.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Free kids mocktail during stay", - "description": "Got the kiddos with you? That’s amazing – hello there, buddy! We wanna make sure that kids feel like the VIPs they really are, so every stay they get a refreshing mocktail on us.", - "unlocked": true - }, - { - "name": "Late checkout when available", - "description": "We get it – late checkout can be a total trip-saver at times. At this point in our friendship, there’s no need to rush out of bed: we’ve got you covered. Check out 1 hour later at no cost and get those last, extra refreshing minutes of sleep.", - "unlocked": true - }, - { - "name": "Restaurant voucher", - "description": "Yeah, that’s right: for each friendship point-boosting night, you get a voucher to redeem when you dine or drink at our restaurants and bars.", - "unlocked": true, - "value": "€10" - }, - { - "name": "Friendship point boost", - "description": "Now here’s something pretty sweet: every time you get a boost of friendship points, you get 25% or 50% extra – a boost! So you know, start racking up those points on stays, meals and more and you’ll get a free night in no time.", - "unlocked": true, - "value": "+25%" - }, - { - "name": "Early check-in when available", - "description": "Wanna get a head start on your stay? No problem, no problem. Check in 1 hour earlier at no cost and take the highway to relaxation and bliss.", - "unlocked": true - }, - { - "name": "Free upgrades when available", - "description": "Let's cut to the chase – we’re taking things up a notch. At this level in our friendship, we'll upgrade your room whenever possible for an even comfier stay. Sounds good, right?", - "unlocked": true - }, - { - "name": "2-for-1 breakfast", - "description": "Now, this is a delicious deal: regardless if you’re staying with us or not, go grab a breakfast buddy because you're eating two for the price of one! Just make sure to check the details first so everything is in place for your yummy get-together.", - "unlocked": true - }, - { - "name": "48hr room guarantee", - "description": "Hush – this one's a special treat just for a few of you! So listen up: even if we're fully booked, you're guaranteed a room as long as you book 48 hours in advance. Pretty incredible!", - "unlocked": false - }, - { - "name": "Free breakfast – always", - "description": "Care for a tasty morning treat? Come on over! You now get to kickstart your day with free breakfast – and guess what: that goes regardless if you’re staying with us or not.", - "unlocked": false - }, - { - "name": "Yearly awesome gift", - "description": "As our Best Friend, you totally deserve princess treatment – so we’ve got an exclusive, annual and pretty awesome gift all lined up. What it is? Well, that’s a surprise. No peeking!", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "At this friendship level, your kid is our friend, too – and that means a special kid’s boost gift when you stay with us. Why? Because kids are cool! They totally deserve VIP treatment.", - "unlocked": false - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requirement": "250 000p", - "description": "It doesn't matter if it's peak or off season, you're always there for us. Enjoy even more tailored perks - just the way you like them.", - "icon": "/_static/icons/loyaltylevels/true-friend.svg", - "benefits": [ - { - "name": "Friendly rates", - "description": "Being our friend means you’re always scoring the best deal: any day, any place. No secret handshake or booking code needed – just dive into the booking and see for yourself.", - "unlocked": true - }, - { - "name": "Discount on food", - "description": "What’s more delicious than a discount? As our friend, you get 10-15% off on our restaurant and hotel shop food on weekends and selected holidays – and that goes for when you’re staying with us and when you’re not. So come on, spoil yourself! Get some room service.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Free kids mocktail during stay", - "description": "Got the kiddos with you? That’s amazing – hello there, buddy! We wanna make sure that kids feel like the VIPs they really are, so every stay they get a refreshing mocktail on us.", - "unlocked": true - }, - { - "name": "Late checkout when available", - "description": "We get it – late checkout can be a total trip-saver at times. At this point in our friendship, there’s no need to rush out of bed: we’ve got you covered. Check out 1 hour later at no cost and get those last, extra refreshing minutes of sleep.", - "unlocked": true - }, - { - "name": "Restaurant voucher", - "description": "Yeah, that’s right: for each friendship point-boosting night, you get a voucher to redeem when you dine or drink at our restaurants and bars.", - "unlocked": true, - "value": "€15" - }, - { - "name": "Friendship point boost", - "description": "Now here’s something pretty sweet: every time you get a boost of friendship points, you get 25% or 50% extra – a boost! So you know, start racking up those points on stays, meals and more and you’ll get a free night in no time.", - "unlocked": true, - "value": "+50%" - }, - { - "name": "Early check-in when available", - "description": "Wanna get a head start on your stay? No problem, no problem. Check in 1 hour earlier at no cost and take the highway to relaxation and bliss.", - "unlocked": true - }, - { - "name": "Free upgrades when available", - "description": "Let's cut to the chase – we’re taking things up a notch. At this level in our friendship, we'll upgrade your room whenever possible for an even comfier stay. Sounds good, right?", - "unlocked": true - }, - { - "name": "2-for-1 breakfast", - "description": "Now, this is a delicious deal: regardless if you’re staying with us or not, go grab a breakfast buddy because you're eating two for the price of one! Just make sure to check the details first so everything is in place for your yummy get-together.", - "unlocked": false - }, - { - "name": "48hr room guarantee", - "description": "Hush – this one's a special treat just for a few of you! So listen up: even if we're fully booked, you're guaranteed a room as long as you book 48 hours in advance. Pretty incredible!", - "unlocked": true - }, - { - "name": "Free breakfast – always", - "description": "Care for a tasty morning treat? Come on over! You now get to kickstart your day with free breakfast – and guess what: that goes regardless if you’re staying with us or not.", - "unlocked": true - }, - { - "name": "Yearly awesome gift", - "description": "As our Best Friend, you totally deserve princess treatment – so we’ve got an exclusive, annual and pretty awesome gift all lined up. What it is? Well, that’s a surprise. No peeking!", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "At this friendship level, your kid is our friend, too – and that means a special kid’s boost gift when you stay with us. Why? Because kids are cool! They totally deserve VIP treatment.", - "unlocked": false - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requirement": "400 000p or 100 nights", - "description": "There are no words for a bond like this, but here's a few anyway: It simply doesn't get any better when it comes to very, very exclusive experiences!", - "icon": "/_static/icons/loyaltylevels/best-friend.svg", - "benefits": [ - { - "name": "Friendly rates", - "description": "Being our friend means you’re always scoring the best deal: any day, any place. No secret handshake or booking code needed – just dive into the booking and see for yourself.", - "unlocked": true - }, - { - "name": "Discount on food", - "description": "What’s more delicious than a discount? As our friend, you get 10-15% off on our restaurant and hotel shop food on weekends and selected holidays – and that goes for when you’re staying with us and when you’re not. So come on, spoil yourself! Get some room service.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Free kids mocktail during stay", - "description": "Got the kiddos with you? That’s amazing – hello there, buddy! We wanna make sure that kids feel like the VIPs they really are, so every stay they get a refreshing mocktail on us.", - "unlocked": true - }, - { - "name": "Late checkout when available", - "description": "We get it – late checkout can be a total trip-saver at times. At this point in our friendship, there’s no need to rush out of bed: we’ve got you covered. Check out 1 hour later at no cost and get those last, extra refreshing minutes of sleep.", - "unlocked": true - }, - { - "name": "Restaurant voucher", - "description": "Yeah, that’s right: for each friendship point-boosting night, you get a voucher to redeem when you dine or drink at our restaurants and bars.", - "unlocked": true, - "value": "€20" - }, - { - "name": "Friendship point boost", - "description": "Now here’s something pretty sweet: every time you get a boost of friendship points, you get 25% or 50% extra – a boost! So you know, start racking up those points on stays, meals and more and you’ll get a free night in no time.", - "unlocked": true, - "value": "+50%" - }, - { - "name": "Early check-in when available", - "description": "Wanna get a head start on your stay? No problem, no problem. Check in 1 hour earlier at no cost and take the highway to relaxation and bliss.", - "unlocked": true - }, - { - "name": "Free upgrades when available", - "description": "Let's cut to the chase – we’re taking things up a notch. At this level in our friendship, we'll upgrade your room whenever possible for an even comfier stay. Sounds good, right?", - "unlocked": true - }, - { - "name": "2-for-1 breakfast", - "description": "Now, this is a delicious deal: regardless if you’re staying with us or not, go grab a breakfast buddy because you're eating two for the price of one! Just make sure to check the details first so everything is in place for your yummy get-together.", - "unlocked": false - }, - { - "name": "48hr room guarantee", - "description": "Hush – this one's a special treat just for a few of you! So listen up: even if we're fully booked, you're guaranteed a room as long as you book 48 hours in advance. Pretty incredible!", - "unlocked": true - }, - { - "name": "Free breakfast – always", - "description": "Care for a tasty morning treat? Come on over! You now get to kickstart your day with free breakfast – and guess what: that goes regardless if you’re staying with us or not.", - "unlocked": true - }, - { - "name": "Yearly awesome gift", - "description": "As our Best Friend, you totally deserve princess treatment – so we’ve got an exclusive, annual and pretty awesome gift all lined up. What it is? Well, that’s a surprise. No peeking!", - "unlocked": true - }, - { - "name": "Kid’s boost", - "description": "At this friendship level, your kid is our friend, too – and that means a special kid’s boost gift when you stay with us. Why? Because kids are cool! They totally deserve VIP treatment.", - "unlocked": true - } - ] - } - ] -} diff --git a/components/Blocks/DynamicContent/OverviewTable/data/FI.json b/components/Blocks/DynamicContent/OverviewTable/data/FI.json deleted file mode 100644 index d1aa3ee98..000000000 --- a/components/Blocks/DynamicContent/OverviewTable/data/FI.json +++ /dev/null @@ -1,538 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requirement": "0 p", - "description": "Ystävänämme pääset nauttimaan kaikesta ihanasta, mitä Scandic tarjoaa.", - "icon": "/_static/icons/loyaltylevels/new-friend.svg", - "benefits": [ - { - "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan.", - "unlocked": true - }, - { - "name": "Alennus ruoasta", - "description": "Mikä herkullinen etu! Hyödynnä 10 %:n alennus hotelliemme ravintoloissa ja shopissa viikonloppuisin. Tarjous on voimassa niin majoittujille kuin hotellitunnelmaa hetkeksi etsiville. Hemmottele siis itseäsi ja löydä tie lähimpään Scandiciin.", - "unlocked": true, - "value": "10 %" - }, - { - "name": "Mocktail lapsille maksutta", - "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", - "unlocked": true - }, - { - "name": "Myöhäinen uloskirjautuminen", - "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": false - }, - { - "name": "Ravintolakuponki", - "description": "", - "unlocked": false - }, - { - "name": "Enemmän pisteitä", - "description": "", - "unlocked": false - }, - { - "name": "Aikainen sisäänkirjautuminen", - "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": false - }, - { - "name": "Maksuton huoneluokan korotus", - "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": false - }, - { - "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", - "unlocked": false - }, - { - "name": "48 tunnin huonetakuu", - "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", - "unlocked": false - }, - { - "name": "Aamiainen aina maksutta", - "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", - "unlocked": false - }, - { - "name": "Upea vuotuinen lahja", - "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", - "unlocked": false - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requirement": "5 000 p", - "description": "Tästä on hyvä jatkaa, yksi yöpyminen ja iloinen yllätys kerrallaan!", - "icon": "/_static/icons/loyaltylevels/good-friend.svg", - "benefits": [ - { - "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan.", - "unlocked": true - }, - { - "name": "Alennus ruoasta", - "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Mocktail lapsille maksutta", - "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", - "unlocked": true - }, - { - "name": "Myöhäinen uloskirjautuminen", - "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": false - }, - { - "name": "Ravintolakuponki", - "description": "", - "unlocked": false - }, - { - "name": "Enemmän pisteitä", - "description": "", - "unlocked": false - }, - { - "name": "Aikainen sisäänkirjautuminen", - "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": false - }, - { - "name": "Maksuton huoneluokan korotus", - "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": false - }, - { - "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", - "unlocked": false - }, - { - "name": "48 tunnin huonetakuu", - "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", - "unlocked": false - }, - { - "name": "Aamiainen aina maksutta", - "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", - "unlocked": false - }, - { - "name": "Upea vuotuinen lahja", - "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", - "unlocked": false - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requirement": "10 000 p", - "description": "Nyt etusi vain paranevat, sillä olemmehan jo enemmän kuin hyvän päivän tuttuja.", - "icon": "/_static/icons/loyaltylevels/close-friend.svg", - "benefits": [ - { - "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan.", - "unlocked": true - }, - { - "name": "Alennus ruoasta", - "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Mocktail lapsille maksutta", - "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", - "unlocked": true - }, - { - "name": "Myöhäinen uloskirjautuminen", - "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Ravintolakuponki", - "description": "Tässä sinulle makoisa herkku: saat 5 € ravintolakupongin jokaisesta pisteisiin oikeuttavasta yöstä. Katso tästä lisätiedot.", - "unlocked": true, - "value": "5 €" - }, - { - "name": "Enemmän pisteitä", - "description": "", - "unlocked": false - }, - { - "name": "Aikainen sisäänkirjautuminen", - "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": false - }, - { - "name": "Maksuton huoneluokan korotus", - "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": false - }, - { - "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", - "unlocked": false - }, - { - "name": "48 tunnin huonetakuu", - "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", - "unlocked": false - }, - { - "name": "Aamiainen aina maksutta", - "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", - "unlocked": false - }, - { - "name": "Upea vuotuinen lahja", - "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", - "unlocked": false - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requirement": "25 000 p", - "description": "Kippis syventyvälle ystävyydellemme. Nyt pääset nauttimaan liudasta uusia etuja.", - "icon": "/_static/icons/loyaltylevels/dear-friend.svg", - "benefits": [ - { - "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan.", - "unlocked": true - }, - { - "name": "Alennus ruoasta", - "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Mocktail lapsille maksutta", - "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", - "unlocked": true - }, - { - "name": "Myöhäinen uloskirjautuminen", - "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Ravintolakuponki", - "description": "Ystävyytemme on entistä makeampaa! Saat 7,50 € ravintolakupongin jokaisesta pisteisiin oikeuttavasta yöstä. Huippua, vai mitä?", - "unlocked": true, - "value": "7,50 €" - }, - { - "name": "Enemmän pisteitä", - "description": "Tässä lisäboostia sinulle: saat 25 % enemmän pisteitä joka kerta kun ansaitset pisteitä! Pistä tuulemaan ja haali pisteet yöpymisistä, aterioista ja muusta, niin pääset nauttimaan palkintoyöstä tuossa tuokiossa.", - "unlocked": true, - "value": "25 %" - }, - { - "name": "Aikainen sisäänkirjautuminen", - "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Maksuton huoneluokan korotus", - "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": false - }, - { - "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", - "unlocked": false - }, - { - "name": "48 tunnin huonetakuu", - "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", - "unlocked": false - }, - { - "name": "Aamiainen aina maksutta", - "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", - "unlocked": false - }, - { - "name": "Upea vuotuinen lahja", - "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", - "unlocked": false - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requirement": "100 000 p", - "description": "Haluamme panostaa ystävyyteemme myös jatkossa ja annammekin sinulle kasan uusia, ihania etuja.", - "icon": "/_static/icons/loyaltylevels/loyal-friend.svg", - "benefits": [ - { - "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan.", - "unlocked": true - }, - { - "name": "Alennus ruoasta", - "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Mocktail lapsille maksutta", - "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", - "unlocked": true - }, - { - "name": "Myöhäinen uloskirjautuminen", - "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Ravintolakuponki", - "description": "Saat 10 € ravintolakupongin jokaisesta pisteisiin oikeuttavasta yöstä. Se tuo varmasti iloa seuraavaan herkutteluhetkeen.", - "unlocked": true, - "value": "10 €" - }, - { - "name": "Enemmän pisteitä", - "description": "Tässä lisäboostia sinulle: saat 25 % enemmän pisteitä joka kerta kun ansaitset pisteitä! Pistä tuulemaan ja haali pisteet yöpymisistä, aterioista ja muusta, niin pääset nauttimaan palkintoyöstä tuossa tuokiossa.", - "unlocked": true, - "value": "25 %" - }, - { - "name": "Aikainen sisäänkirjautuminen", - "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Maksuton huoneluokan korotus", - "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", - "unlocked": true - }, - { - "name": "48 tunnin huonetakuu", - "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", - "unlocked": false - }, - { - "name": "Aamiainen aina maksutta", - "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", - "unlocked": false - }, - { - "name": "Upea vuotuinen lahja", - "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", - "unlocked": false - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requirement": "250 000 p", - "description": "Tosiystävän tapaan haluamme palkita sinua entistä yksilöllisemmillä eduilla.", - "icon": "/_static/icons/loyaltylevels/true-friend.svg", - "benefits": [ - { - "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan.", - "unlocked": true - }, - { - "name": "Alennus ruoasta", - "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Mocktail lapsille maksutta", - "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", - "unlocked": true - }, - { - "name": "Myöhäinen uloskirjautuminen", - "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Ravintolakuponki", - "description": "Ystävyytemme vain vahvistuu ja nyt saat 15 € ravintolakupongin jokaisesta pisteisiin oikeuttavasta yöstä. Kuinkahan paljon herkkuja sillä saisi?", - "unlocked": true, - "value": "15 €" - }, - { - "name": "Enemmän pisteitä", - "description": "Saat 25 % tai 50 % enemmän pisteitä joka kerta kun ansaitset pisteitä!.", - "unlocked": true, - "value": "50 %" - }, - { - "name": "Aikainen sisäänkirjautuminen", - "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Maksuton huoneluokan korotus", - "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", - "unlocked": false - }, - { - "name": "48 tunnin huonetakuu", - "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", - "unlocked": true - }, - { - "name": "Aamiainen aina maksutta", - "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", - "unlocked": true - }, - { - "name": "Upea vuotuinen lahja", - "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", - "unlocked": false - }, - { - "name": "Kid’s boost", - "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", - "unlocked": false - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requirement": "400 000 p tai 100 yötä", - "description": "Koska sanat eivät riitä kiittämään ystävyydestämme, pääset nyt käsiksi kaikkein eksklusiivisimpiin elämyksiin.", - "icon": "/_static/icons/loyaltylevels/best-friend.svg", - "benefits": [ - { - "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan.", - "unlocked": true - }, - { - "name": "Alennus ruoasta", - "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Mocktail lapsille maksutta", - "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", - "unlocked": true - }, - { - "name": "Myöhäinen uloskirjautuminen", - "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Ravintolakuponki", - "description": "Ystävänämme saat ravintolakupongin jokaisesta pisteisiin oikeuttavasta yöstä.", - "unlocked": true, - "value": "20 €" - }, - { - "name": "Enemmän pisteitä", - "description": "Saat 25 % tai 50 % enemmän pisteitä joka kerta kun ansaitset pisteitä!.", - "unlocked": true, - "value": "50 %" - }, - { - "name": "Aikainen sisäänkirjautuminen", - "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Maksuton huoneluokan korotus", - "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", - "unlocked": true - }, - { - "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", - "unlocked": false - }, - { - "name": "48 tunnin huonetakuu", - "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", - "unlocked": true - }, - { - "name": "Aamiainen aina maksutta", - "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", - "unlocked": true - }, - { - "name": "Upea vuotuinen lahja", - "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", - "unlocked": true - }, - { - "name": "Kid’s boost", - "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", - "unlocked": true - } - ] - } - ] -} diff --git a/components/Blocks/DynamicContent/OverviewTable/data/NO.json b/components/Blocks/DynamicContent/OverviewTable/data/NO.json deleted file mode 100644 index fbe79e466..000000000 --- a/components/Blocks/DynamicContent/OverviewTable/data/NO.json +++ /dev/null @@ -1,538 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requirement": "0p", - "description": "Dette er starten på noe vakkert: Som en New Friend, gjør deg klar for en reise fylt av fine Scandic-opplevelser.", - "icon": "/_static/icons/loyaltylevels/new-friend.svg", - "benefits": [ - { - "name": "Vennlige priser", - "description": "Å være vennen vår betyr at du alltid får det beste tilbudet: Når som helst, hvor som helst. Det er ikke nødvendig med noe hemmelig håndtrykk eller bestillingskode – bare dykk inn i bestillingene og se selv.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Nam! Nyt en smakfull 10 % rabatt i restauranten og shoppen vår i helgene. Dette tilbudet gjelder enten du er gjesten vår over natten eller bare kommer innom for en matbit. Så, sett i gang, unn deg selv noe godt.", - "unlocked": true, - "value": "10 %" - }, - { - "name": "Gratis barne-mocktail under oppholdet", - "description": "Har du barna med deg? Det er fantastisk – hei der, kompis! Vi vil sørge for at ungene føler seg som de VIP-ene de faktisk er, og spanderer en forfriskende mocktail på dem ved hvert opphold.", - "unlocked": true - }, - { - "name": "Sen utsjekking når tilgjengelig", - "description": "Vi skjønner – sen utsjekkingen kan til tider være redningen for en reise. På dette punktet i vennskapet vårt, trenger du ikke skynde deg ut av sengen: Vi går god for deg, så sjekk ut én time senere kostnadsfritt, og få de siste, ekstra oppfriskende minuttene med søvn!", - "unlocked": false - }, - { - "name": "Restaurantkupong", - "description": "", - "unlocked": false - }, - { - "name": "Friendsboost", - "description": "", - "unlocked": false - }, - { - "name": "Tidlig innsjekk når tilgjengelig", - "description": "Vil du få et forsprang på oppholdet ditt? Det er ikke noe problem. Sjekk inn én time tidligere kostnadsfritt, og ta snarveien til avslapning og lykke.", - "unlocked": false - }, - { - "name": "Gratis oppgraderinger (når tilgjengelig)", - "description": "La oss ikke gå som katten rundt grøten – vi tar ting opp et nivå. På dette nivået i vennskapet vårt vil vi oppgradere rommet ditt når det er mulig, for et enda mer komfortabelt opphold. Høres ikke det bra ut?", - "unlocked": false - }, - { - "name": "2-for-1 frokost", - "description": "Dette er virkelig et deilig tilbud: Uavhengig om du bor hos oss eller ikke, finn deg en frokostkompis, og dere kan spise for prisen av én! Bare sørg for å sjekke detaljene først, slik at alt er på plass for deres smakfulle sammenkomst.", - "unlocked": false - }, - { - "name": "Romgaranti i 48 timer", - "description": "Hysj – dette er en spesiell godbit, kun for noen få av dere! Så merk deg: Selv om vi er fullbooket, er du garantert et rom så lenge du bestiller 48 timer på forhånd. Ganske utrolig!", - "unlocked": false - }, - { - "name": "Gratis frokost – alltid", - "description": "Har du lyst på en smakfull morgengodbit? Bare kom innom! Nå kan du sparke i gang dagen din med gratis frokost – og gjett hva: Det gjelder uavhengig om du bor hos oss eller ikke.", - "unlocked": false - }, - { - "name": "Årlig fantastisk gave", - "description": "Som vår Best Friend fortjener du absolutt spesialbehandling, så vi har en eksklusiv, årlig, og ganske fantastisk gave klar til deg. Hva det er? Vel, det er en overraskelse. Ingen sniktitting!", - "unlocked": false - }, - { - "name": "Boost for barn", - "description": "På dette vennskapsnivået er barnet ditt også vår venn – og det betyr en spesiell boost-gave for barn når du bor hos oss. Hvorfor? Fordi barn er kule! De fortjener virkelig VIP-behandling.", - "unlocked": false - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requirement": "5 000p", - "description": "Du har vært her mye i det siste! Og ærlig talt føler vi at vi er på bølgelengde – ett behagelig opphold og én morsom overraskelse om gangen.", - "icon": "/_static/icons/loyaltylevels/good-friend.svg", - "benefits": [ - { - "name": "Vennlige priser", - "description": "Å være vennen vår betyr at du alltid får det beste tilbudet: Når som helst, hvor som helst. Det er ikke nødvendig med noe hemmelig håndtrykk eller bestillingskode – bare dykk inn i bestillingene og se selv.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Gratis barne-mocktail under oppholdet", - "description": "Har du barna med deg? Det er fantastisk – hei der, kompis! Vi vil sørge for at ungene føler seg som de VIP-ene de faktisk er, og spanderer en forfriskende mocktail på dem ved hvert opphold.", - "unlocked": true - }, - { - "name": "Sen utsjekking når tilgjengelig", - "description": "Vi skjønner – sen utsjekkingen kan til tider være redningen for en reise. På dette punktet i vennskapet vårt, trenger du ikke skynde deg ut av sengen: Vi går god for deg, så sjekk ut én time senere kostnadsfritt, og få de siste, ekstra oppfriskende minuttene med søvn!", - "unlocked": false - }, - { - "name": "Restaurantkupong", - "description": "", - "unlocked": false - }, - { - "name": "Friendsboost", - "description": "", - "unlocked": false - }, - { - "name": "Tidlig innsjekk når tilgjengelig", - "description": "Vil du få et forsprang på oppholdet ditt? Det er ikke noe problem. Sjekk inn én time tidligere kostnadsfritt, og ta snarveien til avslapning og lykke.", - "unlocked": false - }, - { - "name": "Gratis oppgraderinger (når tilgjengelig)", - "description": "La oss ikke gå som katten rundt grøten – vi tar ting opp et nivå. På dette nivået i vennskapet vårt vil vi oppgradere rommet ditt når det er mulig, for et enda mer komfortabelt opphold. Høres ikke det bra ut?", - "unlocked": false - }, - { - "name": "2-for-1 frokost", - "description": "Dette er virkelig et deilig tilbud: Uavhengig om du bor hos oss eller ikke, finn deg en frokostkompis, og dere kan spise for prisen av én! Bare sørg for å sjekke detaljene først, slik at alt er på plass for deres smakfulle sammenkomst.", - "unlocked": false - }, - { - "name": "Romgaranti i 48 timer", - "description": "Hysj – dette er en spesiell godbit, kun for noen få av dere! Så merk deg: Selv om vi er fullbooket, er du garantert et rom så lenge du bestiller 48 timer på forhånd. Ganske utrolig!", - "unlocked": false - }, - { - "name": "Gratis frokost – alltid", - "description": "Har du lyst på en smakfull morgengodbit? Bare kom innom! Nå kan du sparke i gang dagen din med gratis frokost – og gjett hva: Det gjelder uavhengig om du bor hos oss eller ikke.", - "unlocked": false - }, - { - "name": "Årlig fantastisk gave", - "description": "Som vår Best Friend fortjener du absolutt spesialbehandling, så vi har en eksklusiv, årlig, og ganske fantastisk gave klar til deg. Hva det er? Vel, det er en overraskelse. Ingen sniktitting!", - "unlocked": false - }, - { - "name": "Boost for barn", - "description": "På dette vennskapsnivået er barnet ditt også vår venn – og det betyr en spesiell boost-gave for barn når du bor hos oss. Hvorfor? Fordi barn er kule! De fortjener virkelig VIP-behandling.", - "unlocked": false - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requirement": "10 000p", - "description": "Nå er det seriøst: Vi begynner virkelig å bli kjent med hverandre, noe som gjør Scandic-opplevelsen din mye mer personlig.", - "icon": "/_static/icons/loyaltylevels/close-friend.svg", - "benefits": [ - { - "name": "Vennlige priser", - "description": "Å være vennen vår betyr at du alltid får det beste tilbudet: Når som helst, hvor som helst. Det er ikke nødvendig med noe hemmelig håndtrykk eller bestillingskode – bare dykk inn i bestillingene og se selv.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15 % rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du bor hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Gratis barne-mocktail under oppholdet", - "description": "Har du barna med deg? Det er fantastisk – hei der, kompis! Vi vil sørge for at ungene føler seg som de VIP-ene de faktisk er, og spanderer en forfriskende mocktail på dem ved hvert opphold.", - "unlocked": true - }, - { - "name": "Sen utsjekking når tilgjengelig", - "description": "Vi skjønner – sen utsjekkingen kan til tider være redningen for en reise. På dette punktet i vennskapet vårt, trenger du ikke skynde deg ut av sengen: Vi går god for deg, så sjekk ut én time senere kostnadsfritt, og få de siste, ekstra oppfriskende minuttene med søvn!", - "unlocked": true - }, - { - "name": "Restaurantkupong", - "description": "Så, her er godbiten: For hver vennskapspoenggivende natt du bor hos oss, gir vi deg en restaurantkupong på 50 NOK.", - "unlocked": true, - "value": "50 NOK" - }, - { - "name": "Friendsboost", - "description": "", - "unlocked": false - }, - { - "name": "Tidlig innsjekk når tilgjengelig", - "description": "Vil du få et forsprang på oppholdet ditt? Det er ikke noe problem. Sjekk inn én time tidligere kostnadsfritt, og ta snarveien til avslapning og lykke.", - "unlocked": false - }, - { - "name": "Gratis oppgraderinger (når tilgjengelig)", - "description": "La oss ikke gå som katten rundt grøten – vi tar ting opp et nivå. På dette nivået i vennskapet vårt vil vi oppgradere rommet ditt når det er mulig, for et enda mer komfortabelt opphold. Høres ikke det bra ut?", - "unlocked": false - }, - { - "name": "2-for-1 frokost", - "description": "Dette er virkelig et deilig tilbud: Uavhengig om du bor hos oss eller ikke, finn deg en frokostkompis, og dere kan spise for prisen av én! Bare sørg for å sjekke detaljene først, slik at alt er på plass for deres smakfulle sammenkomst.", - "unlocked": false - }, - { - "name": "Romgaranti i 48 timer", - "description": "Hysj – dette er en spesiell godbit, kun for noen få av dere! Så merk deg: Selv om vi er fullbooket, er du garantert et rom så lenge du bestiller 48 timer på forhånd. Ganske utrolig!", - "unlocked": false - }, - { - "name": "Gratis frokost – alltid", - "description": "Har du lyst på en smakfull morgengodbit? Bare kom innom! Nå kan du sparke i gang dagen din med gratis frokost – og gjett hva: Det gjelder uavhengig om du bor hos oss eller ikke.", - "unlocked": false - }, - { - "name": "Årlig fantastisk gave", - "description": "Som vår Best Friend fortjener du absolutt spesialbehandling, så vi har en eksklusiv, årlig, og ganske fantastisk gave klar til deg. Hva det er? Vel, det er en overraskelse. Ingen sniktitting!", - "unlocked": false - }, - { - "name": "Boost for barn", - "description": "På dette vennskapsnivået er barnet ditt også vår venn – og det betyr en spesiell boost-gave for barn når du bor hos oss. Hvorfor? Fordi barn er kule! De fortjener virkelig VIP-behandling.", - "unlocked": false - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requirement": "25 000p", - "description": "Hurra for oss! Dette ser ut til å gå i retning av venner for livet – og det kommer med tilgang til mye mer av Scandic.", - "icon": "/_static/icons/loyaltylevels/dear-friend.svg", - "benefits": [ - { - "name": "Vennlige priser", - "description": "Å være vennen vår betyr at du alltid får det beste tilbudet: Når som helst, hvor som helst. Det er ikke nødvendig med noe hemmelig håndtrykk eller bestillingskode – bare dykk inn i bestillingene og se selv.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Gratis barne-mocktail under oppholdet", - "description": "Har du barna med deg? Det er fantastisk – hei der, kompis! Vi vil sørge for at ungene føler seg som de VIP-ene de faktisk er, og spanderer en forfriskende mocktail på dem ved hvert opphold.", - "unlocked": true - }, - { - "name": "Sen utsjekking når tilgjengelig", - "description": "Vi skjønner – sen utsjekkingen kan til tider være redningen for en reise. På dette punktet i vennskapet vårt, trenger du ikke skynde deg ut av sengen: Vi går god for deg, så sjekk ut én time senere kostnadsfritt, og få de siste, ekstra oppfriskende minuttene med søvn!", - "unlocked": true - }, - { - "name": "Restaurantkupong", - "description": "Vennskapet vårt ble nettopp enda bedre! Nyt en restaurantkupong på 75 NOK for hver poenggivende natt. Bra, ikke sant?", - "unlocked": true, - "value": "75 NOK" - }, - { - "name": "Friendsboost", - "description": "Her har du noe veldig bra: Hver gang du øker antall vennskapspoeng, får du 25 % ekstra – ekstra på det ekstra! Så, begynn å samle poeng på opphold, måltider og mer, og du vil veldig snart få et gratis opphold.", - "unlocked": true, - "value": "25 %" - }, - { - "name": "Tidlig innsjekk når tilgjengelig", - "description": "Vil du få et forsprang på oppholdet ditt? Det er ikke noe problem. Sjekk inn én time tidligere kostnadsfritt, og ta snarveien til avslapning og lykke.", - "unlocked": true - }, - { - "name": "Gratis oppgraderinger (når tilgjengelig)", - "description": "La oss ikke gå som katten rundt grøten – vi tar ting opp et nivå. På dette nivået i vennskapet vårt vil vi oppgradere rommet ditt når det er mulig, for et enda mer komfortabelt opphold. Høres ikke det bra ut?", - "unlocked": false - }, - { - "name": "2-for-1 frokost", - "description": "Dette er virkelig et deilig tilbud: Uavhengig om du bor hos oss eller ikke, finn deg en frokostkompis, og dere kan spise for prisen av én! Bare sørg for å sjekke detaljene først, slik at alt er på plass for deres smakfulle sammenkomst.", - "unlocked": false - }, - { - "name": "Romgaranti i 48 timer", - "description": "Hysj – dette er en spesiell godbit, kun for noen få av dere! Så merk deg: Selv om vi er fullbooket, er du garantert et rom så lenge du bestiller 48 timer på forhånd. Ganske utrolig!", - "unlocked": false - }, - { - "name": "Gratis frokost – alltid", - "description": "Har du lyst på en smakfull morgengodbit? Bare kom innom! Nå kan du sparke i gang dagen din med gratis frokost – og gjett hva: Det gjelder uavhengig om du bor hos oss eller ikke.", - "unlocked": false - }, - { - "name": "Årlig fantastisk gave", - "description": "Som vår Best Friend fortjener du absolutt spesialbehandling, så vi har en eksklusiv, årlig, og ganske fantastisk gave klar til deg. Hva det er? Vel, det er en overraskelse. Ingen sniktitting!", - "unlocked": false - }, - { - "name": "Boost for barn", - "description": "På dette vennskapsnivået er barnet ditt også vår venn – og det betyr en spesiell boost-gave for barn når du bor hos oss. Hvorfor? Fordi barn er kule! De fortjener virkelig VIP-behandling.", - "unlocked": false - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requirement": "100 000p", - "description": "Du har vært lojal mot oss gjennom opphold, happy hours og treningsøkter – så vi er der for deg med noen av de aller beste fordelene våre.", - "icon": "/_static/icons/loyaltylevels/loyal-friend.svg", - "benefits": [ - { - "name": "Vennlige priser", - "description": "Å være vennen vår betyr at du alltid får det beste tilbudet: Når som helst, hvor som helst. Det er ikke nødvendig med noe hemmelig håndtrykk eller bestillingskode – bare dykk inn i bestillingene og se selv.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Gratis barne-mocktail under oppholdet", - "description": "Har du barna med deg? Det er fantastisk – hei der, kompis! Vi vil sørge for at ungene føler seg som de VIP-ene de faktisk er, og spanderer en forfriskende mocktail på dem ved hvert opphold.", - "unlocked": true - }, - { - "name": "Sen utsjekking når tilgjengelig", - "description": "Vi skjønner – sen utsjekkingen kan til tider være redningen for en reise. På dette punktet i vennskapet vårt, trenger du ikke skynde deg ut av sengen: Vi går god for deg, så sjekk ut én time senere kostnadsfritt, og få de siste, ekstra oppfriskende minuttene med søvn!", - "unlocked": true - }, - { - "name": "Restaurantkupong", - "description": "Vi gir deg en restaurantkupong på 100 NOK for hver vennskapspoenggivende natt du bor. Det kan gi deg et fjell av croissanter! Herlig, hva?", - "unlocked": true, - "value": "100 NOK" - }, - { - "name": "Friendsboost", - "description": "Her har du noe veldig bra: Hver gang du øker antall vennskapspoeng, får du 25 % ekstra – ekstra på det ekstra! Så, begynn å samle poeng på opphold, måltider og mer, og du vil veldig snart få et gratis opphold.", - "unlocked": true, - "value": "25 %" - }, - { - "name": "Tidlig innsjekk når tilgjengelig", - "description": "Vil du få et forsprang på oppholdet ditt? Det er ikke noe problem. Sjekk inn én time tidligere kostnadsfritt, og ta snarveien til avslapning og lykke.", - "unlocked": true - }, - { - "name": "Gratis oppgraderinger (når tilgjengelig)", - "description": "La oss ikke gå som katten rundt grøten – vi tar ting opp et nivå. På dette nivået i vennskapet vårt vil vi oppgradere rommet ditt når det er mulig, for et enda mer komfortabelt opphold. Høres ikke det bra ut?", - "unlocked": true - }, - { - "name": "2-for-1 frokost", - "description": "Dette er virkelig et deilig tilbud: Uavhengig om du bor hos oss eller ikke, finn deg en frokostkompis, og dere kan spise for prisen av én! Bare sørg for å sjekke detaljene først, slik at alt er på plass for deres smakfulle sammenkomst.", - "unlocked": true - }, - { - "name": "Romgaranti i 48 timer", - "description": "Hysj – dette er en spesiell godbit, kun for noen få av dere! Så merk deg: Selv om vi er fullbooket, er du garantert et rom så lenge du bestiller 48 timer på forhånd. Ganske utrolig!", - "unlocked": false - }, - { - "name": "Gratis frokost – alltid", - "description": "Har du lyst på en smakfull morgengodbit? Bare kom innom! Nå kan du sparke i gang dagen din med gratis frokost – og gjett hva: Det gjelder uavhengig om du bor hos oss eller ikke.", - "unlocked": false - }, - { - "name": "Årlig fantastisk gave", - "description": "Som vår Best Friend fortjener du absolutt spesialbehandling, så vi har en eksklusiv, årlig, og ganske fantastisk gave klar til deg. Hva det er? Vel, det er en overraskelse. Ingen sniktitting!", - "unlocked": false - }, - { - "name": "Boost for barn", - "description": "På dette vennskapsnivået er barnet ditt også vår venn – og det betyr en spesiell boost-gave for barn når du bor hos oss. Hvorfor? Fordi barn er kule! De fortjener virkelig VIP-behandling.", - "unlocked": false - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requirement": "250 000p", - "description": "Det spiller ingen rolle om det er høysesong eller lavsesong, du er alltid der for oss. Nyt enda flere skreddersydde fordeler – akkurat slik du liker dem.", - "icon": "/_static/icons/loyaltylevels/true-friend.svg", - "benefits": [ - { - "name": "Vennlige priser", - "description": "Å være vennen vår betyr at du alltid får det beste tilbudet: Når som helst, hvor som helst. Det er ikke nødvendig med noe hemmelig håndtrykk eller bestillingskode – bare dykk inn i bestillingene og se selv.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Gratis barne-mocktail under oppholdet", - "description": "Har du barna med deg? Det er fantastisk – hei der, kompis! Vi vil sørge for at ungene føler seg som de VIP-ene de faktisk er, og spanderer en forfriskende mocktail på dem ved hvert opphold.", - "unlocked": true - }, - { - "name": "Sen utsjekking når tilgjengelig", - "description": "Vi skjønner – sen utsjekkingen kan til tider være redningen for en reise. På dette punktet i vennskapet vårt, trenger du ikke skynde deg ut av sengen: Vi går god for deg, så sjekk ut én time senere kostnadsfritt, og få de siste, ekstra oppfriskende minuttene med søvn!", - "unlocked": true - }, - { - "name": "Restaurantkupong", - "description": "Dette går virkelig flott! Nå vil hver vennskapspoenggivende natt gi deg en restaurantkupong på 150 NOK. Det vil garantert være nyttig for den neste lekre frokosten eller middagsdaten din!", - "unlocked": true, - "value": "150 NOK" - }, - { - "name": "Friendsboost", - "description": "Gled deg! Hver gang du tjener nye Friends-poeng får du 25 % eller 50 % ekstra poeng – som en superboost! Begynn å tjene poeng ved å bo og spise hos oss, og du vil få en bonusnatt før du aner det.", - "unlocked": true, - "value": "50 %" - }, - { - "name": "Tidlig innsjekk når tilgjengelig", - "description": "Vil du få et forsprang på oppholdet ditt? Det er ikke noe problem. Sjekk inn én time tidligere kostnadsfritt, og ta snarveien til avslapning og lykke.", - "unlocked": true - }, - { - "name": "Gratis oppgraderinger (når tilgjengelig)", - "description": "La oss ikke gå som katten rundt grøten – vi tar ting opp et nivå. På dette nivået i vennskapet vårt vil vi oppgradere rommet ditt når det er mulig, for et enda mer komfortabelt opphold. Høres ikke det bra ut?", - "unlocked": true - }, - { - "name": "2-for-1 frokost", - "description": "Dette er virkelig et deilig tilbud: Uavhengig om du bor hos oss eller ikke, finn deg en frokostkompis, og dere kan spise for prisen av én! Bare sørg for å sjekke detaljene først, slik at alt er på plass for deres smakfulle sammenkomst.", - "unlocked": false - }, - { - "name": "Romgaranti i 48 timer", - "description": "Hysj – dette er en spesiell godbit, kun for noen få av dere! Så merk deg: Selv om vi er fullbooket, er du garantert et rom så lenge du bestiller 48 timer på forhånd. Ganske utrolig!", - "unlocked": true - }, - { - "name": "Gratis frokost – alltid", - "description": "Har du lyst på en smakfull morgengodbit? Bare kom innom! Nå kan du sparke i gang dagen din med gratis frokost – og gjett hva: Det gjelder uavhengig om du bor hos oss eller ikke.", - "unlocked": true - }, - { - "name": "Årlig fantastisk gave", - "description": "Som vår Best Friend fortjener du absolutt spesialbehandling, så vi har en eksklusiv, årlig, og ganske fantastisk gave klar til deg. Hva det er? Vel, det er en overraskelse. Ingen sniktitting!", - "unlocked": false - }, - { - "name": "Boost for barn", - "description": "På dette vennskapsnivået er barnet ditt også vår venn – og det betyr en spesiell boost-gave for barn når du bor hos oss. Hvorfor? Fordi barn er kule! De fortjener virkelig VIP-behandling.", - "unlocked": false - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requirement": "400 000p eller 100 netter", - "description": "Det finnes ikke ord for et bånd som dette, men vi gjør et forsøk allikevel: Det blir bare ikke bedre når det gjelder svært eksklusive opplevelser!", - "icon": "/_static/icons/loyaltylevels/best-friend.svg", - "benefits": [ - { - "name": "Vennlige priser", - "description": "Å være vennen vår betyr at du alltid får det beste tilbudet: Når som helst, hvor som helst. Det er ikke nødvendig med noe hemmelig håndtrykk eller bestillingskode – bare dykk inn i bestillingene og se selv.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.", - "unlocked": true, - "value": "15 %" - }, - { - "name": "Gratis barne-mocktail under oppholdet", - "description": "Har du barna med deg? Det er fantastisk – hei der, kompis! Vi vil sørge for at ungene føler seg som de VIP-ene de faktisk er, og spanderer en forfriskende mocktail på dem ved hvert opphold.", - "unlocked": true - }, - { - "name": "Sen utsjekking når tilgjengelig", - "description": "Vi skjønner – sen utsjekkingen kan til tider være redningen for en reise. På dette punktet i vennskapet vårt, trenger du ikke skynde deg ut av sengen: Vi går god for deg, så sjekk ut én time senere kostnadsfritt, og få de siste, ekstra oppfriskende minuttene med søvn!", - "unlocked": true - }, - { - "name": "Restaurantkupong", - "description": "Vi gir deg en restaurantkupong på 200 NOK for hver vennskapspoenggivende natt du bor. Det kan gi deg et fjell av croissanter! Herlig, hva?", - "unlocked": true, - "value": "200 NOK" - }, - { - "name": "Friendsboost", - "description": "Gled deg! Hver gang du tjener nye Friends-poeng får du 25 % eller 50 % ekstra poeng – som en superboost! Begynn å tjene poeng ved å bo og spise hos oss, og du vil få en bonusnatt før du aner det.", - "unlocked": true, - "value": "50 %" - }, - { - "name": "Tidlig innsjekk når tilgjengelig", - "description": "Vil du få et forsprang på oppholdet ditt? Det er ikke noe problem. Sjekk inn én time tidligere kostnadsfritt, og ta snarveien til avslapning og lykke.", - "unlocked": true - }, - { - "name": "Gratis oppgraderinger (når tilgjengelig)", - "description": "La oss ikke gå som katten rundt grøten – vi tar ting opp et nivå. På dette nivået i vennskapet vårt vil vi oppgradere rommet ditt når det er mulig, for et enda mer komfortabelt opphold. Høres ikke det bra ut?", - "unlocked": true - }, - { - "name": "2-for-1 frokost", - "description": "Dette er virkelig et deilig tilbud: Uavhengig om du bor hos oss eller ikke, finn deg en frokostkompis, og dere kan spise for prisen av én! Bare sørg for å sjekke detaljene først, slik at alt er på plass for deres smakfulle sammenkomst.", - "unlocked": false - }, - { - "name": "Romgaranti i 48 timer", - "description": "Hysj – dette er en spesiell godbit, kun for noen få av dere! Så merk deg: Selv om vi er fullbooket, er du garantert et rom så lenge du bestiller 48 timer på forhånd. Ganske utrolig!", - "unlocked": true - }, - { - "name": "Gratis frokost – alltid", - "description": "Har du lyst på en smakfull morgengodbit? Bare kom innom! Nå kan du sparke i gang dagen din med gratis frokost – og gjett hva: Det gjelder uavhengig om du bor hos oss eller ikke.", - "unlocked": true - }, - { - "name": "Årlig fantastisk gave", - "description": "Som vår Best Friend fortjener du absolutt spesialbehandling, så vi har en eksklusiv, årlig, og ganske fantastisk gave klar til deg. Hva det er? Vel, det er en overraskelse. Ingen sniktitting!", - "unlocked": true - }, - { - "name": "Boost for barn", - "description": "På dette vennskapsnivået er barnet ditt også vår venn – og det betyr en spesiell boost-gave for barn når du bor hos oss. Hvorfor? Fordi barn er kule! De fortjener virkelig VIP-behandling.", - "unlocked": true - } - ] - } - ] -} diff --git a/components/Blocks/DynamicContent/OverviewTable/data/SV.json b/components/Blocks/DynamicContent/OverviewTable/data/SV.json deleted file mode 100644 index 8468d58c7..000000000 --- a/components/Blocks/DynamicContent/OverviewTable/data/SV.json +++ /dev/null @@ -1,538 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requirement": "0p", - "description": "Det är nu den börjar, vår härliga resa tillsammans. Som en New Friend kommer du att upptäcka oss om och om igen under dina äventyr med Scandic.", - "icon": "/_static/icons/loyaltylevels/new-friend.svg", - "benefits": [ - { - "name": "Friendspriser på rum", - "description": "Som vår vän får du alltid det bästa erbjudandet: oavsett tid och plats. Inga hemliga deals eller bokningskoder behövs – det är bara att boka.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Mums! Prova vår välsmakande 10 % rabatt i våra restauranger och hotellshoppar på helgerna, oavsett om du övernattar eller bara tittar förbi på en bit mat. Så passa på att unna dig det lilla extra.", - "unlocked": true, - "value": "10%" - }, - { - "name": "Fri mocktail för barn under vistelse", - "description": "Har du barnen med dig på resan? Fantastiskt – hallå där, kompis! Hos oss är barn värdefulla små VIP-gäster, så därför bjuder vi på en läskande och god mocktail vid ankomst.", - "unlocked": true - }, - { - "name": "Sen utcheckning om möjligt", - "description": "Vi fattar, ibland kan man behöva en sovmorgon. Som de vänner vi är låter vi dig checka ut en timme senare utan kostnad. Lite extra tid kan sätta hela känslan för resten av dagen.", - "unlocked": false - }, - { - "name": "Restaurangkupong", - "description": "Finns det nåt bättre än en bra rabatt? Som vår vän får du rabatt i våra restauranger och hotellshoppen på helger och utvalda helgdagar, oavsett om du övernattar eller inte. Så, passa att unna dig det lilla extra.", - "unlocked": false - }, - { - "name": "Friendsboost", - "description": "Sweet! Varje gång du tjänar nya friendspoäng bjuder vi dig på 25-50 % extra poäng – som en superboost! Börja samla poäng genom att bo och äta så har du kirrat en bonusnatt på nolltid.", - "unlocked": false - }, - { - "name": "Tidig incheckning om möjligt", - "description": "Vill du ha ditt rum lite i förväg? Inga problem. Checka in en timme tidigare utan kostnad och få en timmes extra avkoppling.", - "unlocked": false - }, - { - "name": "Fri uppgradering om möjligt", - "description": "Vi steppar upp saker ett snäpp. Vår vänskap har nått nivån där vi kommer uppgradera ditt rum när det är möjligt, så du kan få en ännu skönare vistelse. Bra, eller hur?", - "unlocked": false - }, - { - "name": "Frukost 2 för 1", - "description": "Okej, nu snackar vi. Oavsett om du bor hos oss eller inte, ta med en frukostkompis eftersom du äter två frukostar till priset av en! Kom bara ihåg att dubbelkolla detaljerna först så att allt är i sin ordning för en maxad frukost.", - "unlocked": false - }, - { - "name": "48 timmars rumsgaranti", - "description": "Schhh – en hemlis för våra närmaste vänner: vi garanterar dig ett rum, även om vi är fullbokade, så länge du gör bokningen minst 48 timmar i förväg. Bra va?", - "unlocked": false - }, - { - "name": "Alltid fri frukost", - "description": "Kickstarta dagen med en kostnadsfri frukost hos oss – och du, det gäller oavsett om du bor hos oss eller inte. Det är bara att komma förbi!", - "unlocked": false - }, - { - "name": "Årlig spännande present", - "description": "Som Best Friend förtjänar du det allra bästa. Därför ska du få en spännande present varje år, men vad det blir är en överraskning. Tjuvkika inte!", - "unlocked": false - }, - { - "name": "Boost för barn", - "description": "Vid det här laget räknar vi även dina barn som våra vänner, vilket betyder att ni får en speciell överraskning när ni bor hos oss. Lite VIP-behandling. För att barn är coola.", - "unlocked": false - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requirement": "5 000p", - "description": "Vi har fått se ganska mycket av varandra senaste tiden! Helt ärligt, det känns som att vi har nåt på gång, men vi tar det i vår takt, en vistelse i taget.", - "icon": "/_static/icons/loyaltylevels/good-friend.svg", - "benefits": [ - { - "name": "Friendspriser på rum", - "description": "Som vår vän får du alltid det bästa erbjudandet: oavsett tid och plats. Inga hemliga deals eller bokningskoder behövs – det är bara att boka.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Mums! Prova vår välsmakande 10-15% rabatt i våra restauranger och hotellshoppar på helgerna, oavsett om du övernattar eller bara tittar förbi på en bit mat. Så passa på att unna dig det lilla extra.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Fri mocktail för barn under vistelse", - "description": "Har du barnen med dig på resan? Fantastiskt – hallå där, kompis! Hos oss är barn värdefulla små VIP-gäster, så därför bjuder vi på en läskande och god mocktail vid ankomst.", - "unlocked": true - }, - { - "name": "Sen utcheckning om möjligt", - "description": "Vi fattar, ibland kan man behöva en sovmorgon. Som de vänner vi är låter vi dig checka ut en timme senare utan kostnad. Lite extra tid kan sätta hela känslan för resten av dagen.", - "unlocked": false - }, - { - "name": "Restaurangkupong", - "description": "Finns det nåt bättre än en bra rabatt? Som vår vän får du rabatt i våra restauranger och hotellshoppen på helger och utvalda helgdagar, oavsett om du övernattar eller inte. Så, passa att unna dig det lilla extra.", - "unlocked": false - }, - { - "name": "Friendsboost", - "description": "Sweet! Varje gång du tjänar nya friendspoäng bjuder vi dig på 25-50 % extra poäng – som en superboost! Börja samla poäng genom att bo och äta så har du kirrat en bonusnatt på nolltid.", - "unlocked": false - }, - { - "name": "Tidig incheckning om möjligt", - "description": "Vill du ha ditt rum lite i förväg? Inga problem. Checka in en timme tidigare utan kostnad och få en timmes extra avkoppling.", - "unlocked": false - }, - { - "name": "Fri uppgradering om möjligt", - "description": "Vi steppar upp saker ett snäpp. Vår vänskap har nått nivån där vi kommer uppgradera ditt rum när det är möjligt, så du kan få en ännu skönare vistelse. Bra, eller hur?", - "unlocked": false - }, - { - "name": "Frukost 2 för 1", - "description": "Okej, nu snackar vi. Oavsett om du bor hos oss eller inte, ta med en frukostkompis eftersom du äter två frukostar till priset av en! Kom bara ihåg att dubbelkolla detaljerna först så att allt är i sin ordning för en maxad frukost.", - "unlocked": false - }, - { - "name": "48 timmars rumsgaranti", - "description": "Schhh – en hemlis för våra närmaste vänner: vi garanterar dig ett rum, även om vi är fullbokade, så länge du gör bokningen minst 48 timmar i förväg. Bra va?", - "unlocked": false - }, - { - "name": "Alltid fri frukost", - "description": "Kickstarta dagen med en kostnadsfri frukost hos oss – och du, det gäller oavsett om du bor hos oss eller inte. Det är bara att komma förbi!", - "unlocked": false - }, - { - "name": "Årlig spännande present", - "description": "Som Best Friend förtjänar du det allra bästa. Därför ska du få en spännande present varje år, men vad det blir är en överraskning. Tjuvkika inte!", - "unlocked": false - }, - { - "name": "Boost för barn", - "description": "Vid det här laget räknar vi även dina barn som våra vänner, vilket betyder att ni får en speciell överraskning när ni bor hos oss. Lite VIP-behandling. För att barn är coola.", - "unlocked": false - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requirement": "10 000p", - "description": "Det börjar bli seriöst nu: vi har verkligen lärt känna varann. Därför tycker vi att det är dags att din upplevelse hos oss blir ännu mer personlig.", - "icon": "/_static/icons/loyaltylevels/close-friend.svg", - "benefits": [ - { - "name": "Friendspriser på rum", - "description": "Som vår vän får du alltid det bästa erbjudandet: oavsett tid och plats. Inga hemliga deals eller bokningskoder behövs – det är bara att boka.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Mums! Prova vår välsmakande 10-15% rabatt i våra restauranger och hotellshoppar på helgerna, oavsett om du övernattar eller bara tittar förbi på en bit mat. Så passa på att unna dig det lilla extra.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Fri mocktail för barn under vistelse", - "description": "Har du barnen med dig på resan? Fantastiskt – hallå där, kompis! Hos oss är barn värdefulla små VIP-gäster, så därför bjuder vi på en läskande och god mocktail vid ankomst.", - "unlocked": true - }, - { - "name": "Sen utcheckning om möjligt", - "description": "Vi fattar, ibland kan man behöva en sovmorgon. Som de vänner vi är låter vi dig checka ut en timme senare utan kostnad. Lite extra tid kan sätta hela känslan för resten av dagen.", - "unlocked": true - }, - { - "name": "Restaurangkupong", - "description": "Okej, såhär: för varje natt som ger friendspoäng bjuder vi även på en restaurangkupong värd 50 kr.", - "unlocked": true, - "value": "50 kr" - }, - { - "name": "Friendsboost", - "description": "", - "unlocked": false - }, - { - "name": "Tidig incheckning om möjligt", - "description": "Vill du ha ditt rum lite i förväg? Inga problem. Checka in en timme tidigare utan kostnad och få en timmes extra avkoppling.", - "unlocked": false - }, - { - "name": "Fri uppgradering om möjligt", - "description": "Vi steppar upp saker ett snäpp. Vår vänskap har nått nivån där vi kommer uppgradera ditt rum när det är möjligt, så du kan få en ännu skönare vistelse. Bra, eller hur?", - "unlocked": false - }, - { - "name": "Frukost 2 för 1", - "description": "Okej, nu snackar vi. Oavsett om du bor hos oss eller inte, ta med en frukostkompis eftersom du äter två frukostar till priset av en! Kom bara ihåg att dubbelkolla detaljerna först så att allt är i sin ordning för en maxad frukost.", - "unlocked": false - }, - { - "name": "48 timmars rumsgaranti", - "description": "Schhh – en hemlis för våra närmaste vänner: vi garanterar dig ett rum, även om vi är fullbokade, så länge du gör bokningen minst 48 timmar i förväg. Bra va?", - "unlocked": false - }, - { - "name": "Alltid fri frukost", - "description": "Kickstarta dagen med en kostnadsfri frukost hos oss – och du, det gäller oavsett om du bor hos oss eller inte. Det är bara att komma förbi!", - "unlocked": false - }, - { - "name": "Årlig spännande present", - "description": "Som Best Friend förtjänar du det allra bästa. Därför ska du få en spännande present varje år, men vad det blir är en överraskning. Tjuvkika inte!", - "unlocked": false - }, - { - "name": "Boost för barn", - "description": "Vid det här laget räknar vi även dina barn som våra vänner, vilket betyder att ni får en speciell överraskning när ni bor hos oss. Lite VIP-behandling. För att barn är coola.", - "unlocked": false - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requirement": "25 000p", - "description": "Skål för oss! Vi känner tydliga vänner för livet-vibbar i luften och det öppnar upp för ännu fler Scandicfördelar.", - "icon": "/_static/icons/loyaltylevels/dear-friend.svg", - "benefits": [ - { - "name": "Friendspriser på rum", - "description": "Som vår vän får du alltid det bästa erbjudandet: oavsett tid och plats. Inga hemliga deals eller bokningskoder behövs – det är bara att boka.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Mums! Prova vår välsmakande 10-15% rabatt i våra restauranger och hotellshoppar på helgerna, oavsett om du övernattar eller bara tittar förbi på en bit mat. Så passa på att unna dig det lilla extra.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Fri mocktail för barn under vistelse", - "description": "Har du barnen med dig på resan? Fantastiskt – hallå där, kompis! Hos oss är barn värdefulla små VIP-gäster, så därför bjuder vi på en läskande och god mocktail vid ankomst.", - "unlocked": true - }, - { - "name": "Sen utcheckning om möjligt", - "description": "Vi fattar, ibland kan man behöva en sovmorgon. Som de vänner vi är låter vi dig checka ut en timme senare utan kostnad. Lite extra tid kan sätta hela känslan för resten av dagen.", - "unlocked": true - }, - { - "name": "Restaurangkupong", - "description": "Vår vänskap har vuxit sig starkare. Njut av en restaurangkupong värd 75 kr för varje natt som ger friendspoäng.", - "unlocked": true, - "value": "75 kr" - }, - { - "name": "Friendsboost", - "description": "Sweet! Varje gång du tjänar nya friendspoäng bjuder vi dig på 25-50 % extra poäng – som en superboost! Börja samla poäng genom att bo och äta så har du kirrat en bonusnatt på nolltid.", - "unlocked": true, - "value": "25%" - }, - { - "name": "Tidig incheckning om möjligt", - "description": "Vill du ha ditt rum lite i förväg? Inga problem. Checka in en timme tidigare utan kostnad och få en timmes extra avkoppling.", - "unlocked": true - }, - { - "name": "Fri uppgradering om möjligt", - "description": "Vi steppar upp saker ett snäpp. Vår vänskap har nått nivån där vi kommer uppgradera ditt rum när det är möjligt, så du kan få en ännu skönare vistelse. Bra, eller hur?", - "unlocked": false - }, - { - "name": "Frukost 2 för 1", - "description": "Okej, nu snackar vi. Oavsett om du bor hos oss eller inte, ta med en frukostkompis eftersom du äter två frukostar till priset av en! Kom bara ihåg att dubbelkolla detaljerna först så att allt är i sin ordning för en maxad frukost.", - "unlocked": false - }, - { - "name": "48 timmars rumsgaranti", - "description": "Schhh – en hemlis för våra närmaste vänner: vi garanterar dig ett rum, även om vi är fullbokade, så länge du gör bokningen minst 48 timmar i förväg. Bra va?", - "unlocked": false - }, - { - "name": "Alltid fri frukost", - "description": "Kickstarta dagen med en kostnadsfri frukost hos oss – och du, det gäller oavsett om du bor hos oss eller inte. Det är bara att komma förbi!", - "unlocked": false - }, - { - "name": "Årlig spännande present", - "description": "Som Best Friend förtjänar du det allra bästa. Därför ska du få en spännande present varje år, men vad det blir är en överraskning. Tjuvkika inte!", - "unlocked": false - }, - { - "name": "Boost för barn", - "description": "Vid det här laget räknar vi även dina barn som våra vänner, vilket betyder att ni får en speciell överraskning när ni bor hos oss. Lite VIP-behandling. För att barn är coola.", - "unlocked": false - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requirement": "100 000p", - "description": "Du har bott hos oss otaliga nätter, hängt med oss sena timmar och genom ur och skur. Det är dags att du får några av våra allra bästa förmåner.", - "icon": "/_static/icons/loyaltylevels/loyal-friend.svg", - "benefits": [ - { - "name": "Friendspriser på rum", - "description": "Som vår vän får du alltid det bästa erbjudandet: oavsett tid och plats. Inga hemliga deals eller bokningskoder behövs – det är bara att boka.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Mums! Prova vår välsmakande 10-15% rabatt i våra restauranger och hotellshoppar på helgerna, oavsett om du övernattar eller bara tittar förbi på en bit mat. Så passa på att unna dig det lilla extra.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Fri mocktail för barn under vistelse", - "description": "Har du barnen med dig på resan? Fantastiskt – hallå där, kompis! Hos oss är barn värdefulla små VIP-gäster, så därför bjuder vi på en läskande och god mocktail vid ankomst.", - "unlocked": true - }, - { - "name": "Sen utcheckning om möjligt", - "description": "Vi fattar, ibland kan man behöva en sovmorgon. Som de vänner vi är låter vi dig checka ut en timme senare utan kostnad. Lite extra tid kan sätta hela känslan för resten av dagen.", - "unlocked": true - }, - { - "name": "Restaurangkupong", - "description": "Vi bjuder dig på en restaurangkupong värd 100 kr för varje natt som ger friendsspoäng. Det motsvarar ett mindre berg av croissanter!", - "unlocked": true, - "value": "100 kr" - }, - { - "name": "Friendsboost", - "description": "Sweet! Varje gång du tjänar nya friendspoäng bjuder vi dig på 25-50 % extra poäng – som en superboost! Börja samla poäng genom att bo och äta så har du kirrat en bonusnatt på nolltid.", - "unlocked": true, - "value": "25%" - }, - { - "name": "Tidig incheckning om möjligt", - "description": "Vill du ha ditt rum lite i förväg? Inga problem. Checka in en timme tidigare utan kostnad och få en timmes extra avkoppling.", - "unlocked": true - }, - { - "name": "Fri uppgradering om möjligt", - "description": "Vi steppar upp saker ett snäpp. Vår vänskap har nått nivån där vi kommer uppgradera ditt rum när det är möjligt, så du kan få en ännu skönare vistelse. Bra, eller hur?", - "unlocked": true - }, - { - "name": "Frukost 2 för 1", - "description": "Okej, nu snackar vi. Oavsett om du bor hos oss eller inte, ta med en frukostkompis eftersom du äter två frukostar till priset av en! Kom bara ihåg att dubbelkolla detaljerna först så att allt är i sin ordning för en maxad frukost.", - "unlocked": true - }, - { - "name": "48 timmars rumsgaranti", - "description": "Schhh – en hemlis för våra närmaste vänner: vi garanterar dig ett rum, även om vi är fullbokade, så länge du gör bokningen minst 48 timmar i förväg. Bra va?", - "unlocked": false - }, - { - "name": "Alltid fri frukost", - "description": "Kickstarta dagen med en kostnadsfri frukost hos oss – och du, det gäller oavsett om du bor hos oss eller inte. Det är bara att komma förbi!", - "unlocked": false - }, - { - "name": "Årlig spännande present", - "description": "Som Best Friend förtjänar du det allra bästa. Därför ska du få en spännande present varje år, men vad det blir är en överraskning. Tjuvkika inte!", - "unlocked": false - }, - { - "name": "Boost för barn", - "description": "Vid det här laget räknar vi även dina barn som våra vänner, vilket betyder att ni får en speciell överraskning när ni bor hos oss. Lite VIP-behandling. För att barn är coola.", - "unlocked": false - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requirement": "250 000p", - "description": "Dig kan man lita på, oavsett säsong finns du där för oss. Därför vill vi att du ska få njuta av fler förmåner anpassade efter dig.", - "icon": "/_static/icons/loyaltylevels/true-friend.svg", - "benefits": [ - { - "name": "Friendspriser på rum", - "description": "Som vår vän får du alltid det bästa erbjudandet: oavsett tid och plats. Inga hemliga deals eller bokningskoder behövs – det är bara att boka.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Mums! Prova vår välsmakande 10-15% rabatt i våra restauranger och hotellshoppar på helgerna, oavsett om du övernattar eller bara tittar förbi på en bit mat. Så passa på att unna dig det lilla extra.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Fri mocktail för barn under vistelse", - "description": "Har du barnen med dig på resan? Fantastiskt – hallå där, kompis! Hos oss är barn värdefulla små VIP-gäster, så därför bjuder vi på en läskande och god mocktail vid ankomst.", - "unlocked": true - }, - { - "name": "Sen utcheckning om möjligt", - "description": "Vi fattar, ibland kan man behöva en sovmorgon. Som de vänner vi är låter vi dig checka ut en timme senare utan kostnad. Lite extra tid kan sätta hela känslan för resten av dagen.", - "unlocked": true - }, - { - "name": "Restaurangkupong", - "description": "Det blir bättre och bättre! Nu får du en restaurangkupong värd 150 kr för varje natt som ge friendsspoäng. Perfekt när du känner för en lyxig frukost eller ska på middagsdate!", - "unlocked": true, - "value": "150 kr" - }, - { - "name": "Friendsboost", - "description": "Sweet! Varje gång du tjänar nya friendspoäng bjuder vi dig på 25-50 % extra poäng – som en superboost! Börja samla poäng genom att bo och äta så har du kirrat en bonusnatt på nolltid.", - "unlocked": true, - "value": "50%" - }, - { - "name": "Tidig incheckning om möjligt", - "description": "Vill du ha ditt rum lite i förväg? Inga problem. Checka in en timme tidigare utan kostnad och få en timmes extra avkoppling.", - "unlocked": true - }, - { - "name": "Fri uppgradering om möjligt", - "description": "Vi steppar upp saker ett snäpp. Vår vänskap har nått nivån där vi kommer uppgradera ditt rum när det är möjligt, så du kan få en ännu skönare vistelse. Bra, eller hur?", - "unlocked": true - }, - { - "name": "Frukost 2 för 1", - "description": "Okej, nu snackar vi. Oavsett om du bor hos oss eller inte, ta med en frukostkompis eftersom du äter två frukostar till priset av en! Kom bara ihåg att dubbelkolla detaljerna först så att allt är i sin ordning för en maxad frukost.", - "unlocked": false - }, - { - "name": "48 timmars rumsgaranti", - "description": "Schhh – en hemlis för våra närmaste vänner: vi garanterar dig ett rum, även om vi är fullbokade, så länge du gör bokningen minst 48 timmar i förväg. Bra va?", - "unlocked": true - }, - { - "name": "Alltid fri frukost", - "description": "Kickstarta dagen med en kostnadsfri frukost hos oss – och du, det gäller oavsett om du bor hos oss eller inte. Det är bara att komma förbi!", - "unlocked": true - }, - { - "name": "Årlig spännande present", - "description": "Som Best Friend förtjänar du det allra bästa. Därför ska du få en spännande present varje år, men vad det blir är en överraskning. Tjuvkika inte!", - "unlocked": false - }, - { - "name": "Boost för barn", - "description": "Vid det här laget räknar vi även dina barn som våra vänner, vilket betyder att ni får en speciell överraskning när ni bor hos oss. Lite VIP-behandling. För att barn är coola.", - "unlocked": false - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requirement": "400 000p eller 100 nätter", - "description": "Det finns inga ord för en vänskap som vår, men vi vill ändå säga: Bättre upplevelser än så här går inte att få, nu vi talar om exklusiva upplevelser!", - "icon": "/_static/icons/loyaltylevels/best-friend.svg", - "benefits": [ - { - "name": "Friendspriser på rum", - "description": "Som vår vän får du alltid det bästa erbjudandet: oavsett tid och plats. Inga hemliga deals eller bokningskoder behövs – det är bara att boka.", - "unlocked": true - }, - { - "name": "Rabatt på mat", - "description": "Mums! Prova vår välsmakande 10-15 % rabatt i våra restauranger och hotellshoppar på helgerna, oavsett om du övernattar eller bara tittar förbi på en bit mat. Så passa på att unna dig det lilla extra.", - "unlocked": true, - "value": "15%" - }, - { - "name": "Fri mocktail för barn under vistelse", - "description": "Har du barnen med dig på resan? Fantastiskt – hallå där, kompis! Hos oss är barn värdefulla små VIP-gäster, så därför bjuder vi på en läskande och god mocktail vid ankomst.", - "unlocked": true - }, - { - "name": "Sen utcheckning om möjligt", - "description": "Vi fattar, ibland kan man behöva en sovmorgon. Som de vänner vi är låter vi dig checka ut en timme senare utan kostnad. Lite extra tid kan sätta hela känslan för resten av dagen.", - "unlocked": true - }, - { - "name": "Restaurangkupong", - "description": "Det blir bättre och bättre! Nu får du en restaurangkupong för varje natt som ger friendsspoäng. Perfekt när du känner för en lyxig frukost eller ska på middagsdate!", - "unlocked": true, - "value": "200 kr" - }, - { - "name": "Friendsboost", - "description": "Sweet! Varje gång du tjänar nya friendspoäng bjuder vi dig på 25-50 % extra poäng – som en superboost! Börja samla poäng genom att bo och äta så har du kirrat en bonusnatt på nolltid.", - "unlocked": true, - "value": "50%" - }, - { - "name": "Tidig incheckning om möjligt", - "description": "Vill du ha ditt rum lite i förväg? Inga problem. Checka in en timme tidigare utan kostnad och få en timmes extra avkoppling.", - "unlocked": true - }, - { - "name": "Fri uppgradering om möjligt", - "description": "Vi steppar upp saker ett snäpp. Vår vänskap har nått nivån där vi kommer uppgradera ditt rum när det är möjligt, så du kan få en ännu skönare vistelse. Bra, eller hur?", - "unlocked": true - }, - { - "name": "Frukost 2 för 1", - "description": "Okej, nu snackar vi. Oavsett om du bor hos oss eller inte, ta med en frukostkompis eftersom du äter två frukostar till priset av en! Kom bara ihåg att dubbelkolla detaljerna först så att allt är i sin ordning för en maxad frukost.", - "unlocked": false - }, - { - "name": "48 timmars rumsgaranti", - "description": "Schhh – en hemlis för våra närmaste vänner: vi garanterar dig ett rum, även om vi är fullbokade, så länge du gör bokningen minst 48 timmar i förväg. Bra va?", - "unlocked": true - }, - { - "name": "Alltid fri frukost", - "description": "Kickstarta dagen med en kostnadsfri frukost hos oss – och du, det gäller oavsett om du bor hos oss eller inte. Det är bara att komma förbi!", - "unlocked": true - }, - { - "name": "Årlig spännande present", - "description": "Som Best Friend förtjänar du det allra bästa. Därför ska du få en spännande present varje år, men vad det blir är en överraskning. Tjuvkika inte!", - "unlocked": true - }, - { - "name": "Boost för barn", - "description": "Vid det här laget räknar vi även dina barn som våra vänner, vilket betyder att ni får en speciell överraskning när ni bor hos oss. Lite VIP-behandling. För att barn är coola.", - "unlocked": true - } - ] - } - ] -} diff --git a/components/Blocks/DynamicContent/OverviewTable/index.tsx b/components/Blocks/DynamicContent/OverviewTable/index.tsx index a15135855..866a0c7d6 100644 --- a/components/Blocks/DynamicContent/OverviewTable/index.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/index.tsx @@ -3,16 +3,20 @@ import { serverClient } from "@/lib/trpc/server" import SectionWrapper from "../SectionWrapper" import OverviewTableClient from "./Client" -import type { OverviewTableProps } from "@/types/components/blocks/dynamicContent" +import { OverviewTableProps } from "@/types/components/blocks/dynamicContent" export default async function OverviewTable({ dynamic_content, firstItem, }: OverviewTableProps) { + const levels = await serverClient().contentstack.rewards.all() const membershipLevel = await serverClient().user.safeMembershipLevel() return ( - + ) } diff --git a/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx b/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx index f062c5a30..4e0f76291 100644 --- a/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx +++ b/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx @@ -1,7 +1,7 @@ import { MembershipLevelEnum } from "@/constants/membershipLevels" +import { serverClient } from "@/lib/trpc/server" import { getIntl } from "@/i18n" -import { getMembershipLevelObject } from "@/utils/membershipLevel" import { getMembership } from "@/utils/user" import PointsContainer from "../../../Overview/Stats/Points/Container" @@ -20,11 +20,12 @@ export default async function Points({ user, lang }: UserProps & LangParams) { const { formatMessage } = await getIntl() const membership = getMembership(user.memberships) - const nextLevel = getMembershipLevelObject( - membership?.nextLevel as MembershipLevelEnum, - lang - ) - + if (!membership?.nextLevel) { + return null + } + const nextLevel = await serverClient().contentstack.loyaltyLevels.byLevel({ + level: MembershipLevelEnum[membership.nextLevel], + }) return ( diff --git a/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx b/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx new file mode 100644 index 000000000..471a39ede --- /dev/null +++ b/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx @@ -0,0 +1,70 @@ +"use client" + +import { trpc } from "@/lib/trpc/client" +import { Reward } from "@/server/routers/contentstack/reward/output" + +import LoadingSpinner from "@/components/LoadingSpinner" +import Grids from "@/components/TempDesignSystem/Grids" +import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" +import Title from "@/components/TempDesignSystem/Text/Title" +import useLang from "@/hooks/useLang" + +import styles from "./current.module.css" + +type CurrentRewardsClientProps = { + initialCurrentRewards: { rewards: Reward[]; nextCursor: number | undefined } +} +export default function ClientCurrentRewards({ + initialCurrentRewards, +}: CurrentRewardsClientProps) { + const lang = useLang() + const { data, isFetching, fetchNextPage, hasNextPage, isLoading } = + trpc.contentstack.rewards.current.useInfiniteQuery( + { + limit: 3, + lang, + }, + { + getNextPageParam: (lastPage) => lastPage?.nextCursor, + initialData: { + pageParams: [undefined, 1], + pages: [initialCurrentRewards], + }, + } + ) + function loadMoreData() { + if (hasNextPage) { + fetchNextPage() + } + } + const filteredRewards = + data?.pages.filter((page) => page && page.rewards) ?? [] + const rewards = filteredRewards.flatMap((page) => page?.rewards) as Reward[] + return isLoading ? ( + + ) : rewards.length ? ( +
+ + {rewards.map((reward, idx) => ( +
+ + {reward.label} + +
+ ))} +
+ {hasNextPage ? ( + isFetching ? ( + + ) : ( + + ) + ) : null} +
+ ) : null +} diff --git a/components/Blocks/DynamicContent/Benefits/CurrentLevel/current.module.css b/components/Blocks/DynamicContent/Rewards/CurrentLevel/current.module.css similarity index 100% rename from components/Blocks/DynamicContent/Benefits/CurrentLevel/current.module.css rename to components/Blocks/DynamicContent/Rewards/CurrentLevel/current.module.css diff --git a/components/Blocks/DynamicContent/Rewards/CurrentLevel/index.tsx b/components/Blocks/DynamicContent/Rewards/CurrentLevel/index.tsx new file mode 100644 index 000000000..cb970e5e7 --- /dev/null +++ b/components/Blocks/DynamicContent/Rewards/CurrentLevel/index.tsx @@ -0,0 +1,32 @@ +import { serverClient } from "@/lib/trpc/server" + +import SectionContainer from "@/components/Section/Container" +import SectionHeader from "@/components/Section/Header" +import SectionLink from "@/components/Section/Link" + +import ClientCurrentRewards from "./Client" + +import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage" + +export default async function CurrentRewardsBlock({ + title, + subtitle, + link, +}: AccountPageComponentProps) { + const initialCurrentRewards = + await serverClient().contentstack.rewards.current({ + limit: 3, + }) + + if (!initialCurrentRewards) { + return null + } + + return ( + + + + + + ) +} diff --git a/components/Blocks/DynamicContent/Benefits/NextLevel/index.tsx b/components/Blocks/DynamicContent/Rewards/NextLevel/index.tsx similarity index 65% rename from components/Blocks/DynamicContent/Benefits/NextLevel/index.tsx rename to components/Blocks/DynamicContent/Rewards/NextLevel/index.tsx index cc2b16793..cf86c6aa1 100644 --- a/components/Blocks/DynamicContent/Benefits/NextLevel/index.tsx +++ b/components/Blocks/DynamicContent/Rewards/NextLevel/index.tsx @@ -1,7 +1,7 @@ import { Lock } from "react-feather" import { MembershipLevelEnum } from "@/constants/membershipLevels" -import { getProfile } from "@/lib/trpc/memoizedRequests" +import { serverClient } from "@/lib/trpc/server" import SectionContainer from "@/components/Section/Container" import SectionHeader from "@/components/Section/Header" @@ -11,38 +11,39 @@ import Grids from "@/components/TempDesignSystem/Grids" import Body from "@/components/TempDesignSystem/Text/Body" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" -import { getLang } from "@/i18n/serverContext" -import { getMembershipLevelObject } from "@/utils/membershipLevel" import styles from "./next.module.css" import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage" -export default async function NextLevelBenefitsBlock({ +export default async function NextLevelRewardsBlock({ title, subtitle, link, }: AccountPageComponentProps) { const intl = await getIntl() - const user = await getProfile() - if (!user || "error" in user || !user.membership) { + const membershipLevel = await serverClient().user.membershipLevel() + + if (!membershipLevel || !membershipLevel?.nextLevel) { return null } - const nextLevel = getMembershipLevelObject( - user.membership.nextLevel as MembershipLevelEnum, - getLang() - ) - if (!nextLevel) { - // TODO: handle this case, when missing or when user is top level? + + const nextLevelRewards = await serverClient().contentstack.rewards.byLevel({ + level_id: MembershipLevelEnum[membershipLevel?.nextLevel], + unique: true, + }) + + // TODO: handle this case, when missing or when user is top level? + if (!nextLevelRewards) { return null } - // TODO: how to handle different count of unlockable benefits? + return ( - {nextLevel.benefits.map((benefit) => ( -
+ {nextLevelRewards.rewards.map((reward) => ( +
{intl.formatMessage({ id: "Level up to unlock" })} @@ -51,11 +52,11 @@ export default async function NextLevelBenefitsBlock({ {intl.formatMessage( { id: "As our" }, - { level: nextLevel.name } + { level: nextLevelRewards.level?.name } )} - {" "} + - {benefit.title} + {reward.label}
diff --git a/components/Blocks/DynamicContent/Benefits/NextLevel/next.module.css b/components/Blocks/DynamicContent/Rewards/NextLevel/next.module.css similarity index 100% rename from components/Blocks/DynamicContent/Benefits/NextLevel/next.module.css rename to components/Blocks/DynamicContent/Rewards/NextLevel/next.module.css diff --git a/components/Blocks/DynamicContent/index.tsx b/components/Blocks/DynamicContent/index.tsx index cb6233139..bc024baeb 100644 --- a/components/Blocks/DynamicContent/index.tsx +++ b/components/Blocks/DynamicContent/index.tsx @@ -1,5 +1,3 @@ -import CurrentBenefitsBlock from "@/components/Blocks/DynamicContent/Benefits/CurrentLevel" -import NextLevelBenefitsBlock from "@/components/Blocks/DynamicContent/Benefits/NextLevel" import HowItWorks from "@/components/Blocks/DynamicContent/HowItWorks" import LoyaltyLevels from "@/components/Blocks/DynamicContent/LoyaltyLevels" import Overview from "@/components/Blocks/DynamicContent/Overview" @@ -7,6 +5,8 @@ import OverviewTable from "@/components/Blocks/DynamicContent/OverviewTable" import EarnAndBurn from "@/components/Blocks/DynamicContent/Points/EarnAndBurn" import ExpiringPoints from "@/components/Blocks/DynamicContent/Points/ExpiringPoints" import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview" +import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentLevel" +import NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel" import PreviousStays from "@/components/Blocks/DynamicContent/Stays/Previous" import SoonestStays from "@/components/Blocks/DynamicContent/Stays/Soonest" import UpcomingStays from "@/components/Blocks/DynamicContent/Stays/Upcoming" @@ -20,7 +20,7 @@ export default async function DynamicContent({ }: DynamicContentProps) { switch (dynamic_content.component) { case DynamicContentEnum.Blocks.components.current_benefits: - return + return case DynamicContentEnum.Blocks.components.earn_and_burn: return case DynamicContentEnum.Blocks.components.expiring_points: @@ -39,7 +39,7 @@ export default async function DynamicContent({ case DynamicContentEnum.Blocks.components.membership_overview: return case DynamicContentEnum.Blocks.components.next_benefits: - return + return case DynamicContentEnum.Blocks.components.overview_table: return ( toggleDropdown(DropdownTypeEnum.MyPagesMenu)} > - + {intl.formatMessage({ id: "Hi" })} {user.firstName}! {membershipLevel.name} - + {membershipPoints} {intl.formatMessage({ id: "points" })} @@ -61,10 +62,7 @@ export default function MyPagesMenuContent({
    {navigation.menuItems.map((menuItem, idx) => ( -
  • +
    • {menuItem.links.map((link) => ( diff --git a/components/Levels/Icon.tsx b/components/Levels/Icon.tsx new file mode 100644 index 000000000..61803f30c --- /dev/null +++ b/components/Levels/Icon.tsx @@ -0,0 +1,38 @@ +import { MembershipLevelEnum } from "@/constants/membershipLevels" + +import { + BestFriend, + CloseFriend, + DearFriend, + GoodFriend, + LoyalFriend, + NewFriend, + TrueFriend, +} from "@/components/Levels" + +import type { MembershipLevelIconProps } from "@/types/components/myPages/membership" + +export default function MembershipLevelIcon({ + level, + color = "pale", + ...props +}: MembershipLevelIconProps) { + switch (level) { + case MembershipLevelEnum.L1: + return + case MembershipLevelEnum.L2: + return + case MembershipLevelEnum.L3: + return + case MembershipLevelEnum.L4: + return + case MembershipLevelEnum.L5: + return + case MembershipLevelEnum.L6: + return + case MembershipLevelEnum.L7: + return + default: + return null + } +} diff --git a/components/Levels/Level/BestFriend.tsx b/components/Levels/Level/BestFriend.tsx index c0febca29..682f5ad61 100644 --- a/components/Levels/Level/BestFriend.tsx +++ b/components/Levels/Level/BestFriend.tsx @@ -2,7 +2,13 @@ import { levelVariants } from "../variants" import type { LevelProps } from "../levels" -export default function BestFriend({ className, color, ...props }: LevelProps) { +export default function BestFriend({ + className, + color, + height = "75", + width = "159", + ...props +}: LevelProps) { const classNames = levelVariants({ className, color, @@ -11,9 +17,9 @@ export default function BestFriend({ className, color, ...props }: LevelProps) { diff --git a/components/Levels/Level/CloseFriend.tsx b/components/Levels/Level/CloseFriend.tsx index 0f80c344a..8c0720f83 100644 --- a/components/Levels/Level/CloseFriend.tsx +++ b/components/Levels/Level/CloseFriend.tsx @@ -5,6 +5,8 @@ import type { LevelProps } from "../levels" export default function CloseFriend({ className, color, + height = "75", + width = "158", ...props }: LevelProps) { const classNames = levelVariants({ @@ -15,9 +17,9 @@ export default function CloseFriend({ diff --git a/components/Levels/Level/DearFriend.tsx b/components/Levels/Level/DearFriend.tsx index 96bf1cd58..ab210d88e 100644 --- a/components/Levels/Level/DearFriend.tsx +++ b/components/Levels/Level/DearFriend.tsx @@ -2,7 +2,13 @@ import { levelVariants } from "../variants" import type { LevelProps } from "../levels" -export default function DearFriend({ className, color, ...props }: LevelProps) { +export default function DearFriend({ + className, + color, + height = "75", + width = "159", + ...props +}: LevelProps) { const classNames = levelVariants({ className, color, @@ -11,9 +17,9 @@ export default function DearFriend({ className, color, ...props }: LevelProps) { diff --git a/components/Levels/Level/GoodFriend.tsx b/components/Levels/Level/GoodFriend.tsx index 09339d614..b5e5edb06 100644 --- a/components/Levels/Level/GoodFriend.tsx +++ b/components/Levels/Level/GoodFriend.tsx @@ -2,7 +2,13 @@ import { levelVariants } from "../variants" import type { LevelProps } from "../levels" -export default function GoodFriend({ className, color, ...props }: LevelProps) { +export default function GoodFriend({ + className, + color, + height = "75", + width = "159", + ...props +}: LevelProps) { const classNames = levelVariants({ className, color, @@ -11,9 +17,9 @@ export default function GoodFriend({ className, color, ...props }: LevelProps) { diff --git a/components/Levels/Level/LoyalFriend.tsx b/components/Levels/Level/LoyalFriend.tsx index 9e79b18a3..51816a2a9 100644 --- a/components/Levels/Level/LoyalFriend.tsx +++ b/components/Levels/Level/LoyalFriend.tsx @@ -5,6 +5,8 @@ import type { LevelProps } from "../levels" export default function LoyalFriend({ className, color, + height = "75", + width = "158", ...props }: LevelProps) { const classNames = levelVariants({ @@ -15,9 +17,9 @@ export default function LoyalFriend({ diff --git a/components/Levels/Level/NewFriend.tsx b/components/Levels/Level/NewFriend.tsx index 70b312d51..a0055df83 100644 --- a/components/Levels/Level/NewFriend.tsx +++ b/components/Levels/Level/NewFriend.tsx @@ -2,7 +2,13 @@ import { levelVariants } from "../variants" import type { LevelProps } from "../levels" -export default function NewFriend({ className, color, ...props }: LevelProps) { +export default function NewFriend({ + className, + color, + height = "75", + width = "159", + ...props +}: LevelProps) { const classNames = levelVariants({ className, color, @@ -11,9 +17,9 @@ export default function NewFriend({ className, color, ...props }: LevelProps) { diff --git a/components/Levels/Level/TrueFriend.tsx b/components/Levels/Level/TrueFriend.tsx index 22515e9b5..a122758b5 100644 --- a/components/Levels/Level/TrueFriend.tsx +++ b/components/Levels/Level/TrueFriend.tsx @@ -2,7 +2,13 @@ import { levelVariants } from "../variants" import type { LevelProps } from "../levels" -export default function TrueFriend({ className, color, ...props }: LevelProps) { +export default function TrueFriend({ + className, + color, + height = "75", + width = "159", + ...props +}: LevelProps) { const classNames = levelVariants({ className, color, @@ -11,9 +17,9 @@ export default function TrueFriend({ className, color, ...props }: LevelProps) { diff --git a/components/Levels/levels.ts b/components/Levels/levels.ts index 10a3c5b21..f7121351b 100644 --- a/components/Levels/levels.ts +++ b/components/Levels/levels.ts @@ -4,4 +4,7 @@ import type { VariantProps } from "class-variance-authority" export interface LevelProps extends Omit, "color">, - VariantProps {} + VariantProps { + height?: string + width?: string +} diff --git a/components/TempDesignSystem/ShowMoreButton/button.module.css b/components/TempDesignSystem/ShowMoreButton/button.module.css new file mode 100644 index 000000000..f55557f33 --- /dev/null +++ b/components/TempDesignSystem/ShowMoreButton/button.module.css @@ -0,0 +1,4 @@ +.container { + display: flex; + justify-content: center; +} diff --git a/components/TempDesignSystem/ShowMoreButton/index.tsx b/components/TempDesignSystem/ShowMoreButton/index.tsx new file mode 100644 index 000000000..6e9cae7bf --- /dev/null +++ b/components/TempDesignSystem/ShowMoreButton/index.tsx @@ -0,0 +1,32 @@ +"use client" + +import { useIntl } from "react-intl" + +import { ChevronDownIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" + +import styles from "./button.module.css" + +import type { ShowMoreButtonParams } from "@/types/components/myPages/stays/button" + +export default function ShowMoreButton({ + disabled, + loadMoreData, +}: ShowMoreButtonParams) { + const { formatMessage } = useIntl() + return ( +
      + +
      + ) +} diff --git a/components/Webviews/AccountPage/Blocks.tsx b/components/Webviews/AccountPage/Blocks.tsx index f8c8f0bb0..8e24918d4 100644 --- a/components/Webviews/AccountPage/Blocks.tsx +++ b/components/Webviews/AccountPage/Blocks.tsx @@ -1,8 +1,8 @@ -import CurrentBenefitsBlock from "@/components/Blocks/DynamicContent/Benefits/CurrentLevel" -import NextLevelBenefitsBlock from "@/components/Blocks/DynamicContent/Benefits/NextLevel" import Overview from "@/components/Blocks/DynamicContent/Overview" import EarnAndBurn from "@/components/Blocks/DynamicContent/Points/EarnAndBurn" import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview" +import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentLevel" +import NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel" import Shortcuts from "@/components/Blocks/Shortcuts" import JsonToHtml from "@/components/JsonToHtml" import { getLang } from "@/i18n/serverContext" @@ -31,9 +31,9 @@ function DynamicComponent({ dynamic_content }: AccountPageContentProps) { case DynamicContentEnum.Blocks.components.points_overview: return case DynamicContentEnum.Blocks.components.current_benefits: - return + return case DynamicContentEnum.Blocks.components.next_benefits: - return + return case DynamicContentEnum.Blocks.components.expiring_points: // TODO: Add once available // return diff --git a/data/loyaltyLevels/DA.json b/data/loyaltyLevels/DA.json deleted file mode 100644 index 4587fde60..000000000 --- a/data/loyaltyLevels/DA.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requiredPoints": 0, - "requiredNights": 0, - "benefits": [ - { - "title": "Prisvenlige værelser" - }, - { - "title": "10% weekendrabat på mad" - }, - { - "title": "Gratis mocktail til børn under opholdet" - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requiredPoints": 5000, - "requiredNights": 0, - "benefits": [ - { - "title": "15% weekendrabat på mad" - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requiredPoints": 10000, - "requiredNights": 0, - "benefits": [ - { - "title": "Sen check ud – 1 time, når tilgængeligt" - }, - { - "title": "Voucher på DKK 50,-" - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requiredPoints": 25000, - "requiredNights": 0, - "benefits": [ - { - "title": "25% optjeningsrate" - }, - { - "title": "Tidlig check ind, når tilgængeligt" - }, - { - "title": "Voucher på DKK 75,-" - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requiredPoints": 100000, - "requiredNights": 0, - "benefits": [ - { - "title": "Gratis opgraderinger, når tilgængelige" - }, - { - "title": "Voucher på DKK 100,-" - }, - { - "title": "2-for-1 morgenmad" - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requiredPoints": 250000, - "requiredNights": 0, - "benefits": [ - { - "title": "50% optjeningsrate" - }, - { - "title": "Voucher på DKK 150,-" - }, - { - "title": "48-timers værelsesgaranti" - }, - { - "title": "Altid gratis morgenmad" - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requiredPoints": 400000, - "requiredNights": 100, - "benefits": [ - { - "title": "Voucher på DKK 200,-" - }, - { - "title": "Årlig eksklusiv gave" - }, - { - "title": "Børneboost" - } - ] - } - ] -} diff --git a/data/loyaltyLevels/DE.json b/data/loyaltyLevels/DE.json deleted file mode 100644 index 64d2a3923..000000000 --- a/data/loyaltyLevels/DE.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requiredPoints": 0, - "requiredNights": 0, - "benefits": [ - { - "title": "Freundschaftspreise für Hotelzimmer" - }, - { - "title": "10 % Rabatt auf Speisen an den Wochenenden" - }, - { - "title": "Kostenloser Kinder-Mocktail während des Aufenthalts" - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requiredPoints": 5000, - "requiredNights": 0, - "benefits": [ - { - "title": "15 % Rabatt auf Speisen an den Wochenenden" - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requiredPoints": 10000, - "requiredNights": 0, - "benefits": [ - { - "title": "Später Check-Out – 1 Stunde, wenn verfügbar" - }, - { - "title": "Gutschein über 5 EUR" - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requiredPoints": 25000, - "requiredNights": 0, - "benefits": [ - { - "title": "25 % mehr Punkte" - }, - { - "title": "Früher Check-In – 1 Stunde, wenn verfügbar" - }, - { - "title": "Gutschein über 7,50 EUR" - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requiredPoints": 100000, - "requiredNights": 0, - "benefits": [ - { - "title": "Kostenloses Zimmer-Upgrade, wenn verfügbar" - }, - { - "title": "Gutschein über 10 EUR" - }, - { - "title": "Frühstück für Zwei zum Preis von einem" - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requiredPoints": 250000, - "requiredNights": 0, - "benefits": [ - { - "title": "50 % mehr Punkte" - }, - { - "title": "Gutschein über 15 EUR" - }, - { - "title": "48-Stunden-Zimmergarantie" - }, - { - "title": "Jederzeit ein kostenloses Frühstück" - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requiredPoints": 400000, - "requiredNights": 100, - "benefits": [ - { - "title": "Gutschein über 20 EUR" - }, - { - "title": "Ein exklusives Geschenk pro Jahr" - }, - { - "title": "Ein Geschenk für Kinder" - } - ] - } - ] -} diff --git a/data/loyaltyLevels/EN.json b/data/loyaltyLevels/EN.json deleted file mode 100644 index 958d9c4cb..000000000 --- a/data/loyaltyLevels/EN.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requiredPoints": 0, - "requiredNights": 0, - "benefits": [ - { - "title": "Friendly room rates" - }, - { - "title": "10% off on food on weekends" - }, - { - "title": "Free kids mocktail during stay" - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requiredPoints": 5000, - "requiredNights": 0, - "benefits": [ - { - "title": "15% on food on weekends" - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requiredPoints": 10000, - "requiredNights": 0, - "benefits": [ - { - "title": "Late checkout - 1 hour when available" - }, - { - "title": "5 EUR voucher" - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requiredPoints": 25000, - "requiredNights": 0, - "benefits": [ - { - "title": "25% earn rate" - }, - { - "title": "Early check-in - 1 hour when available" - }, - { - "title": "7.50 EUR voucher" - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requiredPoints": 100000, - "requiredNights": 0, - "benefits": [ - { - "title": "Free room upgrade when available" - }, - { - "title": "10 EUR voucher" - }, - { - "title": "2-for-1 breakfast" - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requiredPoints": 250000, - "requiredNights": 0, - "benefits": [ - { - "title": "50% earn rate" - }, - { - "title": "15 EUR voucher" - }, - { - "title": "48h room guarantee" - }, - { - "title": "Always free breakfast" - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requiredPoints": 400000, - "requiredNights": 100, - "benefits": [ - { - "title": "20 EUR voucher" - }, - { - "title": "Yearly exclusive gift" - }, - { - "title": "Kid's boost" - } - ] - } - ] -} diff --git a/data/loyaltyLevels/FI.json b/data/loyaltyLevels/FI.json deleted file mode 100644 index dcd499757..000000000 --- a/data/loyaltyLevels/FI.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requiredPoints": 0, - "requiredNights": 0, - "benefits": [ - { - "title": "Ystävälliset huonehinnat" - }, - { - "title": "10 % alennusta ruoasta viikonloppuisin" - }, - { - "title": "Maksuton lasten mocktail majoituksen aikana" - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requiredPoints": 5000, - "requiredNights": 0, - "benefits": [ - { - "title": "15 % alennusta ruoasta viikonloppuisin" - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requiredPoints": 10000, - "requiredNights": 0, - "benefits": [ - { - "title": "Myöhäinen uloskirjautuminen – 1 tunti lisäaikaa varaustilanteen mukaan" - }, - { - "title": "Ravintolakuponki (arvo 5 €)" - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requiredPoints": 25000, - "requiredNights": 0, - "benefits": [ - { - "title": "Ansaintakerroin +25 %" - }, - { - "title": "Aikainen sisäänkirjautuminen – 1 tunti lisäaikaa varaustilanteen mukaan" - }, - { - "title": "Ravintolakuponki (arvo 7,50 €)" - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requiredPoints": 100000, - "requiredNights": 0, - "benefits": [ - { - "title": "Maksuton huoneluokan korotus varaustilanteen mukaan" - }, - { - "title": "Ravintolakuponki (arvo 10 €)" - }, - { - "title": "Aamiainen – kaksi yhden hinnalla" - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requiredPoints": 250000, - "requiredNights": 0, - "benefits": [ - { - "title": "Ansaintakerroin +50 %" - }, - { - "title": "Ravintolakuponki (arvo 15 €)" - }, - { - "title": "48 tunnin huonetakuu" - }, - { - "title": "Aamiainen aina maksutta" - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requiredPoints": 400000, - "requiredNights": 100, - "benefits": [ - { - "title": "Ravintolakuponki (arvo 20 €)" - }, - { - "title": "Henkilökohtainen lahja vuosittain" - }, - { - "title": "Tervetuliaislahja lapselle" - } - ] - } - ] -} diff --git a/data/loyaltyLevels/NO.json b/data/loyaltyLevels/NO.json deleted file mode 100644 index f4fa72f19..000000000 --- a/data/loyaltyLevels/NO.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requiredPoints": 0, - "requiredNights": 0, - "benefits": [ - { - "title": "Vennlige rompriser" - }, - { - "title": "10 % rabatt på mat i helger" - }, - { - "title": "Gratis barne-mocktail under oppholdet" - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requiredPoints": 5000, - "requiredNights": 0, - "benefits": [ - { - "title": "15 % rabatt på mat i helger" - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requiredPoints": 10000, - "requiredNights": 0, - "benefits": [ - { - "title": "Sen utsjekking – 1 time når tilgjengelig" - }, - { - "title": "Kupong på 50 NOK" - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requiredPoints": 25000, - "requiredNights": 0, - "benefits": [ - { - "title": "25 % opptjeningsrate" - }, - { - "title": "Tidlig innsjekk – 1 time når tilgjengelig" - }, - { - "title": "Kupong på 75 NOK" - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requiredPoints": 100000, - "requiredNights": 0, - "benefits": [ - { - "title": "Gratis romoppgradering når tilgjengelig" - }, - { - "title": "Kupong på 100 NOK" - }, - { - "title": "2-for-1 frokost" - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requiredPoints": 250000, - "requiredNights": 0, - "benefits": [ - { - "title": "50 % opptjeningsrate" - }, - { - "title": "Kupong på 150 NOK" - }, - { - "title": "Romgaranti i 48 timer" - }, - { - "title": "Alltid gratis frokost" - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requiredPoints": 400000, - "requiredNights": 100, - "benefits": [ - { - "title": "Kupong på 200 NOK" - }, - { - "title": "Årlig eksklusiv gave" - }, - { - "title": "Boost for barn" - } - ] - } - ] -} diff --git a/data/loyaltyLevels/SV.json b/data/loyaltyLevels/SV.json deleted file mode 100644 index c543b884a..000000000 --- a/data/loyaltyLevels/SV.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "levels": [ - { - "level": 1, - "name": "New Friend", - "requiredPoints": 0, - "requiredNights": 0, - "benefits": [ - { - "title": "Friendspriser på rum" - }, - { - "title": "10 % rabatt på mat under helger" - }, - { - "title": "Fri mocktail för barn under vistelse" - } - ] - }, - { - "level": 2, - "name": "Good Friend", - "requiredPoints": 5000, - "requiredNights": 0, - "benefits": [ - { - "title": "15 % rabatt på mat under helger" - } - ] - }, - { - "level": 3, - "name": "Close Friend", - "requiredPoints": 10000, - "requiredNights": 0, - "benefits": [ - { - "title": "Sen utcheckning – 1 timme, i mån av plats" - }, - { - "title": "Kupong 50 kr" - } - ] - }, - { - "level": 4, - "name": "Dear Friend", - "requiredPoints": 25000, - "requiredNights": 0, - "benefits": [ - { - "title": "25 % poängboost" - }, - { - "title": "Tidig incheckning – 1 timme, i mån av plats" - }, - { - "title": "Kupong 75 kr" - } - ] - }, - { - "level": 5, - "name": "Loyal Friend", - "requiredPoints": 100000, - "requiredNights": 0, - "benefits": [ - { - "title": "Kostnadsfri uppgradering av rum, i mån av plats" - }, - { - "title": "Kupong 100 kr" - }, - { - "title": "Frukost 2 för 1" - } - ] - }, - { - "level": 6, - "name": "True Friend", - "requiredPoints": 250000, - "requiredNights": 0, - "benefits": [ - { - "title": "50 % poängboost" - }, - { - "title": "Kupong 150 kr" - }, - { - "title": "48 timmars rumsgaranti" - }, - { - "title": "Alltid kostnadsfri frukost" - } - ] - }, - { - "level": 7, - "name": "Best Friend", - "requiredPoints": 400000, - "requiredNights": 100, - "benefits": [ - { - "title": "Kupong 200 kr" - }, - { - "title": "Spännande gåva varje år" - }, - { - "title": "Boost för barn" - } - ] - } - ] -} diff --git a/data/loyaltyLevels/index.ts b/data/loyaltyLevels/index.ts deleted file mode 100644 index 04054d3fd..000000000 --- a/data/loyaltyLevels/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Lang } from "@/constants/languages" - -import DA from "./DA.json" -import DE from "./DE.json" -import EN from "./EN.json" -import FI from "./FI.json" -import NO from "./NO.json" -import SV from "./SV.json" - -const levelsData = { - [Lang.en]: EN, - [Lang.sv]: SV, - [Lang.no]: NO, - [Lang.fi]: FI, - [Lang.da]: DA, - [Lang.de]: DE, -} - -export default levelsData diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 0d85c5061..1fbb3af98 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -19,6 +19,8 @@ export namespace endpoints { locations = "hotel/v1/Locations", previousStays = "booking/v1/Stays/past", upcomingStays = "booking/v1/Stays/future", + rewards = `${profile}/reward`, + tierRewards = `${profile}/TierRewards`, } } diff --git a/lib/graphql/Query/LoyaltyLevels.graphql b/lib/graphql/Query/LoyaltyLevels.graphql new file mode 100644 index 000000000..652d096d7 --- /dev/null +++ b/lib/graphql/Query/LoyaltyLevels.graphql @@ -0,0 +1,25 @@ +query GetAllLoyaltyLevels($lang: String!, $level_ids: [String]!) { + all_loyalty_level(where: { level_id_in: $level_ids }, locale: $lang) { + items { + description + level_id + name + required_points + required_nights + user_facing_tag + } + } +} + +query GetLoyaltyLevel($lang: String!, $level_id: String!) { + all_loyalty_level(where: { level_id: $level_id }, locale: $lang) { + items { + description + level_id + name + required_points + required_nights + user_facing_tag + } + } +} diff --git a/lib/graphql/Query/Rewards.graphql b/lib/graphql/Query/Rewards.graphql new file mode 100644 index 000000000..58915640f --- /dev/null +++ b/lib/graphql/Query/Rewards.graphql @@ -0,0 +1,15 @@ +query GetRewards($locale: String!, $rewardIds: [String!]) { + all_reward(locale: $locale, where: { reward_id_in: $rewardIds }) { + items { + taxonomies { + term_uid + } + label + grouped_label + description + grouped_description + value + reward_id + } + } +} diff --git a/server/routers/contentstack/index.ts b/server/routers/contentstack/index.ts index d409f06ae..bde6065db 100644 --- a/server/routers/contentstack/index.ts +++ b/server/routers/contentstack/index.ts @@ -7,9 +7,11 @@ import { breadcrumbsRouter } from "./breadcrumbs" import { contentPageRouter } from "./contentPage" import { hotelPageRouter } from "./hotelPage" import { languageSwitcherRouter } from "./languageSwitcher" +import { loyaltyLevelRouter } from "./loyaltyLevel" import { loyaltyPageRouter } from "./loyaltyPage" import { metaDataRouter } from "./metadata" import { myPagesRouter } from "./myPages" +import { rewardRouter } from "./reward" export const contentstackRouter = router({ accountPage: accountPageRouter, @@ -22,4 +24,6 @@ export const contentstackRouter = router({ contentPage: contentPageRouter, myPages: myPagesRouter, metaData: metaDataRouter, + rewards: rewardRouter, + loyaltyLevels: loyaltyLevelRouter, }) diff --git a/server/routers/contentstack/loyaltyLevel/index.ts b/server/routers/contentstack/loyaltyLevel/index.ts new file mode 100644 index 000000000..4b5f9d720 --- /dev/null +++ b/server/routers/contentstack/loyaltyLevel/index.ts @@ -0,0 +1,5 @@ +import { mergeRouters } from "@/server/trpc" + +import { loyaltyLevelQueryRouter } from "./query" + +export const loyaltyLevelRouter = mergeRouters(loyaltyLevelQueryRouter) diff --git a/server/routers/contentstack/loyaltyLevel/input.ts b/server/routers/contentstack/loyaltyLevel/input.ts new file mode 100644 index 000000000..7e10ccfe4 --- /dev/null +++ b/server/routers/contentstack/loyaltyLevel/input.ts @@ -0,0 +1,7 @@ +import { z } from "zod" + +import { MembershipLevelEnum } from "@/constants/membershipLevels" + +export const loyaltyLevelInput = z.object({ + level: z.nativeEnum(MembershipLevelEnum), +}) diff --git a/server/routers/contentstack/loyaltyLevel/output.ts b/server/routers/contentstack/loyaltyLevel/output.ts new file mode 100644 index 000000000..c449c65c2 --- /dev/null +++ b/server/routers/contentstack/loyaltyLevel/output.ts @@ -0,0 +1,24 @@ +import { z } from "zod" + +import { MembershipLevelEnum } from "@/constants/membershipLevels" + +export const validateLoyaltyLevelsSchema = z + .object({ + all_loyalty_level: z.object({ + items: z.array( + z.object({ + level_id: z.nativeEnum(MembershipLevelEnum), + name: z.string(), + user_facing_tag: z.string().optional(), + description: z.string().optional(), + required_nights: z.number().optional().nullable(), + required_points: z.number(), + }) + ), + }), + }) + .transform((data) => data.all_loyalty_level.items) + +export type LoyaltyLevelsResponse = z.input + +export type LoyaltyLevel = z.output[0] diff --git a/server/routers/contentstack/loyaltyLevel/query.ts b/server/routers/contentstack/loyaltyLevel/query.ts new file mode 100644 index 000000000..58882dde7 --- /dev/null +++ b/server/routers/contentstack/loyaltyLevel/query.ts @@ -0,0 +1,147 @@ +import { metrics } from "@opentelemetry/api" + +import { + MembershipLevel, + MembershipLevelEnum, +} from "@/constants/membershipLevels" +import { + GetAllLoyaltyLevels, + GetLoyaltyLevel, +} from "@/lib/graphql/Query/LoyaltyLevels.graphql" +import { request } from "@/lib/graphql/request" +import { Context } from "@/server/context" +import { notFound } from "@/server/errors/trpc" +import { contentstackBaseProcedure, router } from "@/server/trpc" + +import { generateLoyaltyConfigTag } from "@/utils/generateTag" + +import { loyaltyLevelInput } from "./input" +import { LoyaltyLevelsResponse, validateLoyaltyLevelsSchema } from "./output" + +const meter = metrics.getMeter("trpc.loyaltyLevel") +// OpenTelemetry metrics: Loyalty Level +const getAllLoyaltyLevelCounter = meter.createCounter( + "trpc.contentstack.loyaltyLevel.all" +) + +const getAllLoyaltyLevelSuccessCounter = meter.createCounter( + "trpc.contentstack.loyaltyLevel.all-success" +) +const getAllLoyaltyLevelFailCounter = meter.createCounter( + "trpc.contentstack.loyaltyLevel.all-fail" +) + +export async function getAllLoyaltyLevels(ctx: Context) { + getAllLoyaltyLevelCounter.add(1) + + // Ideally we should fetch all available tiers from API, but since they + // are static, we can just use the enum values. We want to know which + // levels we are fetching so that we can use tags to cache them + const allLevelIds = Object.values(MembershipLevelEnum) + + const tags = allLevelIds.map((levelId) => + generateLoyaltyConfigTag(ctx.lang, "loyalty_level", levelId) + ) + + const loyaltyLevelsConfigResponse = await request( + GetAllLoyaltyLevels, + { lang: ctx.lang, level_ids: allLevelIds }, + { next: { tags }, cache: "force-cache" } + ) + + if (!loyaltyLevelsConfigResponse.data) { + getAllLoyaltyLevelFailCounter.add(1) + const notFoundError = notFound(loyaltyLevelsConfigResponse) + console.error( + "contentstack.loyaltyLevels not found error", + JSON.stringify({ + query: { + lang: ctx.lang, + }, + error: { code: notFoundError.code }, + }) + ) + throw notFoundError + } + + const validatedLoyaltyLevels = validateLoyaltyLevelsSchema.safeParse( + loyaltyLevelsConfigResponse.data + ) + if (!validatedLoyaltyLevels.success) { + getAllLoyaltyLevelFailCounter.add(1) + console.error(validatedLoyaltyLevels.error) + console.error( + "contentstack.rewards validation error", + JSON.stringify({ + query: { + lang: ctx.lang, + }, + error: validatedLoyaltyLevels.error, + }) + ) + return [] + } + + getAllLoyaltyLevelSuccessCounter.add(1) + return validatedLoyaltyLevels.data +} + +export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) { + getAllLoyaltyLevelCounter.add(1) + + const loyaltyLevelsConfigResponse = await request( + GetLoyaltyLevel, + { lang: ctx.lang, level_id }, + { + next: { + tags: [generateLoyaltyConfigTag(ctx.lang, "loyalty_level", level_id)], + }, + cache: "force-cache", + } + ) + if ( + !loyaltyLevelsConfigResponse.data || + !loyaltyLevelsConfigResponse.data.all_loyalty_level.items.length + ) { + getAllLoyaltyLevelFailCounter.add(1) + const notFoundError = notFound(loyaltyLevelsConfigResponse) + console.error( + "contentstack.loyaltyLevels not found error", + JSON.stringify({ + query: { lang: ctx.lang, level_id }, + error: { code: notFoundError.code }, + }) + ) + throw notFoundError + } + + const validatedLoyaltyLevels = validateLoyaltyLevelsSchema.safeParse( + loyaltyLevelsConfigResponse.data + ) + if (!validatedLoyaltyLevels.success) { + getAllLoyaltyLevelFailCounter.add(1) + console.error(validatedLoyaltyLevels.error) + console.error( + "contentstack.rewards validation error", + JSON.stringify({ + query: { lang: ctx.lang, level_id }, + error: validatedLoyaltyLevels.error, + }) + ) + return null + } + + getAllLoyaltyLevelSuccessCounter.add(1) + return validatedLoyaltyLevels.data[0] +} + +export const loyaltyLevelQueryRouter = router({ + byLevel: contentstackBaseProcedure + .input(loyaltyLevelInput) + .query(async function ({ ctx, input }) { + return getLoyaltyLevel(ctx, input.level) + }), + all: contentstackBaseProcedure.query(async function ({ ctx }) { + return getAllLoyaltyLevels(ctx) + }), +}) diff --git a/server/routers/contentstack/reward/index.ts b/server/routers/contentstack/reward/index.ts new file mode 100644 index 000000000..9b2203350 --- /dev/null +++ b/server/routers/contentstack/reward/index.ts @@ -0,0 +1,5 @@ +import { mergeRouters } from "@/server/trpc" + +import { rewardQueryRouter } from "./query" + +export const rewardRouter = mergeRouters(rewardQueryRouter) diff --git a/server/routers/contentstack/reward/input.ts b/server/routers/contentstack/reward/input.ts new file mode 100644 index 000000000..43da34bed --- /dev/null +++ b/server/routers/contentstack/reward/input.ts @@ -0,0 +1,19 @@ +import { z } from "zod" + +import { Lang } from "@/constants/languages" +import { MembershipLevelEnum } from "@/constants/membershipLevels" + +export const rewardsByLevelInput = z.object({ + level_id: z.nativeEnum(MembershipLevelEnum), + unique: z.boolean().default(false), +}) + +export const rewardsAllInput = z + .object({ unique: z.boolean() }) + .default({ unique: false }) + +export const rewardsCurrentInput = z.object({ + limit: z.number().min(0).default(3), + cursor: z.number().optional().default(0), + lang: z.nativeEnum(Lang).optional(), +}) diff --git a/server/routers/contentstack/reward/output.ts b/server/routers/contentstack/reward/output.ts new file mode 100644 index 000000000..302a6b4a7 --- /dev/null +++ b/server/routers/contentstack/reward/output.ts @@ -0,0 +1,82 @@ +import { z } from "zod" + +import { MembershipLevelEnum } from "@/constants/membershipLevels" + +export const validateApiRewardSchema = z.object({ + data: z.array( + z + .object({ + title: z.string().optional(), + id: z.string().optional(), + type: z.string().optional(), + status: z.string().optional(), + rewardId: z.string().optional(), + redeemLocation: z.string().optional(), + autoApplyReward: z.boolean().default(false), + rewardType: z.string().optional(), + rewardTierLevel: z.string().optional(), + }) + .optional() + ), +}) + +enum TierKey { + tier1 = MembershipLevelEnum.L1, + tier2 = MembershipLevelEnum.L2, + tier3 = MembershipLevelEnum.L3, + tier4 = MembershipLevelEnum.L4, + tier5 = MembershipLevelEnum.L5, + tier6 = MembershipLevelEnum.L6, + tier7 = MembershipLevelEnum.L7, +} + +type Key = keyof typeof TierKey + +export const validateApiTierRewardsSchema = z.record( + z.nativeEnum(TierKey).transform((data) => { + return TierKey[data as unknown as Key] + }), + z.array( + z + .object({ + title: z.string().optional(), + id: z.string().optional(), + type: z.string().optional(), + status: z.string().optional(), + rewardId: z.string().optional(), + redeemLocation: z.string().optional(), + autoApplyReward: z.boolean().default(false), + rewardType: z.string().optional(), + rewardTierLevel: z.string().optional(), + }) + .optional() + ) +) + +export const validateCmsRewardsSchema = z + .object({ + data: z.object({ + all_reward: z.object({ + items: z.array( + z.object({ + taxonomies: z.array( + z.object({ + term_uid: z.string().optional(), + }) + ), + label: z.string().optional(), + reward_id: z.string(), + grouped_label: z.string().optional(), + description: z.string().optional(), + grouped_description: z.string().optional(), + value: z.string().optional(), + }) + ), + }), + }), + }) + .transform((data) => data.data.all_reward.items) + +export type CmsRewardsResponse = z.input + +export type Reward = z.output[0] diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts new file mode 100644 index 000000000..543cd46cc --- /dev/null +++ b/server/routers/contentstack/reward/query.ts @@ -0,0 +1,371 @@ +import { metrics } from "@opentelemetry/api" + +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 { + contentStackBaseWithProfileServiceProcedure, + contentStackBaseWithProtectedProcedure, + router, +} from "@/server/trpc" + +import { generateLoyaltyConfigTag } from "@/utils/generateTag" + +import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query" +import { + rewardsAllInput, + rewardsByLevelInput, + rewardsCurrentInput, +} from "./input" +import { + CmsRewardsResponse, + Reward, + validateApiRewardSchema, + validateApiTierRewardsSchema, + validateCmsRewardsSchema, +} from "./output" + +const meter = metrics.getMeter("trpc.reward") +// OpenTelemetry metrics: Reward + +const getCurrentRewardCounter = meter.createCounter( + "trpc.contentstack.reward.current" +) +const getCurrentRewardSuccessCounter = meter.createCounter( + "trpc.contentstack.reward.current-success" +) + +const getCurrentRewardFailCounter = meter.createCounter( + "trpc.contentstack.reward.current-fail" +) + +const getByLevelRewardCounter = meter.createCounter( + "trpc.contentstack.reward.byLevel" +) +const getByLevelRewardSuccessCounter = meter.createCounter( + "trpc.contentstack.reward.byLevel-success" +) + +const getByLevelRewardFailCounter = meter.createCounter( + "trpc.contentstack.reward.byLevel-fail" +) + +const getAllRewardCounter = meter.createCounter("trpc.contentstack.reward.all") + +const getAllRewardSuccessCounter = meter.createCounter( + "trpc.contentstack.reward.all-success" +) +const getAllRewardFailCounter = meter.createCounter( + "trpc.contentstack.reward.all-fail" +) + +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, + }), + }) + 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) + + 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, + }) + ) + return null + } + + return validatedApiTierRewards.data +} + +async function getCmsRewards(locale: Lang, rewardIds: string[]) { + const tags = rewardIds.map((id) => + generateLoyaltyConfigTag(locale, "reward", id) + ) + + const cmsRewardsResponse = await request( + GetRewards, + { + locale: locale, + rewardIds, + }, + { next: { tags }, cache: "force-cache" } + ) + + if (!cmsRewardsResponse.data) { + getAllRewardFailCounter.add(1, { + lang: locale, + error_type: "validation_error", + error: JSON.stringify(cmsRewardsResponse.data), + }) + const notFoundError = notFound(cmsRewardsResponse) + console.error( + "contentstack.rewards not found error", + JSON.stringify({ + query: { + locale, + rewardIds, + }, + error: { code: notFoundError.code }, + }) + ) + throw notFoundError + } + + const validatedCmsRewards = + validateCmsRewardsSchema.safeParse(cmsRewardsResponse) + + if (!validatedCmsRewards.success) { + getAllRewardFailCounter.add(1, { + locale, + rewardIds, + error_type: "validation_error", + error: JSON.stringify(validatedCmsRewards.error), + }) + console.error(validatedCmsRewards.error) + console.error( + "contentstack.rewards validation error", + JSON.stringify({ + query: { locale, rewardIds }, + error: validatedCmsRewards.error, + }) + ) + return null + } + + return validatedCmsRewards.data +} + +export const rewardQueryRouter = router({ + current: contentStackBaseWithProtectedProcedure + .input(rewardsCurrentInput) + .query(async function ({ input, ctx }) { + getCurrentRewardCounter.add(1) + + const { limit, cursor } = input + + const apiResponse = await api.get(api.endpoints.v1.rewards, { + cache: undefined, // override defaultOptions + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + next: { revalidate: 60 * 60 }, + }) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + getCurrentRewardFailCounter.add(1, { + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) + console.error( + "api.reward error ", + JSON.stringify({ + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + return null + } + + const data = await apiResponse.json() + + const validatedApiRewards = validateApiRewardSchema.safeParse(data) + + if (!validatedApiRewards.success) { + getCurrentRewardFailCounter.add(1, { + locale: ctx.lang, + error_type: "validation_error", + error: JSON.stringify(validatedApiRewards.error), + }) + console.error(validatedApiRewards.error) + console.error( + "contentstack.rewards validation error", + JSON.stringify({ + query: { locale: ctx.lang }, + error: validatedApiRewards.error, + }) + ) + return null + } + + const rewardIds = validatedApiRewards.data.data + .map((reward) => reward?.rewardId) + .filter(Boolean) + .sort() as string[] + + const slicedData = rewardIds.slice(cursor, limit + cursor) + + const cmsRewards = await getCmsRewards(ctx.lang, slicedData) + + if (!cmsRewards) { + return null + } + + const nextCursor = + limit + cursor < rewardIds.length ? limit + cursor : undefined + + getCurrentRewardSuccessCounter.add(1) + return { + rewards: cmsRewards, + nextCursor, + } + }), + byLevel: contentStackBaseWithProfileServiceProcedure + .input(rewardsByLevelInput) + .query(async function ({ input, ctx }) { + getByLevelRewardCounter.add(1) + const { level_id } = input + + const allUpcomingApiRewards = await getAllApiRewards(ctx) + + if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) { + getByLevelRewardFailCounter.add(1) + + return null + } + + let apiRewards = allUpcomingApiRewards[level_id]! + + if (input.unique) { + apiRewards = allUpcomingApiRewards[level_id]!.filter( + (reward) => reward?.rewardTierLevel === level_id + ) + } + + const rewardIds = apiRewards + .map((reward) => reward?.rewardId) + .filter((id): id is string => Boolean(id)) + + const contentStackRewards = await getCmsRewards(ctx.lang, rewardIds) + if (!contentStackRewards) { + return null + } + + const loyaltyLevelsConfig = await getLoyaltyLevel(ctx, input.level_id) + + const levelsWithRewards = apiRewards + .map((reward) => { + const contentStackReward = contentStackRewards.find((r) => { + return r.reward_id === reward?.rewardId + }) + + if (contentStackReward) { + return contentStackReward + } else { + console.error("No contentStackReward found", reward?.rewardId) + } + }) + .filter((reward): reward is Reward => Boolean(reward)) + + getByLevelRewardSuccessCounter.add(1) + return { level: loyaltyLevelsConfig, rewards: levelsWithRewards } + }), + all: contentStackBaseWithProfileServiceProcedure + .input(rewardsAllInput) + .query(async function ({ input, ctx }) { + getAllRewardCounter.add(1) + const allApiRewards = await getAllApiRewards(ctx) + + if (!allApiRewards) { + return [] + } + + const rewardIds = Object.values(allApiRewards) + .flatMap((level) => level.map((reward) => reward?.rewardId)) + .filter((id): id is string => Boolean(id)) + + const contentStackRewards = await getCmsRewards( + ctx.lang, + getUniqueRewardIds(rewardIds) + ) + + if (!contentStackRewards) { + return [] + } + + const loyaltyLevelsConfig = await getAllLoyaltyLevels(ctx) + const levelsWithRewards = Object.entries(allApiRewards).map( + ([level, rewards]) => { + const combinedRewards = rewards + .filter((r) => (input.unique ? r?.rewardTierLevel === level : true)) + .map((reward) => { + const contentStackReward = contentStackRewards.find((r) => { + return r.reward_id === reward?.rewardId + }) + + if (contentStackReward) { + return contentStackReward + } else { + console.error("No contentStackReward found", reward?.rewardId) + } + }) + .filter((reward): reward is Reward => Boolean(reward)) + + const levelConfig = loyaltyLevelsConfig.find( + (l) => l.level_id === level + ) + + if (!levelConfig) { + getAllRewardFailCounter.add(1) + + console.error("contentstack.loyaltyLevels level not found") + throw notFound() + } + return { ...levelConfig, rewards: combinedRewards } + } + ) + + getAllRewardSuccessCounter.add(1) + return levelsWithRewards + }), +}) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index b1562419a..a515ea420 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 { - contentStackUidWithServiceProcedure, + contentStackUidWithHotelServiceProcedure, + hotelServiceProcedure, publicProcedure, router, - serviceProcedure, } from "@/server/trpc" import { toApiLang } from "@/server/utils" @@ -83,7 +83,7 @@ async function getContentstackData( } export const hotelQueryRouter = router({ - get: contentStackUidWithServiceProcedure + get: contentStackUidWithHotelServiceProcedure .input(getHotelInputSchema) .query(async ({ ctx, input }) => { const { lang, uid } = ctx @@ -178,34 +178,34 @@ export const hotelQueryRouter = router({ const roomCategories = included ? included - .filter((item) => item.type === "roomcategories") - .map((roomCategory) => { - const validatedRoom = roomSchema.safeParse(roomCategory) - if (!validatedRoom.success) { - getHotelFailCounter.add(1, { - hotelId, - lang, - include, - error_type: "validation_error", - error: JSON.stringify( - validatedRoom.error.issues.map(({ code, message }) => ({ - code, - message, - })) - ), - }) - console.error( - "api.hotels.hotel validation error", - JSON.stringify({ - query: { hotelId, params }, - error: validatedRoom.error, + .filter((item) => item.type === "roomcategories") + .map((roomCategory) => { + const validatedRoom = roomSchema.safeParse(roomCategory) + if (!validatedRoom.success) { + getHotelFailCounter.add(1, { + hotelId, + lang, + include, + error_type: "validation_error", + error: JSON.stringify( + validatedRoom.error.issues.map(({ code, message }) => ({ + code, + message, + })) + ), }) - ) - throw badRequestError() - } + console.error( + "api.hotels.hotel validation error", + JSON.stringify({ + query: { hotelId, params }, + error: validatedRoom.error, + }) + ) + throw badRequestError() + } - return validatedRoom.data - }) + return validatedRoom.data + }) : [] const activities = contentstackData?.content @@ -233,7 +233,7 @@ export const hotelQueryRouter = router({ } }), availability: router({ - get: serviceProcedure + get: hotelServiceProcedure .input(getAvailabilityInputSchema) .query(async ({ input, ctx }) => { const { @@ -395,7 +395,7 @@ export const hotelQueryRouter = router({ }), }), hotelData: router({ - get: serviceProcedure + get: hotelServiceProcedure .input(getlHotelDataInputSchema) .query(async ({ ctx, input }) => { const { hotelId, language, include } = input @@ -493,7 +493,7 @@ export const hotelQueryRouter = router({ }), }), locations: router({ - get: serviceProcedure.query(async function ({ ctx }) { + get: hotelServiceProcedure.query(async function ({ ctx }) { const searchParams = new URLSearchParams() searchParams.set("language", toApiLang(ctx.lang)) diff --git a/server/tokenManager.ts b/server/tokenManager.ts index 9f4e48a80..e60bb02e5 100644 --- a/server/tokenManager.ts +++ b/server/tokenManager.ts @@ -4,7 +4,9 @@ import { ServiceTokenResponse } from "@/types/tokens" const SERVICE_TOKEN_REVALIDATE_SECONDS = 3599 // 59 minutes and 59 seconds. -export async function fetchServiceToken(): Promise { +export async function fetchServiceToken( + scopes: string[] +): Promise { try { const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, { method: "POST", @@ -16,7 +18,7 @@ export async function fetchServiceToken(): Promise { grant_type: "client_credentials", client_id: env.CURITY_CLIENT_ID_SERVICE, client_secret: env.CURITY_CLIENT_SECRET_SERVICE, - scope: ["hotel"].join(","), + scope: scopes.join(","), }), next: { revalidate: SERVICE_TOKEN_REVALIDATE_SECONDS, diff --git a/server/trpc.ts b/server/trpc.ts index e3085f216..4f5409a01 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -13,6 +13,7 @@ import { import { type Context, createContext } from "./context" import { fetchServiceToken } from "./tokenManager" import { transformer } from "./transformer" +import { langInput } from "./utils" import type { Session } from "next-auth" @@ -39,7 +40,19 @@ export const { createCallerFactory, mergeRouters, router } = t export const publicProcedure = t.procedure export const contentstackBaseProcedure = t.procedure.use(async function (opts) { if (!opts.ctx.lang) { - throw badRequestError("Missing Lang in tRPC context") + // When fetching data client side with TRPC we don't pass through middlewares and therefore do not get the lang through headers + // We can then pass lang as an input in the request and set it to the context in the procedure + const input = await opts.getRawInput() + const parsedInput = langInput.safeParse(input) + if (!parsedInput.success) { + throw badRequestError("Missing Lang in tRPC context") + } + + return opts.next({ + ctx: { + lang: parsedInput.data.lang, + }, + }) } return opts.next({ @@ -108,10 +121,10 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) { }) }) -export const serviceProcedure = t.procedure.use(async (opts) => { - const { access_token } = await fetchServiceToken() +export const profileServiceProcedure = t.procedure.use(async (opts) => { + const { access_token } = await fetchServiceToken(["profile"]) if (!access_token) { - throw internalServerError("Failed to obtain service token") + throw internalServerError("Failed to obtain profile service token") } return opts.next({ ctx: { @@ -120,6 +133,17 @@ export const serviceProcedure = t.procedure.use(async (opts) => { }) }) +export const hotelServiceProcedure = t.procedure.use(async (opts) => { + const { access_token } = await fetchServiceToken(["hotel"]) + if (!access_token) { + throw internalServerError("Failed to obtain hotel service token") + } + return opts.next({ + ctx: { + serviceToken: access_token, + }, + }) +}) export const serverActionProcedure = t.procedure.experimental_caller( experimental_nextAppDirCaller({ createContext, @@ -149,5 +173,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 contentStackUidWithServiceProcedure = - contentstackExtendedProcedureUID.unstable_concat(serviceProcedure) +export const contentStackUidWithHotelServiceProcedure = + contentstackExtendedProcedureUID.unstable_concat(hotelServiceProcedure) + +export const contentStackBaseWithProfileServiceProcedure = + contentstackBaseProcedure.unstable_concat(profileServiceProcedure) + +export const contentStackBaseWithProtectedProcedure = + contentstackBaseProcedure.unstable_concat(protectedProcedure) diff --git a/types/components/header/myPagesMenu.ts b/types/components/header/myPagesMenu.ts index dcad1887d..22cab2ca7 100644 --- a/types/components/header/myPagesMenu.ts +++ b/types/components/header/myPagesMenu.ts @@ -1,6 +1,6 @@ import { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query" -import { MembershipLevel } from "@/utils/user" +import { FriendsMembership } from "@/utils/user" import type { User } from "@/types/user" @@ -11,7 +11,7 @@ type MyPagesNavigation = Awaited< export interface MyPagesMenuProps { navigation: MyPagesNavigation user: Pick - membership?: MembershipLevel | null + membership?: FriendsMembership | null } export interface MyPagesMenuContentProps extends MyPagesMenuProps { diff --git a/types/components/myPages/membership.ts b/types/components/myPages/membership.ts index ce352511f..4aab58de5 100644 --- a/types/components/myPages/membership.ts +++ b/types/components/myPages/membership.ts @@ -1,8 +1,10 @@ -import { membershipLevels } from "@/constants/membershipLevels" +import { MembershipLevel } from "@/constants/membershipLevels" -export type MembershipLevelProps = { - level: membershipLevels -} +import { LevelProps } from "@/components/Levels/levels" + +export type MembershipLevelIconProps = { + level: MembershipLevel +} & LevelProps export type CopyButtonProps = { membershipNumber: string diff --git a/types/components/overviewTable.ts b/types/components/overviewTable.ts index 5cb049523..2d9b6bceb 100644 --- a/types/components/overviewTable.ts +++ b/types/components/overviewTable.ts @@ -1,69 +1,36 @@ -import { Lang } from "@/constants/languages" -import { membershipLevels } from "@/constants/membershipLevels" - -import { MembershipLevel } from "@/utils/user" - -import type { IntlFormatters } from "@formatjs/intl" - -type BenefitTitle = { title: string } +import { MembershipLevel } from "@/constants/membershipLevels" +import { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output" +import { Reward } from "@/server/routers/contentstack/reward/output" export type OverviewTableClientProps = { activeMembership: MembershipLevel | null -} - -export type Level = { - level: membershipLevels - name: string - requiredPoints: number - requiredNights?: number - benefits: BenefitTitle[] + levels: ComparisonLevel[] } export type LevelCardProps = { - formatMessage: IntlFormatters["formatMessage"] - lang: Lang - level: Level + level: LevelWithRewards } -export type ComparisonLevel = { - level: membershipLevels - name: string - description: string - requirement: string - icon: string - benefits: Benefit[] -} +export type LevelWithRewards = LoyaltyLevel & { rewards: Reward[] } -export type Benefit = { - name: string - description: string - unlocked: boolean - value?: string - valueDetails?: string -} +export type ComparisonLevel = LevelWithRewards export type LevelSummaryProps = { level: ComparisonLevel showDescription?: boolean } -export type BenefitCardProps = { - comparedValues: BenefitValueInformation[] +export type RewardCardProps = { + comparedValues: (Reward | undefined)[] title: string description: string } -type BenefitValueInformation = { - unlocked: boolean - value?: string - valueDetails?: string +export type RewardValueProps = { + reward?: Reward } -export type BenefitValueProps = { - benefit: BenefitValueInformation -} - -export type BenefitListProps = { +export type RewardListProps = { levels: ComparisonLevel[] } @@ -77,16 +44,16 @@ export type DesktopSelectColumns = { export type LargeTableProps = { levels: ComparisonLevel[] - activeLevel: membershipLevels | null + activeLevel: MembershipLevel | null Select?: (column: DesktopSelectColumns) => JSX.Element | null } -export type BenefitTableHeaderProps = { +export type RewardTableHeaderProps = { name: string description: string } -export enum overviewTableActionsEnum { +export enum OverviewTableActionsEnum { SET_SELECTED_LEVEL_A_MOBILE = "SET_SELECTED_LEVEL_A_MOBILE", SET_SELECTED_LEVEL_B_MOBILE = "SET_SELECTED_LEVEL_B_MOBILE", SET_SELECTED_LEVEL_A_DESKTOP = "SET_SELECTED_LEVEL_A_DESKTOP", @@ -95,6 +62,6 @@ export enum overviewTableActionsEnum { } export type OverviewTableReducerAction = { - type: overviewTableActionsEnum + type: OverviewTableActionsEnum payload: ComparisonLevel } diff --git a/utils/generateTag.ts b/utils/generateTag.ts index c74dd08c1..08f2b404a 100644 --- a/utils/generateTag.ts +++ b/utils/generateTag.ts @@ -83,3 +83,19 @@ export function generateTagsFromSystem( ) }) } + +/** + * Function to generate tags for loyalty configuration models + * + * @param lang Lang + * @param contentTypeUid content_type_uid of reference + * @param id system shared identifier, e.g reward_id, level_id + * @returns string + */ +export function generateLoyaltyConfigTag( + lang: Lang, + contentTypeUid: string, + id: string +) { + return `${lang}:loyalty_config:${contentTypeUid}:${id}` +} diff --git a/utils/loyaltyTable.ts b/utils/loyaltyTable.ts index bd5e56bc7..9f516b03e 100644 --- a/utils/loyaltyTable.ts +++ b/utils/loyaltyTable.ts @@ -1,18 +1,50 @@ -import type { Benefit, ComparisonLevel } from "@/types/components/overviewTable" +import { Reward } from "@/server/routers/contentstack/reward/output" -export function getUnlockedBenefits(levels: ComparisonLevel[]) { - const allBenefits = levels +import type { ComparisonLevel } from "@/types/components/overviewTable" + +export function getGroupedRewards(levels: ComparisonLevel[]) { + const allRewards = levels .map((level) => { - return level.benefits.filter((benefit) => benefit.unlocked) + return level.rewards }) .flat() - /* Remove duplicate benefits based on the name property */ - return Array.from( - new Map(allBenefits.map((benefit) => [benefit.name, benefit])).values() + const mappedRewards = allRewards.reduce>( + (acc, curr) => { + const taxonomiTerm = curr.taxonomies.find((tax) => tax.term_uid)?.term_uid + + if (taxonomiTerm) { + if (!acc[taxonomiTerm]) { + acc[taxonomiTerm] = [] + } + acc[taxonomiTerm].push(curr) + } else { + if (!acc[curr.reward_id]) { + acc[curr.reward_id] = [] + } + acc[curr.reward_id].push(curr) + } + return acc + }, + {} ) + + return mappedRewards } -export function findBenefit(benefit: Benefit, level: ComparisonLevel) { - return level.benefits.find((b) => b.name === benefit.name) as Benefit +export function findAvailableRewards( + allRewardIds: string[], + level: ComparisonLevel +) { + return level.rewards.find((r) => allRewardIds.includes(r.reward_id)) +} + +export function getGroupedLabelAndDescription(rewards: Reward[]) { + const reward = rewards.find( + (reward) => !!(reward.grouped_label && reward.grouped_label) + ) + return { + label: reward?.grouped_label ?? "", + description: reward?.grouped_description ?? "", + } } diff --git a/utils/membershipLevel.ts b/utils/membershipLevel.ts deleted file mode 100644 index 708502d58..000000000 --- a/utils/membershipLevel.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Lang } from "@/constants/languages" -import { - MembershipLevelEnum, - membershipLevels, -} from "@/constants/membershipLevels" - -import levelsData from "@/data/loyaltyLevels" - -export function getMembershipLevelObject( - membershipLevel: MembershipLevelEnum, - lang: Lang -) { - return levelsData[lang].levels.find( - (level) => level.level === membershipLevels[membershipLevel] - ) -} diff --git a/utils/user.ts b/utils/user.ts index 66ddea937..c0f2561a7 100644 --- a/utils/user.ts +++ b/utils/user.ts @@ -1,9 +1,12 @@ import { z } from "zod" -import { MembershipLevelEnum } from "@/constants/membershipLevels" +import { + MembershipLevel, + MembershipLevelEnum, +} from "@/constants/membershipLevels" import { getMembershipCardsSchema } from "@/server/routers/user/output" -import type { Memberships, User } from "@/types/user" +import type { Membership, Memberships, User } from "@/types/user" enum scandicMemberships { guestpr = "guestpr", @@ -14,9 +17,16 @@ export function getMembership(memberships: Memberships) { return memberships?.find( (membership) => membership.membershipType.toLowerCase() === scandicMemberships.guestpr - ) + ) as FriendsMembership | undefined +} + +export type FriendsMembership = Omit< + NonNullable, + "membershipLevel" | "nextLevel" +> & { + membershipLevel: MembershipLevel + nextLevel: MembershipLevel } -export type MembershipLevel = ReturnType export function getMembershipCards( memberships: z.infer @@ -31,7 +41,7 @@ export function getMembershipCards( } export function isHighestMembership( - membershipLevel: MembershipLevelEnum | undefined + membershipLevel: MembershipLevel | undefined ) { return membershipLevel == MembershipLevelEnum.L7 } @@ -45,3 +55,15 @@ export function getInitials( const lastInitial = lastName.charAt(0).toUpperCase() return `${firstInitial}${lastInitial}` } + +export function getSteppedUpLevel( + currentValue: MembershipLevel, + stepsUp: number +): MembershipLevel { + const values = Object.values(MembershipLevelEnum) + const currentIndex = values.indexOf(currentValue as MembershipLevelEnum) + if (currentIndex === -1 || currentIndex === values.length - 1) { + return currentValue + } + return values[currentIndex + stepsUp] +} From 2a5a3126fef33cf2bd57eef2c91a276096684fbe Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Thu, 26 Sep 2024 11:51:48 +0200 Subject: [PATCH 14/31] fix: refactor OverviewTableClient --- app/api/web/revalidate/loyaltyConfig/route.ts | 17 +--- .../DynamicContent/OverviewTable/Client.tsx | 88 +---------------- .../OverviewTable/LargeTable/index.tsx | 49 ++++++---- .../OverviewTable/RewardList/index.tsx | 4 +- .../DynamicContent/OverviewTable/reducer.ts | 95 +++++++++++++++++++ .../Rewards/CurrentLevel/Client.tsx | 24 +++-- server/routers/contentstack/reward/input.ts | 2 +- 7 files changed, 146 insertions(+), 133 deletions(-) create mode 100644 components/Blocks/DynamicContent/OverviewTable/reducer.ts diff --git a/app/api/web/revalidate/loyaltyConfig/route.ts b/app/api/web/revalidate/loyaltyConfig/route.ts index da036cbd6..d7b770bf9 100644 --- a/app/api/web/revalidate/loyaltyConfig/route.ts +++ b/app/api/web/revalidate/loyaltyConfig/route.ts @@ -5,7 +5,7 @@ import { z } from "zod" import { Lang } from "@/constants/languages" import { env } from "@/env/server" -import { internalServerError } from "@/server/errors/next" +import { badRequest, internalServerError, notFound } from "@/server/errors/next" import { generateLoyaltyConfigTag } from "@/utils/generateTag" @@ -37,15 +37,7 @@ export async function POST(request: NextRequest) { if (secret !== env.REVALIDATE_SECRET) { console.error(`Invalid Secret`) console.error({ secret }) - return Response.json( - { - now: Date.now(), - revalidated: false, - }, - { - status: 400, - } - ) + return badRequest({ revalidated: false, now: Date.now() }) } const data = await request.json() @@ -85,10 +77,7 @@ export async function POST(request: NextRequest) { ) } else { console.error("Invalid content_type") - return Response.json( - { revalidated: false, now: Date.now() }, - { status: 404 } - ) + return notFound({ revalidated: false, now: Date.now() }) } console.info(`Revalidating loyalty config tag: ${tag}`) diff --git a/components/Blocks/DynamicContent/OverviewTable/Client.tsx b/components/Blocks/DynamicContent/OverviewTable/Client.tsx index b7d82badf..1754fce72 100644 --- a/components/Blocks/DynamicContent/OverviewTable/Client.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/Client.tsx @@ -10,10 +10,10 @@ import { import MembershipLevelIcon from "@/components/Levels/Icon" import Select from "@/components/TempDesignSystem/Select" -import { getSteppedUpLevel } from "@/utils/user" import LargeTable from "./LargeTable" import LevelSummary from "./LevelSummary" +import { getInitialState, getLevel, reducer } from "./reducer" import RewardList from "./RewardList" import YourLevel from "./YourLevelScript" @@ -24,11 +24,9 @@ import type { Key } from "react-aria-components" import { ComparisonLevel, DesktopSelectColumns, - LevelWithRewards, type MobileColumnHeaderProps, OverviewTableActionsEnum, type OverviewTableClientProps, - OverviewTableReducerAction, } from "@/types/components/overviewTable" function getLevelNamesForSelect(level: MembershipLevel, levelName: string) { @@ -36,89 +34,7 @@ function getLevelNamesForSelect(level: MembershipLevel, levelName: string) { return [levelToNumber, levelName].join(" - ") } -function getLevel( - membershipLevel: MembershipLevel, - levels: LevelWithRewards[] -) { - return levels.find((level) => level.level_id === membershipLevel)! -} - -function getInitialState({ - activeMembership, - levels, -}: OverviewTableClientProps) { - if (!activeMembership) { - return { - selectedLevelAMobile: getLevel(MembershipLevelEnum.L1, levels), - selectedLevelBMobile: getLevel(MembershipLevelEnum.L2, levels), - selectedLevelADesktop: getLevel(MembershipLevelEnum.L1, levels), - selectedLevelBDesktop: getLevel(MembershipLevelEnum.L2, levels), - selectedLevelCDesktop: getLevel(MembershipLevelEnum.L3, levels), - } - } - const level = MembershipLevelEnum[activeMembership] - - switch (level) { - case MembershipLevelEnum.L6: - return { - selectedLevelAMobile: getLevel(MembershipLevelEnum.L6, levels), - selectedLevelBMobile: getLevel(MembershipLevelEnum.L7, levels), - selectedLevelADesktop: getLevel(MembershipLevelEnum.L5, levels), - selectedLevelBDesktop: getLevel(MembershipLevelEnum.L6, levels), - selectedLevelCDesktop: getLevel(MembershipLevelEnum.L7, levels), - } - case MembershipLevelEnum.L7: - return { - selectedLevelAMobile: getLevel(MembershipLevelEnum.L6, levels), - selectedLevelBMobile: getLevel(MembershipLevelEnum.L7, levels), - selectedLevelADesktop: getLevel(MembershipLevelEnum.L6, levels), - selectedLevelBDesktop: getLevel(MembershipLevelEnum.L7, levels), - selectedLevelCDesktop: getLevel(MembershipLevelEnum.L1, levels), - } - default: - return { - selectedLevelAMobile: getLevel(level, levels), - selectedLevelBMobile: getLevel(getSteppedUpLevel(level, 1), levels), - selectedLevelADesktop: getLevel(level, levels), - selectedLevelBDesktop: getLevel(getSteppedUpLevel(level, 1), levels), - selectedLevelCDesktop: getLevel(getSteppedUpLevel(level, 2), levels), - } - } -} - -function reducer(state: any, action: OverviewTableReducerAction) { - switch (action.type) { - case OverviewTableActionsEnum.SET_SELECTED_LEVEL_A_MOBILE: - return { - ...state, - selectedLevelAMobile: action.payload, - } - case OverviewTableActionsEnum.SET_SELECTED_LEVEL_B_MOBILE: - return { - ...state, - selectedLevelBMobile: action.payload, - } - case OverviewTableActionsEnum.SET_SELECTED_LEVEL_A_DESKTOP: - return { - ...state, - selectedLevelADesktop: action.payload, - } - case OverviewTableActionsEnum.SET_SELECTED_LEVEL_B_DESKTOP: - return { - ...state, - selectedLevelBDesktop: action.payload, - } - case OverviewTableActionsEnum.SET_SELECTED_LEVEL_C_DESKTOP: - return { - ...state, - selectedLevelCDesktop: action.payload, - } - default: - return state - } -} - -export default function OverviewTable({ +export default function OverviewTableClient({ activeMembership, levels, }: OverviewTableClientProps) { diff --git a/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx b/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx index 1b1780787..f19b42444 100644 --- a/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx @@ -22,7 +22,7 @@ export default function LargeTable({ activeLevel, Select, }: LargeTableProps) { - const groupedRewards = getGroupedRewards(levels) + const keyedGroupedRewards = getGroupedRewards(levels) return (
- +
+ - + +
@@ -32,27 +32,34 @@ export default function LargeTable({ Select={Select} /> - {Object.entries(groupedRewards).map(([key, groupedRewards], idx) => { - const { label, description } = - getGroupedLabelAndDescription(groupedRewards) + {Object.entries(keyedGroupedRewards).map( + ([key, groupedRewards], idx) => { + const { label, description } = + getGroupedLabelAndDescription(groupedRewards) - return ( - - - {levels.map((level, idx) => { - const rewardIdsInGroup = groupedRewards.map((b) => b.reward_id) - const reward = findAvailableRewards(rewardIdsInGroup, level) - return ( - - ) - })} - - ) - })} + return ( + + + {levels.map((level, idx) => { + const rewardIdsInGroup = groupedRewards.map( + (b) => b.reward_id + ) + const reward = findAvailableRewards(rewardIdsInGroup, level) + return ( + + ) + })} + + ) + } + )}
- - - -
+ + + +
) diff --git a/components/Blocks/DynamicContent/OverviewTable/RewardList/index.tsx b/components/Blocks/DynamicContent/OverviewTable/RewardList/index.tsx index b0a72a3bd..fcfad53ba 100644 --- a/components/Blocks/DynamicContent/OverviewTable/RewardList/index.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/RewardList/index.tsx @@ -11,9 +11,9 @@ import styles from "./rewardList.module.css" import type { RewardListProps } from "@/types/components/overviewTable" export default function RewardList({ levels }: RewardListProps) { - const groupedRewards = getGroupedRewards(levels) + const keyedGroupedRewards = getGroupedRewards(levels) - return Object.values(groupedRewards).map((groupedRewards) => { + return Object.values(keyedGroupedRewards).map((groupedRewards) => { const rewardIdsInGroup = groupedRewards.map((b) => b.reward_id) const { label, description } = getGroupedLabelAndDescription(groupedRewards) diff --git a/components/Blocks/DynamicContent/OverviewTable/reducer.ts b/components/Blocks/DynamicContent/OverviewTable/reducer.ts new file mode 100644 index 000000000..b2fd7372d --- /dev/null +++ b/components/Blocks/DynamicContent/OverviewTable/reducer.ts @@ -0,0 +1,95 @@ +import { + type MembershipLevel, + MembershipLevelEnum, +} from "@/constants/membershipLevels" + +import { getSteppedUpLevel } from "@/utils/user" + +import { + type LevelWithRewards, + OverviewTableActionsEnum, + type OverviewTableClientProps, + OverviewTableReducerAction, +} from "@/types/components/overviewTable" + +export function getLevel( + membershipLevel: MembershipLevel, + levels: LevelWithRewards[] +) { + return levels.find((level) => level.level_id === membershipLevel)! +} + +export function getInitialState({ + activeMembership, + levels, +}: OverviewTableClientProps) { + if (!activeMembership) { + return { + selectedLevelAMobile: getLevel(MembershipLevelEnum.L1, levels), + selectedLevelBMobile: getLevel(MembershipLevelEnum.L2, levels), + selectedLevelADesktop: getLevel(MembershipLevelEnum.L1, levels), + selectedLevelBDesktop: getLevel(MembershipLevelEnum.L2, levels), + selectedLevelCDesktop: getLevel(MembershipLevelEnum.L3, levels), + } + } + const level = MembershipLevelEnum[activeMembership] + + switch (level) { + case MembershipLevelEnum.L6: + return { + selectedLevelAMobile: getLevel(MembershipLevelEnum.L6, levels), + selectedLevelBMobile: getLevel(MembershipLevelEnum.L7, levels), + selectedLevelADesktop: getLevel(MembershipLevelEnum.L5, levels), + selectedLevelBDesktop: getLevel(MembershipLevelEnum.L6, levels), + selectedLevelCDesktop: getLevel(MembershipLevelEnum.L7, levels), + } + case MembershipLevelEnum.L7: + return { + selectedLevelAMobile: getLevel(MembershipLevelEnum.L6, levels), + selectedLevelBMobile: getLevel(MembershipLevelEnum.L7, levels), + selectedLevelADesktop: getLevel(MembershipLevelEnum.L6, levels), + selectedLevelBDesktop: getLevel(MembershipLevelEnum.L7, levels), + selectedLevelCDesktop: getLevel(MembershipLevelEnum.L1, levels), + } + default: + return { + selectedLevelAMobile: getLevel(level, levels), + selectedLevelBMobile: getLevel(getSteppedUpLevel(level, 1), levels), + selectedLevelADesktop: getLevel(level, levels), + selectedLevelBDesktop: getLevel(getSteppedUpLevel(level, 1), levels), + selectedLevelCDesktop: getLevel(getSteppedUpLevel(level, 2), levels), + } + } +} + +export function reducer(state: any, action: OverviewTableReducerAction) { + switch (action.type) { + case OverviewTableActionsEnum.SET_SELECTED_LEVEL_A_MOBILE: + return { + ...state, + selectedLevelAMobile: action.payload, + } + case OverviewTableActionsEnum.SET_SELECTED_LEVEL_B_MOBILE: + return { + ...state, + selectedLevelBMobile: action.payload, + } + case OverviewTableActionsEnum.SET_SELECTED_LEVEL_A_DESKTOP: + return { + ...state, + selectedLevelADesktop: action.payload, + } + case OverviewTableActionsEnum.SET_SELECTED_LEVEL_B_DESKTOP: + return { + ...state, + selectedLevelBDesktop: action.payload, + } + case OverviewTableActionsEnum.SET_SELECTED_LEVEL_C_DESKTOP: + return { + ...state, + selectedLevelCDesktop: action.payload, + } + default: + return state + } +} diff --git a/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx b/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx index 471a39ede..a57e5e160 100644 --- a/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx +++ b/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx @@ -40,9 +40,16 @@ export default function ClientCurrentRewards({ const filteredRewards = data?.pages.filter((page) => page && page.rewards) ?? [] const rewards = filteredRewards.flatMap((page) => page?.rewards) as Reward[] - return isLoading ? ( - - ) : rewards.length ? ( + + if (isLoading) { + return + } + + if (!rewards.length) { + return null + } + + return (
{rewards.map((reward, idx) => ( @@ -58,13 +65,12 @@ export default function ClientCurrentRewards({
))}
- {hasNextPage ? ( - isFetching ? ( + {hasNextPage && + (isFetching ? ( ) : ( - - ) - ) : null} + + ))} - ) : null + ) } diff --git a/server/routers/contentstack/reward/input.ts b/server/routers/contentstack/reward/input.ts index 43da34bed..bf02e9255 100644 --- a/server/routers/contentstack/reward/input.ts +++ b/server/routers/contentstack/reward/input.ts @@ -13,7 +13,7 @@ export const rewardsAllInput = z .default({ unique: false }) export const rewardsCurrentInput = z.object({ - limit: z.number().min(0).default(3), + limit: z.number().min(1).default(3), cursor: z.number().optional().default(0), lang: z.nativeEnum(Lang).optional(), }) From f9e8d95b25c810d047f53bd6217af3b24cd1d773 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 26 Sep 2024 12:12:17 +0200 Subject: [PATCH 15/31] feat(sw-217): removed optional --- server/routers/contentstack/schemas/blocks/cardsGrid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/contentstack/schemas/blocks/cardsGrid.ts b/server/routers/contentstack/schemas/blocks/cardsGrid.ts index a73eac737..5f1b9af5c 100644 --- a/server/routers/contentstack/schemas/blocks/cardsGrid.ts +++ b/server/routers/contentstack/schemas/blocks/cardsGrid.ts @@ -48,7 +48,7 @@ export const teaserCardBlockSchema = z.object({ secondary_button: buttonSchema, has_primary_button: z.boolean().default(false), has_secondary_button: z.boolean().default(false), - has_sidepeek_button: z.boolean().optional().default(false), + has_sidepeek_button: z.boolean().default(false), side_peek_button: z .object({ title: z.string().optional().default(""), From b9aeb8ff30799ada8239b5c30200f5499c5a2463 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Fri, 20 Sep 2024 16:18:16 +0200 Subject: [PATCH 16/31] fix(SW-135): invalidate router cache on logout for my pages --- app/[lang]/(live)/(protected)/logout/route.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/[lang]/(live)/(protected)/logout/route.ts b/app/[lang]/(live)/(protected)/logout/route.ts index 2bb2f0a52..002e0dca6 100644 --- a/app/[lang]/(live)/(protected)/logout/route.ts +++ b/app/[lang]/(live)/(protected)/logout/route.ts @@ -1,3 +1,4 @@ +import { revalidatePath } from "next/cache" import { NextRequest, NextResponse } from "next/server" import { AuthError } from "next-auth" @@ -94,6 +95,9 @@ export async function GET( redirect: false, }) + // Revalidate the router cache for my pages to make sure we don't show stale user data + revalidatePath("/[lang]/my-pages", "layout") + if (redirectUrlObj) { console.log(`[logout] redirecting to: ${redirectUrlObj.redirect}`) return NextResponse.redirect(redirectUrlObj.redirect) From 943184c50c2cb975b9e25e08c15903a72a0a60cb Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Thu, 26 Sep 2024 11:31:39 +0200 Subject: [PATCH 17/31] fix: reload window on logout --- app/[lang]/(live)/(protected)/(.)logout/page.tsx | 14 ++++++++++++++ app/[lang]/(live)/(protected)/logout/route.ts | 4 ---- 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 app/[lang]/(live)/(protected)/(.)logout/page.tsx diff --git a/app/[lang]/(live)/(protected)/(.)logout/page.tsx b/app/[lang]/(live)/(protected)/(.)logout/page.tsx new file mode 100644 index 000000000..c4884586f --- /dev/null +++ b/app/[lang]/(live)/(protected)/(.)logout/page.tsx @@ -0,0 +1,14 @@ +"use client" + +import { useEffect } from "react" + +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LogoutInterceptedRoute() { + // Reload the browser on logout in order to flush router cache. This is to make sure we don't show stale user specific data. + useEffect(() => { + window.location.reload() + }, []) + + return +} diff --git a/app/[lang]/(live)/(protected)/logout/route.ts b/app/[lang]/(live)/(protected)/logout/route.ts index 002e0dca6..2bb2f0a52 100644 --- a/app/[lang]/(live)/(protected)/logout/route.ts +++ b/app/[lang]/(live)/(protected)/logout/route.ts @@ -1,4 +1,3 @@ -import { revalidatePath } from "next/cache" import { NextRequest, NextResponse } from "next/server" import { AuthError } from "next-auth" @@ -95,9 +94,6 @@ export async function GET( redirect: false, }) - // Revalidate the router cache for my pages to make sure we don't show stale user data - revalidatePath("/[lang]/my-pages", "layout") - if (redirectUrlObj) { console.log(`[logout] redirecting to: ${redirectUrlObj.redirect}`) return NextResponse.redirect(redirectUrlObj.redirect) From af47e9eb18cb83eb707b5b6339219f01769e1a72 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 26 Sep 2024 15:36:07 +0200 Subject: [PATCH 18/31] fix/update hotel data output --- server/routers/hotels/output.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 8ad890049..27d4790ad 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -153,10 +153,8 @@ const detailedFacilitySchema = z.object({ id: z.number(), name: z.string(), code: z.string().optional(), - applyToAllHotels: z.boolean(), + applyToAllHotels: z.boolean().default(false), public: z.boolean(), - icon: z.string(), - iconName: z.string().optional(), sortOrder: z.number(), }) From 3a88b35a776492a370165382027f319264bb1710 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 26 Sep 2024 15:52:10 +0200 Subject: [PATCH 19/31] fix/add filter --- server/routers/hotels/output.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 27d4790ad..82a51754b 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -152,10 +152,9 @@ const hotelContentSchema = z.object({ const detailedFacilitySchema = z.object({ id: z.number(), name: z.string(), - code: z.string().optional(), - applyToAllHotels: z.boolean().default(false), public: z.boolean(), sortOrder: z.number(), + filter: z.string().optional(), }) const healthFacilitySchema = z.object({ From bb93d488bb7bb2c4ad8304655000991d817f5cbe Mon Sep 17 00:00:00 2001 From: Michael Zetterberg Date: Thu, 29 Aug 2024 14:29:58 +0200 Subject: [PATCH 20/31] fix: break the switch statement for proper logs --- app/[lang]/(live)/(protected)/layout.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/[lang]/(live)/(protected)/layout.tsx b/app/[lang]/(live)/(protected)/layout.tsx index 9273fda74..c6e4a0a51 100644 --- a/app/[lang]/(live)/(protected)/layout.tsx +++ b/app/[lang]/(live)/(protected)/layout.tsx @@ -46,10 +46,13 @@ export default async function ProtectedLayout({ redirect(redirectURL) case "notfound": console.error(`[layout:protected] notfound user loading error`) + break case "unknown": console.error(`[layout:protected] unknown user loading error`) + break default: console.error(`[layout:protected] unhandled user loading error`) + break } return

Something went wrong!

} From 8f351491f902b97d934d05a7c36a71d3cacd7077 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 27 Sep 2024 09:05:52 +0200 Subject: [PATCH 21/31] feat(SW-217): updated path --- components/Blocks/CardsGrid.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/Blocks/CardsGrid.tsx b/components/Blocks/CardsGrid.tsx index 839690de8..a55e43b94 100644 --- a/components/Blocks/CardsGrid.tsx +++ b/components/Blocks/CardsGrid.tsx @@ -3,8 +3,7 @@ import SectionHeader from "@/components/Section/Header" import Card from "@/components/TempDesignSystem/Card" import Grids from "@/components/TempDesignSystem/Grids" import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard" - -import TeaserCard from "../TempDesignSystem/TeaserCard" +import TeaserCard from "@/components/TempDesignSystem/TeaserCard" import type { CardsGridProps } from "@/types/components/blocks/cardsGrid" import { CardsGridEnum } from "@/types/enums/cardsGrid" From 9fcf362587049047c7759ebcc1e6d54ec395ab14 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Fri, 20 Sep 2024 14:27:20 +0200 Subject: [PATCH 22/31] feat(SW-272) added mega menu --- .../MyPagesMenu/myPagesMenu.module.css | 17 --- .../NavigationMenuItem/index.tsx | 112 ++++++++++++++---- .../navigationMenuItem.module.css | 105 ++++++++++++++++ .../TempDesignSystem/Card/card.module.css | 38 ++++-- components/TempDesignSystem/Card/card.ts | 10 +- components/TempDesignSystem/Card/index.tsx | 22 ++-- stores/main-menu.ts | 58 +++++++++ types/components/dropdown/dropdown.ts | 3 + 8 files changed, 307 insertions(+), 58 deletions(-) diff --git a/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css index 39002b177..02d097654 100644 --- a/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css +++ b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css @@ -26,21 +26,4 @@ min-width: 20rem; z-index: var(--menu-overlay-z-index); } - - /* Triangle above dropdown */ - .dropdown::before { - content: ""; - position: absolute; - top: -1.25rem; - right: 2.4rem; - transform: rotate(180deg); - border-width: 0.75rem; - border-style: solid; - border-color: var(--Base-Surface-Primary-light-Normal) transparent - transparent transparent; - } - - .dropdown.isExpanded { - display: block; - } } diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx index 453a02b46..5b1b2c80a 100644 --- a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx @@ -1,9 +1,16 @@ "use client" -import { useState } from "react" +import useDropdownStore from "@/stores/main-menu" -import { ChevronDownIcon, ChevronRightIcon } from "@/components/Icons" +import { + ArrowRightIcon, + ChevronDownIcon, + ChevronRightIcon, +} from "@/components/Icons" +import Card from "@/components/TempDesignSystem/Card" import Link from "@/components/TempDesignSystem/Link" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import MainMenuButton from "../../MainMenuButton" @@ -12,28 +19,93 @@ import styles from "./navigationMenuItem.module.css" import type { NavigationMenuItemProps } from "@/types/components/header/navigationMenuItem" export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) { - const { submenu, title, link } = item - const [isExpanded, setIsExpanded] = useState(false) // TODO: Use store to manage this state when adding the menu itself. + const { openMegaMenu, toggleMegaMenu } = useDropdownStore() + const { submenu, title, link, seeAllLink, card } = item + const isMegaMenuOpen = openMegaMenu === title - function handleButtonClick() { - setIsExpanded((prev) => !prev) + useHandleKeyUp((event: KeyboardEvent) => { + if (event.key === "Escape" && isMegaMenuOpen) { + toggleMegaMenu(false) + } + }) + + function handleLinkClick() { + toggleMegaMenu(false) } return submenu.length ? ( - - {title} - {isMobile ? ( - - ) : ( - - )} - + + toggleMegaMenu(title)} + className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : styles.desktop}`} + > + {title} + {isMobile ? ( + + ) : ( + + )} + + {isMegaMenuOpen ? ( + + ) : null} + ) : ( void + onSecondaryButtonClick?: () => void } diff --git a/components/TempDesignSystem/Card/index.tsx b/components/TempDesignSystem/Card/index.tsx index d611c784a..c4f414359 100644 --- a/components/TempDesignSystem/Card/index.tsx +++ b/components/TempDesignSystem/Card/index.tsx @@ -21,24 +21,28 @@ export default function Card({ className, theme, backgroundImage, + onPrimaryButtonClick, + onSecondaryButtonClick, }: CardProps) { const { buttonTheme, primaryLinkColor, secondaryLinkColor } = getTheme(theme) return (
{backgroundImage && ( - {backgroundImage.meta.alt +
+ {backgroundImage.meta.alt +
)}
{scriptedTopTitle ? ( @@ -73,6 +77,7 @@ export default function Card({ href={primaryButton.href} target={primaryButton.openInNewTab ? "_blank" : undefined} color={primaryLinkColor} + onClick={onPrimaryButtonClick} > {primaryButton.title} @@ -90,6 +95,7 @@ export default function Card({ href={secondaryButton.href} target={secondaryButton.openInNewTab ? "_blank" : undefined} color={secondaryLinkColor} + onClick={onSecondaryButtonClick} > {secondaryButton.title} diff --git a/stores/main-menu.ts b/stores/main-menu.ts index 4ccbcb290..cae5320b5 100644 --- a/stores/main-menu.ts +++ b/stores/main-menu.ts @@ -16,6 +16,7 @@ const useDropdownStore = create((set, get) => ({ isHeaderLanguageSwitcherOpen: false, isHeaderLanguageSwitcherMobileOpen: false, isFooterLanguageSwitcherOpen: false, + openMegaMenu: false, handleHamburgerClick: () => { const state = get() if (state.isMyPagesMobileMenuOpen) { @@ -31,9 +32,27 @@ const useDropdownStore = create((set, get) => ({ } } }, + toggleMegaMenu: (menu: string | false) => + set( + produce((state: DropdownState) => { + if (state.openMegaMenu === menu) { + state.openMegaMenu = false + } else { + state.openMegaMenu = menu + } + state.isHamburgerMenuOpen = false + state.isMyPagesMobileMenuOpen = false + state.isMyPagesMenuOpen = false + state.isHeaderLanguageSwitcherOpen = false + state.isHeaderLanguageSwitcherMobileOpen = false + state.isFooterLanguageSwitcherOpen = false + }) + ), toggleDropdown: (dropdown: DropdownTypeEnum) => set( produce((state: DropdownState) => { + state.openMegaMenu = false + switch (dropdown) { case DropdownTypeEnum.HamburgerMenu: state.isHamburgerMenuOpen = !state.isHamburgerMenuOpen @@ -97,3 +116,42 @@ const useDropdownStore = create((set, get) => ({ })) export default useDropdownStore + +const error = [ + { + query: { + hotelId: "698", + params: { hotelId: "698", language: "En", include: "RoomCategories" }, + }, + }, + { + query: { lang: "en" }, + error: { + issues: [ + { + code: "invalid_type", + expected: "object", + received: "undefined", + path: [ + "all_header", + "items", + 0, + "menu_items", + 0, + "submenu", + 0, + "links", + 0, + "linkConnection", + "edges", + 0, + "node", + "web", + ], + message: "Required", + }, + ], + name: "ZodError", + }, + }, +] diff --git a/types/components/dropdown/dropdown.ts b/types/components/dropdown/dropdown.ts index 899086336..e282e5ee1 100644 --- a/types/components/dropdown/dropdown.ts +++ b/types/components/dropdown/dropdown.ts @@ -5,6 +5,8 @@ export interface DropdownState { isHeaderLanguageSwitcherOpen: boolean isHeaderLanguageSwitcherMobileOpen: boolean isFooterLanguageSwitcherOpen: boolean + openMegaMenu: string | false + toggleMegaMenu: (menu: string | false) => void toggleDropdown: (dropdown: DropdownTypeEnum) => void handleHamburgerClick: () => void } @@ -16,6 +18,7 @@ export enum DropdownTypeEnum { HeaderLanguageSwitcher = "headerLanguageSwitcher", HeaderLanguageSwitcherMobile = "headerLanguageSwitcherMobile", FooterLanguageSwitcher = "footerLanguageSwitcher", + MegaMenu = "megaMenu", } export type DropdownType = `${DropdownTypeEnum}` From 34f762082537a6a99cd71e711c2436273b53b052 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Wed, 25 Sep 2024 14:45:08 +0200 Subject: [PATCH 23/31] feat(SW-272) implemented mobile design --- .../Header/MainMenu/MobileMenu/index.tsx | 18 +-- .../NavigationMenu/MegaMenu/index.tsx | 111 ++++++++++++++++ .../MegaMenu/megaMenu.module.css | 108 ++++++++++++++++ .../NavigationMenuItem/index.tsx | 80 +++--------- .../navigationMenuItem.module.css | 119 ++++-------------- components/TempDesignSystem/Link/variants.ts | 1 + stores/main-menu.ts | 90 ++++--------- types/components/dropdown/dropdown.ts | 2 - types/components/header/megaMenu.ts | 9 ++ 9 files changed, 306 insertions(+), 232 deletions(-) create mode 100644 components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx create mode 100644 components/Header/MainMenu/NavigationMenu/MegaMenu/megaMenu.module.css create mode 100644 types/components/header/megaMenu.ts diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index fc875202d..08c7a2d11 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -24,7 +24,6 @@ export default function MobileMenu({ }: React.PropsWithChildren) { const intl = useIntl() const { - handleHamburgerClick, toggleDropdown, isHamburgerMenuOpen, isMyPagesMobileMenuOpen, @@ -32,6 +31,12 @@ export default function MobileMenu({ isFooterLanguageSwitcherOpen, } = useDropdownStore() + const isHamburgerExtended = + isHamburgerMenuOpen || + isMyPagesMobileMenuOpen || + isHeaderLanguageSwitcherMobileOpen || + isFooterLanguageSwitcherOpen + useHandleKeyUp((event: KeyboardEvent) => { if (event.key === "Escape" && isHamburgerMenuOpen) { toggleDropdown(DropdownTypeEnum.HamburgerMenu) @@ -49,18 +54,15 @@ export default function MobileMenu({ <> - + { + if (event.key === "Escape" && isMegaMenuOpen) { + toggleMegaMenu(false) + } + }) + + function closeMegaMenu() { + toggleMegaMenu(false) + } + return ( + + ) +} diff --git a/components/Header/MainMenu/NavigationMenu/MegaMenu/megaMenu.module.css b/components/Header/MainMenu/NavigationMenu/MegaMenu/megaMenu.module.css new file mode 100644 index 000000000..6eb7abd2c --- /dev/null +++ b/components/Header/MainMenu/NavigationMenu/MegaMenu/megaMenu.module.css @@ -0,0 +1,108 @@ +.megaMenuContent { + display: grid; + /* align-content: start; */ +} + +.seeAllLink { + display: flex; + padding: var(--Spacing-x2) var(--Spacing-x3); + align-items: center; + gap: var(--Spacing-x1); + background-color: var(--Base-Surface-Secondary-light-Normal); +} + +.submenus { + list-style: none; + display: grid; + gap: var(--Spacing-x3); + padding: var(--Spacing-x2) var(--Spacing-x4); +} + +.submenu { + list-style: none; +} + +.submenuItem { + display: flex; +} + +.submenusItem { + display: grid; + gap: var(--Spacing-x1); + align-content: start; +} + +.link { + padding: var(--Spacing-x1) 0; + font-weight: var(--typography-Body-Bold-fontWeight); + width: 100%; +} + +.backWrapper { + padding: var(--Spacing-x2); +} + +.backButton { + background-color: transparent; + border: none; + padding: 0; + cursor: pointer; + display: flex; + align-items: center; + gap: var(--Spacing-x1); + width: 100%; +} + +@media screen and (max-width: 767px) { + .megaMenuContent { + gap: var(--Spacing-x2); + } + .submenusItem:first-child { + padding-bottom: var(--Spacing-x2); + border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); + } + + .cardWrapper { + background-color: var(--Base-Surface-Secondary-light-Normal); + padding: var(--Spacing-x4) var(--Spacing-x2); + } +} + +@media screen and (min-width: 768px) { + .megaMenuContent { + grid-template-rows: auto 1fr; + grid-template-areas: + "seeAllLink" + "submenus"; + width: 600px; + } + + .megaMenuContent:has(.cardWrapper) { + width: 900px; + grid-template-columns: repeat(3, 1fr); + grid-template-areas: + "seeAllLink seeAllLink card" + "submenus submenus card"; + } + + .seeAllLink { + grid-area: seeAllLink; + } + + .submenus { + grid-area: submenus; + grid-template-columns: repeat(2, 1fr); + } + + .submenusItem:first-child { + border-right: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); + } + + .cardWrapper { + grid-area: card; + } + + .cardWrapper .card { + border-radius: 0; + } +} diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx index 5b1b2c80a..e0ff99f4d 100644 --- a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx @@ -2,17 +2,12 @@ import useDropdownStore from "@/stores/main-menu" -import { - ArrowRightIcon, - ChevronDownIcon, - ChevronRightIcon, -} from "@/components/Icons" -import Card from "@/components/TempDesignSystem/Card" +import { ChevronDownIcon, ChevronRightIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" -import Caption from "@/components/TempDesignSystem/Text/Caption" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import MainMenuButton from "../../MainMenuButton" +import MegaMenu from "../MegaMenu" import styles from "./navigationMenuItem.module.css" @@ -29,7 +24,7 @@ export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) { } }) - function handleLinkClick() { + function closeMegaMenu() { toggleMegaMenu(false) } @@ -49,62 +44,19 @@ export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) { /> )} - {isMegaMenuOpen ? ( - - ) : null} +
+ {isMegaMenuOpen ? ( + + ) : null} +
) : ( ((set, get) => ({ isHamburgerMenuOpen: false, isMyPagesMobileMenuOpen: false, @@ -17,21 +14,6 @@ const useDropdownStore = create((set, get) => ({ isHeaderLanguageSwitcherMobileOpen: false, isFooterLanguageSwitcherOpen: false, openMegaMenu: false, - handleHamburgerClick: () => { - const state = get() - if (state.isMyPagesMobileMenuOpen) { - set({ isMyPagesMobileMenuOpen: false }) - } else { - if (state.isHeaderLanguageSwitcherMobileOpen) { - set({ isHeaderLanguageSwitcherMobileOpen: false }) - } - if (!state.isFooterLanguageSwitcherOpen) { - set({ isHamburgerMenuOpen: !state.isHamburgerMenuOpen }) - } else { - set({ isFooterLanguageSwitcherOpen: false }) - } - } - }, toggleMegaMenu: (menu: string | false) => set( produce((state: DropdownState) => { @@ -40,7 +22,6 @@ const useDropdownStore = create((set, get) => ({ } else { state.openMegaMenu = menu } - state.isHamburgerMenuOpen = false state.isMyPagesMobileMenuOpen = false state.isMyPagesMenuOpen = false state.isHeaderLanguageSwitcherOpen = false @@ -51,16 +32,28 @@ const useDropdownStore = create((set, get) => ({ toggleDropdown: (dropdown: DropdownTypeEnum) => set( produce((state: DropdownState) => { - state.openMegaMenu = false + const hamburgerShouldStayExpanded = + state.isMyPagesMenuOpen || + state.isHeaderLanguageSwitcherMobileOpen || + state.isFooterLanguageSwitcherOpen || + !!state.openMegaMenu switch (dropdown) { case DropdownTypeEnum.HamburgerMenu: - state.isHamburgerMenuOpen = !state.isHamburgerMenuOpen - state.isMyPagesMobileMenuOpen = false - state.isMyPagesMenuOpen = false - state.isHeaderLanguageSwitcherOpen = false - state.isHeaderLanguageSwitcherMobileOpen = false - state.isFooterLanguageSwitcherOpen = false + if (state.isHamburgerMenuOpen) { + if (hamburgerShouldStayExpanded) { + state.isMyPagesMobileMenuOpen = false + state.isHeaderLanguageSwitcherMobileOpen = false + state.isFooterLanguageSwitcherOpen = false + state.openMegaMenu = false + } else { + state.isHamburgerMenuOpen = false + } + } else if (state.isMyPagesMobileMenuOpen) { + state.isMyPagesMobileMenuOpen = false + } else { + state.isHamburgerMenuOpen = true + } break case DropdownTypeEnum.MyPagesMobileMenu: state.isMyPagesMobileMenuOpen = !state.isMyPagesMobileMenuOpen @@ -69,6 +62,7 @@ const useDropdownStore = create((set, get) => ({ state.isHeaderLanguageSwitcherOpen = false state.isHeaderLanguageSwitcherMobileOpen = false state.isFooterLanguageSwitcherOpen = false + state.openMegaMenu = false break case DropdownTypeEnum.MyPagesMenu: state.isMyPagesMenuOpen = !state.isMyPagesMenuOpen @@ -77,6 +71,7 @@ const useDropdownStore = create((set, get) => ({ state.isHeaderLanguageSwitcherOpen = false state.isHeaderLanguageSwitcherMobileOpen = false state.isFooterLanguageSwitcherOpen = false + state.openMegaMenu = false break case DropdownTypeEnum.HeaderLanguageSwitcher: state.isHeaderLanguageSwitcherOpen = @@ -86,15 +81,16 @@ const useDropdownStore = create((set, get) => ({ state.isMyPagesMenuOpen = false state.isHeaderLanguageSwitcherMobileOpen = false state.isFooterLanguageSwitcherOpen = false + state.openMegaMenu = false break case DropdownTypeEnum.HeaderLanguageSwitcherMobile: state.isHeaderLanguageSwitcherMobileOpen = !state.isHeaderLanguageSwitcherMobileOpen - state.isHamburgerMenuOpen = false state.isMyPagesMobileMenuOpen = false state.isMyPagesMenuOpen = false state.isHeaderLanguageSwitcherOpen = false state.isFooterLanguageSwitcherOpen = false + state.openMegaMenu = false break case DropdownTypeEnum.FooterLanguageSwitcher: state.isFooterLanguageSwitcherOpen = @@ -104,6 +100,7 @@ const useDropdownStore = create((set, get) => ({ state.isMyPagesMenuOpen = false state.isHeaderLanguageSwitcherOpen = false state.isHeaderLanguageSwitcherMobileOpen = false + state.openMegaMenu = false if (state.isFooterLanguageSwitcherOpen) { document.body.classList.add("overflow-hidden") } else { @@ -116,42 +113,3 @@ const useDropdownStore = create((set, get) => ({ })) export default useDropdownStore - -const error = [ - { - query: { - hotelId: "698", - params: { hotelId: "698", language: "En", include: "RoomCategories" }, - }, - }, - { - query: { lang: "en" }, - error: { - issues: [ - { - code: "invalid_type", - expected: "object", - received: "undefined", - path: [ - "all_header", - "items", - 0, - "menu_items", - 0, - "submenu", - 0, - "links", - 0, - "linkConnection", - "edges", - 0, - "node", - "web", - ], - message: "Required", - }, - ], - name: "ZodError", - }, - }, -] diff --git a/types/components/dropdown/dropdown.ts b/types/components/dropdown/dropdown.ts index e282e5ee1..368ffcc43 100644 --- a/types/components/dropdown/dropdown.ts +++ b/types/components/dropdown/dropdown.ts @@ -8,7 +8,6 @@ export interface DropdownState { openMegaMenu: string | false toggleMegaMenu: (menu: string | false) => void toggleDropdown: (dropdown: DropdownTypeEnum) => void - handleHamburgerClick: () => void } export enum DropdownTypeEnum { @@ -18,7 +17,6 @@ export enum DropdownTypeEnum { HeaderLanguageSwitcher = "headerLanguageSwitcher", HeaderLanguageSwitcherMobile = "headerLanguageSwitcherMobile", FooterLanguageSwitcher = "footerLanguageSwitcher", - MegaMenu = "megaMenu", } export type DropdownType = `${DropdownTypeEnum}` diff --git a/types/components/header/megaMenu.ts b/types/components/header/megaMenu.ts new file mode 100644 index 000000000..8eab851f3 --- /dev/null +++ b/types/components/header/megaMenu.ts @@ -0,0 +1,9 @@ +import type { MenuItem } from "@/types/trpc/routers/contentstack/header" + +export interface MegaMenuProps { + title: MenuItem["title"] + seeAllLink: MenuItem["seeAllLink"] + submenu: MenuItem["submenu"] + card: MenuItem["card"] + isMobile: boolean +} From 44d201c8a16dbe35d02b6ebb710da5abe75203c8 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 26 Sep 2024 09:43:38 +0200 Subject: [PATCH 24/31] chore: cleanup --- .../MainMenu/NavigationMenu/MegaMenu/index.tsx | 12 ++---------- .../MegaMenu/megaMenu.module.css | 18 +++++++++++++++--- .../NavigationMenuItem/index.tsx | 8 ++------ .../navigationMenuItem.module.css | 7 ++----- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx b/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx index e89a8186c..e0987bb6b 100644 --- a/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx +++ b/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx @@ -7,7 +7,6 @@ import Card from "@/components/TempDesignSystem/Card" import Link from "@/components/TempDesignSystem/Link" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import { useTrapFocus } from "@/hooks/useTrapFocus" import styles from "./megaMenu.module.css" @@ -21,21 +20,14 @@ export default function MegaMenu({ submenu, card, }: MegaMenuProps) { - const { openMegaMenu, toggleMegaMenu } = useDropdownStore() + const { toggleMegaMenu } = useDropdownStore() const megaMenuRef = useTrapFocus() - const isMegaMenuOpen = openMegaMenu === title - - useHandleKeyUp((event: KeyboardEvent) => { - if (event.key === "Escape" && isMegaMenuOpen) { - toggleMegaMenu(false) - } - }) function closeMegaMenu() { toggleMegaMenu(false) } return ( -
From 92a7fb2883f6155e4b5a4c6f6ac86c82e0b16fcf Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Fri, 27 Sep 2024 08:46:35 +0200 Subject: [PATCH 27/31] feat(SW-189): design changes to static map --- app/globals.css | 1 + .../HotelPage/Map/MapCard/mapCard.module.css | 10 +++++----- .../HotelPage/Map/StaticMap/index.tsx | 4 ++-- .../ContentType/HotelPage/hotelPage.module.css | 18 +++++++++++++++--- components/ContentType/HotelPage/index.tsx | 6 ++++-- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/app/globals.css b/app/globals.css index 5ec453db0..70606b14d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -107,6 +107,7 @@ --main-menu-mobile-height: 75px; --main-menu-desktop-height: 118px; + --booking-widget-desktop-height: 95px; --hotel-page-map-desktop-width: 23.75rem; /* Z-INDEX */ diff --git a/components/ContentType/HotelPage/Map/MapCard/mapCard.module.css b/components/ContentType/HotelPage/Map/MapCard/mapCard.module.css index c42156152..22f248cbd 100644 --- a/components/ContentType/HotelPage/Map/MapCard/mapCard.module.css +++ b/components/ContentType/HotelPage/Map/MapCard/mapCard.module.css @@ -1,13 +1,13 @@ .mapCard { display: grid; position: absolute; - bottom: 15%; - left: var(--Spacing-x2); - right: var(--Spacing-x2); + bottom: 0; + left: 0; + right: 0; background-color: var(--Base-Surface-Primary-light-Normal); padding: var(--Spacing-x2); - box-shadow: 0 0 2.5rem 0 rgba(0, 0, 0, 0.12); - border-radius: var(--Corner-radius-Medium); + border-top-left-radius: var(--Corner-radius-Medium); + border-top-right-radius: var(--Corner-radius-Medium); } .ctaButton { diff --git a/components/ContentType/HotelPage/Map/StaticMap/index.tsx b/components/ContentType/HotelPage/Map/StaticMap/index.tsx index 4c9ebe04a..b320063a9 100644 --- a/components/ContentType/HotelPage/Map/StaticMap/index.tsx +++ b/components/ContentType/HotelPage/Map/StaticMap/index.tsx @@ -18,9 +18,9 @@ export default async function StaticMap({ }: StaticMapProps) { const intl = await getIntl() const mapId = env.GOOGLE_STATIC_MAP_ID - const mapHeight = 785 + const mapHeight = 640 const markerHeight = 100 - const mapLatitudeInPx = mapHeight * 0.2 + const mapLatitudeInPx = mapHeight * 0.25 const mapCoordinates = { lat: calculateLatWithOffset(coordinates.lat, mapLatitudeInPx, zoomLevel), lng: coordinates.lng, diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index ceea1bfbe..eda544de8 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -40,16 +40,28 @@ } .mainSection { grid-area: mainSection; - padding: var(--Spacing-x6) var(--Spacing-x4) 0; + padding: var(--Spacing-x6) var(--Spacing-x4); } .mapContainer { display: flex; grid-area: mapContainer; align-self: start; - position: sticky; - top: 0; justify-content: center; width: 100%; + height: 100%; + background-color: var(--Base-Surface-Primary-light-Normal); + } + + .mapWithCard { + position: sticky; + top: 0; + min-height: 500px; /* Fixed min to not cover the marker with the card */ + height: calc( + 100vh - var(--main-menu-desktop-height) - + var(--booking-widget-desktop-height) + ); /* Full height without the header + booking widget */ + max-height: 935px; /* Fixed max according to figma */ + overflow: hidden; } .pageContainer > nav { diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 89f935e62..fe0a20caa 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -124,8 +124,10 @@ export default async function HotelPage() { {googleMapsApiKey ? ( <> Date: Fri, 27 Sep 2024 08:23:48 +0200 Subject: [PATCH 28/31] fix: revalidation endpoint for loyalty config --- app/api/web/revalidate/loyaltyConfig/route.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/api/web/revalidate/loyaltyConfig/route.ts b/app/api/web/revalidate/loyaltyConfig/route.ts index d7b770bf9..5e8fd51c7 100644 --- a/app/api/web/revalidate/loyaltyConfig/route.ts +++ b/app/api/web/revalidate/loyaltyConfig/route.ts @@ -22,9 +22,7 @@ const validateJsonBody = z.object({ entry: z.object({ reward_id: z.string().optional(), level_id: z.string().optional(), - system: z.object({ - locale: z.nativeEnum(Lang), - }), + locale: z.nativeEnum(Lang), }), }), }) @@ -62,7 +60,7 @@ export async function POST(request: NextRequest) { entry.level_id ) { tag = generateLoyaltyConfigTag( - entry.system.locale, + entry.locale, content_type.uid, entry.level_id ) @@ -71,7 +69,7 @@ export async function POST(request: NextRequest) { entry.reward_id ) { tag = generateLoyaltyConfigTag( - entry.system.locale, + entry.locale, content_type.uid, entry.reward_id ) From fe607f640c9fb6aa00aa5a01eabf602e5480ed0f Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 26 Sep 2024 15:20:22 +0200 Subject: [PATCH 29/31] feat(SW-325): added additional poi groups --- .../HotelPage/Map/DynamicMap/Map/index.tsx | 3 +- .../Map/DynamicMap/Sidebar/index.tsx | 18 ++++---- .../HotelPage/Map/MapCard/index.tsx | 7 ++- components/Icons/Airplane.tsx | 40 +++++++++++++++++ components/Icons/Business.tsx | 40 +++++++++++++++++ components/Icons/Camera.tsx | 27 ++++++++--- components/Icons/get-icon-by-icon-name.ts | 6 +++ components/Icons/index.tsx | 2 + components/Maps/Markers/Poi/index.tsx | 9 ++-- components/Maps/Markers/Poi/poi.module.css | 45 ++++++------------- components/Maps/Markers/Poi/variants.ts | 27 ++++------- components/Maps/Markers/utils.ts | 39 +++++++++------- server/routers/hotels/output.ts | 33 +++++--------- server/routers/hotels/utils.ts | 42 +++++++++++------ types/components/icon.ts | 2 + types/components/maps/poiMarker.ts | 7 +++ types/hotel.ts | 30 ++++++++++++- 17 files changed, 254 insertions(+), 123 deletions(-) create mode 100644 components/Icons/Airplane.tsx create mode 100644 components/Icons/Business.tsx diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx index a049e90c4..60a30a9a3 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx @@ -80,7 +80,8 @@ export default function MapContent({ className={`${styles.poi} ${activePoi === poi.name ? styles.active : ""}`} > diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx index 217fa51bd..c76b3f089 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx @@ -20,12 +20,10 @@ export default function Sidebar({ }: SidebarProps) { const intl = useIntl() const [isFullScreenSidebar, setIsFullScreenSidebar] = useState(false) - const poiCategories = new Set( - pointsOfInterest.map(({ category }) => category) - ) - const poisInCategories = Array.from(poiCategories).map((category) => ({ - category, - pois: pointsOfInterest.filter((poi) => poi.category === category), + const poiGroups = new Set(pointsOfInterest.map(({ group }) => group)) + const poisInGroups = Array.from(poiGroups).map((group) => ({ + group, + pois: pointsOfInterest.filter((poi) => poi.group === group), })) function toggleFullScreenSidebar() { @@ -60,9 +58,9 @@ export default function Sidebar({ )} - {poisInCategories.map(({ category, pois }) => + {poisInGroups.map(({ group, pois }) => pois.length ? ( -
+

- - {intl.formatMessage({ id: category })} + + {intl.formatMessage({ id: group })}

    diff --git a/components/ContentType/HotelPage/Map/MapCard/index.tsx b/components/ContentType/HotelPage/Map/MapCard/index.tsx index 8764870bc..79e9759be 100644 --- a/components/ContentType/HotelPage/Map/MapCard/index.tsx +++ b/components/ContentType/HotelPage/Map/MapCard/index.tsx @@ -39,7 +39,12 @@ export default function MapCard({ hotelName, pois }: MapCardProps) {
      {pois.map((poi) => (
    • - + {poi.name} {poi.distance} km
    • diff --git a/components/Icons/Airplane.tsx b/components/Icons/Airplane.tsx new file mode 100644 index 000000000..c68c37ce1 --- /dev/null +++ b/components/Icons/Airplane.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function AirplaneIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Business.tsx b/components/Icons/Business.tsx new file mode 100644 index 000000000..cb6ded53d --- /dev/null +++ b/components/Icons/Business.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function BusinessIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Camera.tsx b/components/Icons/Camera.tsx index bed8a79e6..728a9e1c1 100644 --- a/components/Icons/Camera.tsx +++ b/components/Icons/Camera.tsx @@ -8,16 +8,29 @@ export default function CameraIcon({ className, color, ...props }: IconProps) { - + + + + + + ) } diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 20bf8d9a8..3a443cea2 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -6,9 +6,11 @@ import TripAdvisorIcon from "./TripAdvisor" import { AccessibilityIcon, AccountCircleIcon, + AirplaneIcon, ArrowRightIcon, BarIcon, BikingIcon, + BusinessIcon, CalendarIcon, CameraIcon, CellphoneIcon, @@ -66,12 +68,16 @@ export function getIconByIconName(icon?: IconName): FC | null { return AccessibilityIcon case IconName.AccountCircle: return AccountCircleIcon + case IconName.Airplane: + return AirplaneIcon case IconName.ArrowRight: return ArrowRightIcon case IconName.Bar: return BarIcon case IconName.Biking: return BikingIcon + case IconName.Business: + return BusinessIcon case IconName.Calendar: return CalendarIcon case IconName.Camera: diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 9d7c0bd2c..db5e29bbb 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -1,8 +1,10 @@ export { default as AccessibilityIcon } from "./Accessibility" export { default as AccountCircleIcon } from "./AccountCircle" +export { default as AirplaneIcon } from "./Airplane" export { default as ArrowRightIcon } from "./ArrowRight" export { default as BarIcon } from "./Bar" export { default as BikingIcon } from "./Biking" +export { default as BusinessIcon } from "./Business" export { default as CalendarIcon } from "./Calendar" export { default as CameraIcon } from "./Camera" export { default as CellphoneIcon } from "./Cellphone" diff --git a/components/Maps/Markers/Poi/index.tsx b/components/Maps/Markers/Poi/index.tsx index db67af80b..9fa3d9e47 100644 --- a/components/Maps/Markers/Poi/index.tsx +++ b/components/Maps/Markers/Poi/index.tsx @@ -1,19 +1,20 @@ import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" -import { getCategoryIconName } from "../utils" +import { getIconByPoiGroupAndCategory } from "../utils" import { poiVariants } from "./variants" import type { PoiMarkerProps } from "@/types/components/maps/poiMarker" export default function PoiMarker({ - category, + group, + categoryName, skipBackground, size = 16, className = "", }: PoiMarkerProps) { - const iconName = getCategoryIconName(category) + const iconName = getIconByPoiGroupAndCategory(group, categoryName) const Icon = iconName ? getIconByIconName(iconName) : null - const classNames = poiVariants({ category, skipBackground, className }) + const classNames = poiVariants({ group, skipBackground, className }) return Icon ? ( diff --git a/components/Maps/Markers/Poi/poi.module.css b/components/Maps/Markers/Poi/poi.module.css index e89b8f702..0a08151ef 100644 --- a/components/Maps/Markers/Poi/poi.module.css +++ b/components/Maps/Markers/Poi/poi.module.css @@ -7,46 +7,29 @@ This will be handled later. */ align-items: center; padding: var(--Spacing-x-half); border-radius: var(--Corner-radius-Rounded); - background-color: var(--Scandic-Beige-90); -} -.airport, -.amusementPark, -.busTerminal, -.fair, -.hospital, -.hotel, -.marketingCity { + background-color: var(--UI-Text-Placeholder); } -.museum { - background: var(--Base-Interactive-Surface-Secondary-normal); +.attractions { + background-color: var(--Base-Interactive-Surface-Secondary-normal); } -.nearbyCompanies, -.parkingGarage { +.business { + background-color: var(--Scandic-Yellow-50); } - -.restaurant { - background: var(--Scandic-Peach-50); +.location { + background-color: var(--UI-Text-Placeholder); } - -.shopping { - background: var(--Base-Interactive-Surface-Primary-normal); +.parking { + background-color: var(--UI-Text-Active); } -.sports, -.theatre { +.publicTransport { + background-color: var(--Base-Interactive-Surface-Tertiary-normal); } - -.tourist { - background: var(--Scandic-Yellow-60); -} - -.transportations { - background: var(--Base-Interactive-Surface-Tertiary-normal); -} -.zoo { +.shoppingDining { + background-color: var(--Base-Interactive-Surface-Primary-normal); } .icon.transparent { - background: transparent; + background-color: transparent; padding: 0; } diff --git a/components/Maps/Markers/Poi/variants.ts b/components/Maps/Markers/Poi/variants.ts index 6eed0ae58..873b4e219 100644 --- a/components/Maps/Markers/Poi/variants.ts +++ b/components/Maps/Markers/Poi/variants.ts @@ -2,26 +2,17 @@ import { cva } from "class-variance-authority" import styles from "./poi.module.css" +import { PointOfInterestGroupEnum } from "@/types/hotel" + export const poiVariants = cva(styles.icon, { variants: { - category: { - Airport: styles.airport, - "Amusement park": styles.amusementPark, - "Bus terminal": styles.busTerminal, - Fair: styles.fair, - Hospital: styles.hospital, - Hotel: styles.hotel, - "Marketing city": styles.marketingCity, - Museum: styles.museum, - "Nearby companies": styles.nearbyCompanies, - "Parking / Garage": styles.parkingGarage, - Restaurant: styles.restaurant, - Shopping: styles.shopping, - Sports: styles.sports, - Theatre: styles.theatre, - Tourist: styles.tourist, - Transportations: styles.transportations, - Zoo: styles.zoo, + group: { + [PointOfInterestGroupEnum.ATTRACTIONS]: styles.attractions, + [PointOfInterestGroupEnum.BUSINESS]: styles.business, + [PointOfInterestGroupEnum.LOCATION]: styles.location, + [PointOfInterestGroupEnum.PARKING]: styles.parking, + [PointOfInterestGroupEnum.PUBLIC_TRANSPORT]: styles.publicTransport, + [PointOfInterestGroupEnum.SHOPPING_DINING]: styles.shoppingDining, }, skipBackground: { true: styles.transparent, diff --git a/components/Maps/Markers/utils.ts b/components/Maps/Markers/utils.ts index bf3e5ec38..fd574eb87 100644 --- a/components/Maps/Markers/utils.ts +++ b/components/Maps/Markers/utils.ts @@ -1,21 +1,30 @@ import { IconName } from "@/types/components/icon" -import type { PointOfInterestCategory } from "@/types/hotel" +import { + PointOfInterestCategoryNameEnum, + PointOfInterestGroupEnum, +} from "@/types/hotel" -/* 2024-09-18: At the moment, the icons for the different categories is unknown. -This will be handled later. */ -export function getCategoryIconName(category?: PointOfInterestCategory | null) { - switch (category) { - case "Transportations": - return IconName.Train - case "Shopping": +export function getIconByPoiGroupAndCategory( + group: PointOfInterestGroupEnum, + category?: PointOfInterestCategoryNameEnum +) { + switch (group) { + case PointOfInterestGroupEnum.PUBLIC_TRANSPORT: + return category === PointOfInterestCategoryNameEnum.AIRPORT + ? IconName.Airplane + : IconName.Train + case PointOfInterestGroupEnum.ATTRACTIONS: + return category === PointOfInterestCategoryNameEnum.MUSEUM + ? IconName.Museum + : IconName.Camera + case PointOfInterestGroupEnum.BUSINESS: + return IconName.Business + case PointOfInterestGroupEnum.PARKING: + return IconName.Parking + case PointOfInterestGroupEnum.SHOPPING_DINING: return IconName.Shopping - case "Museum": - return IconName.Museum - case "Tourist": - return IconName.Cultural - case "Restaurant": - return IconName.Restaurant + case PointOfInterestGroupEnum.LOCATION: default: - return IconName.StarFilled + return IconName.Location } } diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 82a51754b..7f57ba6cd 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -2,6 +2,13 @@ import { z } from "zod" import { toLang } from "@/server/utils" +import { getPoiGroupByCategoryName } from "./utils" + +import { + PointOfInterestCategoryNameEnum, + PointOfInterestGroupEnum, +} from "@/types/hotel" + const ratingsSchema = z .object({ tripAdvisor: z.object({ @@ -213,32 +220,15 @@ const rewardNightSchema = z.object({ }), }) -const poiCategories = z.enum([ - "Airport", - "Amusement park", - "Bus terminal", - "Fair", - "Hospital", - "Hotel", - "Marketing city", - "Museum", - "Nearby companies", - "Parking / Garage", - "Restaurant", - "Shopping", - "Sports", - "Theatre", - "Tourist", - "Transportations", - "Zoo", -]) +const poiGroups = z.nativeEnum(PointOfInterestGroupEnum) +const poiCategoryNames = z.nativeEnum(PointOfInterestCategoryNameEnum) export const pointOfInterestSchema = z .object({ name: z.string(), distance: z.number(), category: z.object({ - name: poiCategories, + name: poiCategoryNames, group: z.string(), }), location: locationSchema, @@ -247,7 +237,8 @@ export const pointOfInterestSchema = z .transform((poi) => ({ name: poi.name, distance: poi.distance, - category: poi.category.name, + categoryName: poi.category.name, + group: getPoiGroupByCategoryName(poi.category.name), coordinates: { lat: poi.location.latitude, lng: poi.location.longitude, diff --git a/server/routers/hotels/utils.ts b/server/routers/hotels/utils.ts index 6fa8e168f..d19d1a492 100644 --- a/server/routers/hotels/utils.ts +++ b/server/routers/hotels/utils.ts @@ -1,5 +1,3 @@ -import { IconName } from "@/types/components/icon" - import deepmerge from "deepmerge" import { unstable_cache } from "next/cache" @@ -15,23 +13,39 @@ import { } from "./output" import type { RequestOptionsWithOutBody } from "@/types/fetch" +import { + PointOfInterestCategoryNameEnum, + PointOfInterestGroupEnum, +} from "@/types/hotel" import type { Lang } from "@/constants/languages" import type { Endpoint } from "@/lib/api/endpoints" -export function getIconByPoiCategory(category: string) { +export function getPoiGroupByCategoryName( + category: PointOfInterestCategoryNameEnum +) { switch (category) { - case "Transportations": - return IconName.Train - case "Shopping": - return IconName.Shopping - case "Museum": - return IconName.Museum - case "Tourist": - return IconName.Cultural - case "Restaurant": - return IconName.Restaurant + case PointOfInterestCategoryNameEnum.AIRPORT: + case PointOfInterestCategoryNameEnum.BUS_TERMINAL: + case PointOfInterestCategoryNameEnum.TRANSPORTATIONS: + return PointOfInterestGroupEnum.PUBLIC_TRANSPORT + case PointOfInterestCategoryNameEnum.AMUSEMENT_PARK: + case PointOfInterestCategoryNameEnum.MUSEUM: + case PointOfInterestCategoryNameEnum.SPORTS: + case PointOfInterestCategoryNameEnum.THEATRE: + case PointOfInterestCategoryNameEnum.TOURIST: + case PointOfInterestCategoryNameEnum.ZOO: + return PointOfInterestGroupEnum.ATTRACTIONS + case PointOfInterestCategoryNameEnum.NEARBY_COMPANIES: + case PointOfInterestCategoryNameEnum.FAIR: + return PointOfInterestGroupEnum.BUSINESS + case PointOfInterestCategoryNameEnum.PARKING_GARAGE: + return PointOfInterestGroupEnum.PARKING + case PointOfInterestCategoryNameEnum.SHOPPING: + case PointOfInterestCategoryNameEnum.RESTAURANT: + return PointOfInterestGroupEnum.SHOPPING_DINING + case PointOfInterestCategoryNameEnum.HOSPITAL: default: - return null + return PointOfInterestGroupEnum.LOCATION } } diff --git a/types/components/icon.ts b/types/components/icon.ts index 6b11649a2..c0fdf6ee0 100644 --- a/types/components/icon.ts +++ b/types/components/icon.ts @@ -9,9 +9,11 @@ export interface IconProps export enum IconName { Accessibility = "Accessibility", AccountCircle = "AccountCircle", + Airplane = "Airplane", ArrowRight = "ArrowRight", Bar = "Bar", Biking = "Biking", + Business = "Business", Calendar = "Calendar", Camera = "Camera", Cellphone = "Cellphone", diff --git a/types/components/maps/poiMarker.ts b/types/components/maps/poiMarker.ts index 89932fb51..34fad0e6f 100644 --- a/types/components/maps/poiMarker.ts +++ b/types/components/maps/poiMarker.ts @@ -2,7 +2,14 @@ import { poiVariants } from "@/components/Maps/Markers/Poi/variants" import type { VariantProps } from "class-variance-authority" +import { + PointOfInterestCategoryNameEnum, + PointOfInterestGroupEnum, +} from "@/types/hotel" + export interface PoiMarkerProps extends VariantProps { + group: PointOfInterestGroupEnum + categoryName?: PointOfInterestCategoryNameEnum size?: number className?: string } diff --git a/types/hotel.ts b/types/hotel.ts index a2656f2b0..54d9c8125 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -20,4 +20,32 @@ export type HotelTripAdvisor = export type RoomData = z.infer export type PointOfInterest = z.output -export type PointOfInterestCategory = PointOfInterest["category"] + +export enum PointOfInterestCategoryNameEnum { + AIRPORT = "Airport", + AMUSEMENT_PARK = "Amusement park", + BUS_TERMINAL = "Bus terminal", + FAIR = "Fair", + HOSPITAL = "Hospital", + HOTEL = "Hotel", + MARKETING_CITY = "Marketing city", + MUSEUM = "Museum", + NEARBY_COMPANIES = "Nearby companies", + PARKING_GARAGE = "Parking / Garage", + RESTAURANT = "Restaurant", + SHOPPING = "Shopping", + SPORTS = "Sports", + THEATRE = "Theatre", + TOURIST = "Tourist", + TRANSPORTATIONS = "Transportations", + ZOO = "Zoo", +} + +export enum PointOfInterestGroupEnum { + PUBLIC_TRANSPORT = "Public transport", + ATTRACTIONS = "Attractions", + BUSINESS = "Business", + LOCATION = "Location", + PARKING = "Parking", + SHOPPING_DINING = "Shopping & Dining", +} From e43c4ce9e842a4a82164b6e4a1454c05c4c17e3f Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Fri, 27 Sep 2024 07:46:36 +0200 Subject: [PATCH 30/31] feat(SW-325): added translations --- i18n/dictionaries/da.json | 52 +++++++++++++++++++++----------------- i18n/dictionaries/de.json | 53 ++++++++++++++++++++++----------------- i18n/dictionaries/en.json | 10 ++++++-- i18n/dictionaries/fi.json | 52 +++++++++++++++++++++----------------- i18n/dictionaries/no.json | 52 +++++++++++++++++++++----------------- i18n/dictionaries/sv.json | 52 +++++++++++++++++++++----------------- 6 files changed, 154 insertions(+), 117 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index d6b4ab721..76a453d7c 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -14,10 +14,12 @@ "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": "fra idag", "As our": "Som vores {level}", "As our Close Friend": "Som vores nære ven", "At latest": "Senest", "At the hotel": "På hotellet", + "Attractions": "Attraktioner", "Back to scandichotels.com": "Tilbage til scandichotels.com", "Bed type": "Seng type", "Book": "Book", @@ -28,7 +30,10 @@ "Breakfast excluded": "Morgenmad ikke inkluderet", "Breakfast included": "Morgenmad inkluderet", "Bus terminal": "Busstation", + "Business": "Forretning", + "by": "inden", "Cancel": "Afbestille", + "characters": "tegn", "Check in": "Check ind", "Check out": "Check ud", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tjek de kreditkort, der er gemt på din profil. Betal med et gemt kort, når du er logget ind for en mere jævn weboplevelse.", @@ -70,9 +75,9 @@ "Explore all levels and benefits": "Udforsk alle niveauer og fordele", "Explore nearby": "Udforsk i nærheden", "Extras to your booking": "Tillæg til din booking", - "FAQ": "Ofte stillede spørgsmål", "Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.", "Fair": "Messe", + "FAQ": "Ofte stillede spørgsmål", "Find booking": "Find booking", "Find hotels": "Find hotel", "Flexibility": "Fleksibilitet", @@ -89,11 +94,15 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel faciliteter", "Hotel surroundings": "Hotel omgivelser", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer", "Hotels": "Hoteller", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det virker", "Image gallery": "Billedgalleri", "Join Scandic Friends": "Tilmeld dig Scandic Friends", + "km to city center": "km til byens centrum", "Language": "Sprog", "Latest searches": "Seneste søgninger", "Level": "Niveau", @@ -105,6 +114,7 @@ "Level 6": "Niveau 6", "Level 7": "Niveau 7", "Level up to unlock": "Stig i niveau for at låse op", + "Location": "Beliggenhed", "Locations": "Placeringer", "Log in": "Log på", "Log in here": "Log ind her", @@ -119,9 +129,9 @@ "Member price": "Medlemspris", "Member price from": "Medlemspris fra", "Members": "Medlemmer", + "Membership cards": "Medlemskort", "Membership ID": "Medlems-id", "Membership ID copied to clipboard": "Medlems-ID kopieret til udklipsholder", - "Membership cards": "Medlemskort", "Menu": "Menu", "Modify": "Ændre", "Month": "Måned", @@ -136,6 +146,9 @@ "Nearby companies": "Nærliggende virksomheder", "New password": "Nyt kodeord", "Next": "Næste", + "next level:": "Næste niveau:", + "night": "nat", + "nights": "nætter", "Nights needed to level up": "Nætter nødvendige for at komme i niveau", "No content published": "Intet indhold offentliggjort", "No matching location found": "Der blev ikke fundet nogen matchende placering", @@ -146,12 +159,15 @@ "Non-refundable": "Ikke-refunderbart", "Not found": "Ikke fundet", "Nr night, nr adult": "{nights, number} nat, {adults, number} voksen", + "number": "nummer", "On your journey": "På din rejse", "Open": "Åben", "Open language menu": "Åbn sprogmenuen", "Open menu": "Åbn menuen", "Open my pages menu": "Åbn mine sider menuen", + "or": "eller", "Overview": "Oversigt", + "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", "Password": "Adgangskode", "Pay later": "Betal senere", @@ -161,6 +177,7 @@ "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", + "points": "Point", "Points": "Point", "Points being calculated": "Point udregnes", "Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021", @@ -169,6 +186,7 @@ "Points needed to stay on level": "Point nødvendige for at holde sig på niveau", "Previous victories": "Tidligere sejre", "Public price from": "Offentlig pris fra", + "Public transport": "Offentlig transport", "Read more": "Læs mere", "Read more about the hotel": "Læs mere om hotellet", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", @@ -192,6 +210,7 @@ "Select language": "Vælg sprog", "Select your language": "Vælg dit sprog", "Shopping": "Shopping", + "Shopping & Dining": "Shopping & Spisning", "Show all amenities": "Vis alle faciliteter", "Show less": "Vis mindre", "Show map": "Vis kort", @@ -201,25 +220,29 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.", "Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.", "Something went wrong!": "Noget gik galt!", + "special character": "speciel karakter", + "spendable points expiring by": "{points} Brugbare point udløber den {date}", "Sports": "Sport", "Standard price": "Standardpris", "Street": "Gade", "Successfully updated profile!": "Profilen er opdateret med succes!", "Summary": "Opsummering", - "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortæl os, hvilke oplysninger og opdateringer du gerne vil modtage, og hvordan, ved at klikke på linket nedenfor.", "Thank you": "Tak", "Theatre": "Teater", "There are no transactions to display": "Der er ingen transaktioner at vise", "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", + "to": "til", "Total Points": "Samlet antal point", "Tourist": "Turist", "Transaction date": "Overførselsdato", "Transactions": "Transaktioner", "Transportations": "Transport", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", + "TUI Points": "TUI Points", "Type of bed": "Sengtype", "Type of room": "Værelsestype", + "uppercase letter": "stort bogstav", "Use bonus cheque": "Brug Bonus Cheque", "User information": "Brugeroplysninger", "View as list": "Vis som liste", @@ -245,9 +268,9 @@ "You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.", "You have no previous stays.": "Du har ingen tidligere ophold.", "You have no upcoming stays.": "Du har ingen kommende ophold.", - "Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!", "Your card was successfully removed!": "Dit kort blev fjernet!", "Your card was successfully saved!": "Dit kort blev gemt!", + "Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!", "Your current level": "Dit nuværende niveau", "Your details": "Dine oplysninger", "Your level": "Dit niveau", @@ -255,22 +278,5 @@ "Zip code": "Postnummer", "Zoo": "Zoo", "Zoom in": "Zoom ind", - "Zoom out": "Zoom ud", - "as of today": "fra idag", - "by": "inden", - "characters": "tegn", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", - "hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer", - "km to city center": "km til byens centrum", - "next level:": "Næste niveau:", - "night": "nat", - "nights": "nætter", - "number": "nummer", - "or": "eller", - "points": "Point", - "special character": "speciel karakter", - "spendable points expiring by": "{points} Brugbare point udløber den {date}", - "to": "til", - "uppercase letter": "stort bogstav" -} \ No newline at end of file + "Zoom out": "Zoom ud" +} diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 279e43409..468d02143 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -14,10 +14,12 @@ "Any changes you've made will be lost.": "Alle Änderungen, die Sie vorgenommen haben, gehen verloren.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Möchten Sie die Karte mit der Endung {lastFourDigits} wirklich aus Ihrem Mitgliedsprofil entfernen?", "Arrival date": "Ankunftsdatum", + "as of today": "Stand heute", "As our": "Als unser {level}", "As our Close Friend": "Als unser enger Freund", "At latest": "Spätestens", "At the hotel": "Im Hotel", + "Attractions": "Attraktionen", "Back to scandichotels.com": "Zurück zu scandichotels.com", "Bed type": "Bettentyp", "Book": "Buchen", @@ -28,7 +30,10 @@ "Breakfast excluded": "Frühstück nicht inbegriffen", "Breakfast included": "Frühstück inbegriffen", "Bus terminal": "Busbahnhof", + "Business": "Geschäft", + "by": "bis", "Cancel": "Stornieren", + "characters": "figuren", "Check in": "Einchecken", "Check out": "Auschecken", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sehen Sie sich die in Ihrem Profil gespeicherten Kreditkarten an. Bezahlen Sie mit einer gespeicherten Karte, wenn Sie angemeldet sind, für ein reibungsloseres Web-Erlebnis.", @@ -70,9 +75,9 @@ "Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile", "Explore nearby": "Erkunden Sie die Umgebung", "Extras to your booking": "Extras zu Ihrer Buchung", - "FAQ": "Häufig gestellte Fragen", "Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.", "Fair": "Messe", + "FAQ": "Häufig gestellte Fragen", "Find booking": "Buchung finden", "Find hotels": "Hotels finden", "Flexibility": "Flexibilität", @@ -89,11 +94,15 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel-Infos", "Hotel surroundings": "Umgebung des Hotels", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "personen", + "hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen", "Hotels": "Hotels", "How do you want to sleep?": "Wie möchtest du schlafen?", "How it works": "Wie es funktioniert", "Image gallery": "Bildergalerie", "Join Scandic Friends": "Treten Sie Scandic Friends bei", + "km to city center": "km bis zum Stadtzentrum", "Language": "Sprache", "Latest searches": "Letzte Suchanfragen", "Level": "Level", @@ -105,6 +114,7 @@ "Level 6": "Level 6", "Level 7": "Level 7", "Level up to unlock": "Zum Freischalten aufsteigen", + "Location": "Ort", "Locations": "Orte", "Log in": "Anmeldung", "Log in here": "Hier einloggen", @@ -119,9 +129,9 @@ "Member price": "Mitgliederpreis", "Member price from": "Mitgliederpreis ab", "Members": "Mitglieder", + "Membership cards": "Mitgliedskarten", "Membership ID": "Mitglieds-ID", "Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert", - "Membership cards": "Mitgliedskarten", "Menu": "Menu", "Modify": "Ändern", "Month": "Monat", @@ -136,6 +146,9 @@ "Nearby companies": "Nahe gelegene Unternehmen", "New password": "Neues Kennwort", "Next": "Nächste", + "next level:": "Nächstes Level:", + "night": "nacht", + "nights": "Nächte", "Nights needed to level up": "Nächte, die zum Levelaufstieg benötigt werden", "No content published": "Kein Inhalt veröffentlicht", "No matching location found": "Kein passender Standort gefunden", @@ -146,11 +159,15 @@ "Non-refundable": "Nicht erstattungsfähig", "Not found": "Nicht gefunden", "Nr night, nr adult": "{nights, number} Nacht, {adults, number} Erwachsener", + "number": "nummer", "On your journey": "Auf deiner Reise", "Open": "Offen", "Open language menu": "Sprachmenü öffnen", "Open menu": "Menü öffnen", "Open my pages menu": "Meine Seiten Menü öffnen", + "or": "oder", + "Overview": "Übersicht", + "Parking": "Parken", "Parking / Garage": "Parken / Garage", "Pay later": "Später bezahlen", "Pay now": "Jetzt bezahlen", @@ -160,6 +177,7 @@ "Phone number": "Telefonnummer", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", "Points": "Punkte", + "points": "Punkte", "Points being calculated": "Punkte werden berechnet", "Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021", "Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.", @@ -167,6 +185,7 @@ "Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben", "Previous victories": "Bisherige Siege", "Public price from": "Öffentlicher Preis ab", + "Public transport": "Öffentliche Verkehrsmittel", "Read more": "Mehr lesen", "Read more about the hotel": "Lesen Sie mehr über das Hotel", "Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen", @@ -190,6 +209,7 @@ "Select language": "Sprache auswählen", "Select your language": "Wählen Sie Ihre Sprache", "Shopping": "Einkaufen", + "Shopping & Dining": "Einkaufen & Essen", "Show all amenities": "Alle Annehmlichkeiten anzeigen", "Show less": "Weniger anzeigen", "Show map": "Karte anzeigen", @@ -199,25 +219,29 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.", "Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.", "Something went wrong!": "Etwas ist schief gelaufen!", + "special character": "sonderzeichen", + "spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}", "Sports": "Sport", "Standard price": "Standardpreis", "Street": "Straße", "Successfully updated profile!": "Profil erfolgreich aktualisiert!", "Summary": "Zusammenfassung", - "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Teilen Sie uns mit, welche Informationen und Updates Sie wie erhalten möchten, indem Sie auf den unten stehenden Link klicken.", "Thank you": "Danke", "Theatre": "Theater", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", + "to": "zu", "Total Points": "Gesamtpunktzahl", "Tourist": "Tourist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktionen", "Transportations": "Transportmittel", "Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)", + "TUI Points": "TUI Points", "Type of bed": "Bettentyp", "Type of room": "Zimmerart", + "uppercase letter": "großbuchstabe", "Use bonus cheque": "Bonusscheck nutzen", "User information": "Nutzerinformation", "View as list": "Als Liste anzeigen", @@ -243,9 +267,9 @@ "You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.", "You have no previous stays.": "Sie haben keine vorherigen Aufenthalte.", "You have no upcoming stays.": "Sie haben keine bevorstehenden Aufenthalte.", - "Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!", "Your card was successfully removed!": "Ihre Karte wurde erfolgreich entfernt!", "Your card was successfully saved!": "Ihre Karte wurde erfolgreich gespeichert!", + "Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!", "Your current level": "Ihr aktuelles Level", "Your details": "Ihre Angaben", "Your level": "Dein level", @@ -253,22 +277,5 @@ "Zip code": "PLZ", "Zoo": "Zoo", "Zoom in": "Vergrößern", - "Zoom out": "Verkleinern", - "as of today": "Stand heute", - "by": "bis", - "characters": "figuren", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personen", - "hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen", - "km to city center": "km bis zum Stadtzentrum", - "next level:": "Nächstes Level:", - "night": "nacht", - "nights": "Nächte", - "number": "nummer", - "or": "oder", - "points": "Punkte", - "special character": "sonderzeichen", - "spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}", - "to": "zu", - "uppercase letter": "großbuchstabe" -} \ No newline at end of file + "Zoom out": "Verkleinern" +} diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 0bc741364..3bab2ea50 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -19,6 +19,7 @@ "As our Close Friend": "As our Close Friend", "At latest": "At latest", "At the hotel": "At the hotel", + "Attractions": "Attractions", "Back to scandichotels.com": "Back to scandichotels.com", "Bed type": "Bed type", "Book": "Book", @@ -29,6 +30,7 @@ "Breakfast excluded": "Breakfast excluded", "Breakfast included": "Breakfast included", "Bus terminal": "Bus terminal", + "Business": "Business", "by": "by", "Cancel": "Cancel", "characters": "characters", @@ -112,6 +114,7 @@ "Level 6": "Level 6", "Level 7": "Level 7", "Level up to unlock": "Level up to unlock", + "Location": "Location", "Locations": "Locations", "Log in": "Log in", "Log in here": "Log in here", @@ -164,6 +167,7 @@ "Open my pages menu": "Open my pages menu", "or": "or", "Overview": "Overview", + "Parking": "Parking", "Parking / Garage": "Parking / Garage", "Password": "Password", "Pay later": "Pay later", @@ -173,8 +177,8 @@ "Phone is required": "Phone is required", "Phone number": "Phone number", "Please enter a valid phone number": "Please enter a valid phone number", - "points": "Points", "Points": "Points", + "points": "Points", "Points being calculated": "Points being calculated", "Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021", "Points may take up to 10 days to be displayed.": "Points may take up to 10 days to be displayed.", @@ -182,6 +186,7 @@ "Points needed to stay on level": "Points needed to stay on level", "Previous victories": "Previous victories", "Public price from": "Public price from", + "Public transport": "Public transport", "Read more": "Read more", "Read more about the hotel": "Read more about the hotel", "Remove card from member profile": "Remove card from member profile", @@ -205,6 +210,7 @@ "Select language": "Select language", "Select your language": "Select your language", "Shopping": "Shopping", + "Shopping & Dining": "Shopping & Dining", "Show all amenities": "Show all amenities", "Show less": "Show less", "Show map": "Show map", @@ -273,4 +279,4 @@ "Zoo": "Zoo", "Zoom in": "Zoom in", "Zoom out": "Zoom out" -} \ No newline at end of file +} diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index f14202c86..a0c5f5207 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -14,10 +14,12 @@ "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?", "Arrival date": "Saapumispäivä", + "as of today": "tänään", "As our": "{level}-etu", "As our Close Friend": "Läheisenä ystävänämme", "At latest": "Viimeistään", "At the hotel": "Hotellissa", + "Attractions": "Nähtävyydet", "Back to scandichotels.com": "Takaisin scandichotels.com", "Bed type": "Vuodetyyppi", "Book": "Varaa", @@ -28,7 +30,10 @@ "Breakfast excluded": "Aamiainen ei sisälly", "Breakfast included": "Aamiainen sisältyy", "Bus terminal": "Bussiasema", + "Business": "Business", + "by": "mennessä", "Cancel": "Peruuttaa", + "characters": "hahmoja", "Check in": "Sisäänkirjautuminen", "Check out": "Uloskirjautuminen", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tarkista profiiliisi tallennetut luottokortit. Maksa tallennetulla kortilla kirjautuneena, jotta verkkokokemus on sujuvampi.", @@ -70,9 +75,9 @@ "Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", "Explore nearby": "Tutustu lähialueeseen", "Extras to your booking": "Varauksessa lisäpalveluita", - "FAQ": "UKK", "Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.", "Fair": "Messukeskus", + "FAQ": "UKK", "Find booking": "Etsi varaus", "Find hotels": "Etsi hotelleja", "Flexibility": "Joustavuus", @@ -89,11 +94,15 @@ "Hotel": "Hotelli", "Hotel facilities": "Hotellin palvelut", "Hotel surroundings": "Hotellin ympäristö", + "hotelPages.rooms.roomCard.person": "henkilö", + "hotelPages.rooms.roomCard.persons": "Henkilöä", + "hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot", "Hotels": "Hotellit", "How do you want to sleep?": "Kuinka haluat nukkua?", "How it works": "Kuinka se toimii", "Image gallery": "Kuvagalleria", "Join Scandic Friends": "Liity jäseneksi", + "km to city center": "km keskustaan", "Language": "Kieli", "Latest searches": "Viimeisimmät haut", "Level": "Level", @@ -105,6 +114,7 @@ "Level 6": "Taso 6", "Level 7": "Taso 7", "Level up to unlock": "Nosta taso avataksesi lukituksen", + "Location": "Sijainti", "Locations": "Sijainnit", "Log in": "Kirjaudu sisään", "Log in here": "Kirjaudu sisään", @@ -119,9 +129,9 @@ "Member price": "Jäsenhinta", "Member price from": "Jäsenhinta alkaen", "Members": "Jäsenet", + "Membership cards": "Jäsenkortit", "Membership ID": "Jäsentunnus", "Membership ID copied to clipboard": "Jäsenyystunnus kopioitu leikepöydälle", - "Membership cards": "Jäsenkortit", "Menu": "Valikko", "Modify": "Muokkaa", "Month": "Kuukausi", @@ -136,6 +146,9 @@ "Nearby companies": "Läheiset yritykset", "New password": "Uusi salasana", "Next": "Seuraava", + "next level:": "pistettä tasolle:", + "night": "yö", + "nights": "yötä", "Nights needed to level up": "Yöt, joita tarvitaan tasolle", "No content published": "Ei julkaistua sisältöä", "No matching location found": "Vastaavaa sijaintia ei löytynyt", @@ -146,12 +159,15 @@ "Non-refundable": "Ei palautettavissa", "Not found": "Ei löydetty", "Nr night, nr adult": "{nights, number} yö, {adults, number} aikuinen", + "number": "määrä", "On your journey": "Matkallasi", "Open": "Avata", "Open language menu": "Avaa kielivalikko", "Open menu": "Avaa valikko", "Open my pages menu": "Avaa omat sivut -valikko", + "or": "tai", "Overview": "Yleiskatsaus", + "Parking": "Pysäköinti", "Parking / Garage": "Pysäköinti / Autotalli", "Password": "Salasana", "Pay later": "Maksa myöhemmin", @@ -161,6 +177,7 @@ "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", + "points": "pistettä", "Points": "Pisteet", "Points being calculated": "Pisteitä lasketaan", "Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021", @@ -169,6 +186,7 @@ "Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet", "Previous victories": "Edelliset voitot", "Public price from": "Julkinen hinta alkaen", + "Public transport": "Julkinen liikenne", "Read more": "Lue lisää", "Read more about the hotel": "Lue lisää hotellista", "Remove card from member profile": "Poista kortti jäsenprofiilista", @@ -193,6 +211,7 @@ "Select language": "Valitse kieli", "Select your language": "Valitse kieli", "Shopping": "Ostokset", + "Shopping & Dining": "Ostokset & Ravintolat", "Show all amenities": "Näytä kaikki mukavuudet", "Show less": "Näytä vähemmän", "Show map": "Näytä kartta", @@ -202,25 +221,29 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.", "Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.", "Something went wrong!": "Jotain meni pieleen!", + "special character": "erikoishahmo", + "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "Sports": "Urheilu", "Standard price": "Normaali hinta", "Street": "Katu", "Successfully updated profile!": "Profiilin päivitys onnistui!", "Summary": "Yhteenveto", - "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.", "Thank you": "Kiitos", "Theatre": "Teatteri", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", + "to": "to", "Total Points": "Kokonaispisteet", "Tourist": "Turisti", "Transaction date": "Tapahtuman päivämäärä", "Transactions": "Tapahtumat", "Transportations": "Kuljetukset", "Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)", + "TUI Points": "TUI Points", "Type of bed": "Vuodetyyppi", "Type of room": "Huonetyyppi", + "uppercase letter": "iso kirjain", "Use bonus cheque": "Käytä bonussekkiä", "User information": "Käyttäjän tiedot", "View as list": "Näytä listana", @@ -246,9 +269,9 @@ "You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.", "You have no previous stays.": "Sinulla ei ole aiempia majoituksia.", "You have no upcoming stays.": "Sinulla ei ole tulevia majoituksia.", - "Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!", "Your card was successfully removed!": "Korttisi poistettiin onnistuneesti!", "Your card was successfully saved!": "Korttisi tallennettu onnistuneesti!", + "Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!", "Your current level": "Nykyinen tasosi", "Your details": "Tietosi", "Your level": "Tasosi", @@ -256,22 +279,5 @@ "Zip code": "Postinumero", "Zoo": "Eläintarha", "Zoom in": "Lähennä", - "Zoom out": "Loitonna", - "as of today": "tänään", - "by": "mennessä", - "characters": "hahmoja", - "hotelPages.rooms.roomCard.person": "henkilö", - "hotelPages.rooms.roomCard.persons": "Henkilöä", - "hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot", - "km to city center": "km keskustaan", - "next level:": "pistettä tasolle:", - "night": "yö", - "nights": "yötä", - "number": "määrä", - "or": "tai", - "points": "pistettä", - "special character": "erikoishahmo", - "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", - "to": "to", - "uppercase letter": "iso kirjain" -} \ No newline at end of file + "Zoom out": "Loitonna" +} diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 12e6f162a..745240373 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -14,10 +14,12 @@ "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 idag", "As our": "Som vår {level}", "As our Close Friend": "Som vår nære venn", "At latest": "Senest", "At the hotel": "På hotellet", + "Attractions": "Attraksjoner", "Back to scandichotels.com": "Tilbake til scandichotels.com", "Bed type": "Seng type", "Book": "Bestill", @@ -28,7 +30,10 @@ "Breakfast excluded": "Frokost ekskludert", "Breakfast included": "Frokost inkludert", "Bus terminal": "Bussterminal", + "Business": "Forretnings", + "by": "innen", "Cancel": "Avbryt", + "characters": "tegn", "Check in": "Sjekk inn", "Check out": "Sjekk ut", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sjekk ut kredittkortene som er lagret på profilen din. Betal med et lagret kort når du er pålogget for en jevnere nettopplevelse.", @@ -70,9 +75,9 @@ "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", "Flexibility": "Fleksibilitet", @@ -89,11 +94,15 @@ "Hotel": "Hotel", "Hotel facilities": "Hotelfaciliteter", "Hotel surroundings": "Hotellomgivelser", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet", "Hotels": "Hoteller", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det fungerer", "Image gallery": "Bildegalleri", "Join Scandic Friends": "Bli med i Scandic Friends", + "km to city center": "km til sentrum", "Language": "Språk", "Latest searches": "Siste søk", "Level": "Nivå", @@ -105,6 +114,7 @@ "Level 6": "Nivå 6", "Level 7": "Nivå 7", "Level up to unlock": "Nivå opp for å låse opp", + "Location": "Beliggenhet", "Locations": "Steder", "Log in": "Logg Inn", "Log in here": "Logg inn her", @@ -119,9 +129,9 @@ "Member price": "Medlemspris", "Member price from": "Medlemspris fra", "Members": "Medlemmer", + "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", "Membership ID copied to clipboard": "Medlems-ID kopiert til utklippstavlen", - "Membership cards": "Medlemskort", "Menu": "Menu", "Modify": "Endre", "Month": "Måned", @@ -136,6 +146,9 @@ "Nearby companies": "Nærliggende selskaper", "New password": "Nytt passord", "Next": "Neste", + "next level:": "Neste nivå:", + "night": "natt", + "nights": "netter", "Nights needed to level up": "Netter som trengs for å komme opp i nivå", "No content published": "Ingen innhold publisert", "No matching location found": "Fant ingen samsvarende plassering", @@ -146,12 +159,15 @@ "Non-refundable": "Ikke-refunderbart", "Not found": "Ikke funnet", "Nr night, nr adult": "{nights, number} natt, {adults, number} voksen", + "number": "antall", "On your journey": "På reisen din", "Open": "Åpen", "Open language menu": "Åpne språkmenyen", "Open menu": "Åpne menyen", "Open my pages menu": "Åpne mine sider menyen", + "or": "eller", "Overview": "Oversikt", + "Parking": "Parkering", "Parking / Garage": "Parkering / Garasje", "Password": "Passord", "Pay later": "Betal senere", @@ -161,6 +177,7 @@ "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", + "points": "poeng", "Points": "Poeng", "Points being calculated": "Poeng beregnes", "Points earned prior to May 1, 2021": "Opptjente poeng før 1. mai 2021", @@ -169,6 +186,7 @@ "Points needed to stay on level": "Poeng som trengs for å holde seg på nivå", "Previous victories": "Tidligere seire", "Public price from": "Offentlig pris fra", + "Public transport": "Offentlig transport", "Read more": "Les mer", "Read more about the hotel": "Les mer om hotellet", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", @@ -192,6 +210,7 @@ "Select language": "Velg språk", "Select your language": "Velg språk", "Shopping": "Shopping", + "Shopping & Dining": "Shopping & Spisesteder", "Show all amenities": "Vis alle fasiliteter", "Show less": "Vis mindre", "Show map": "Vis kart", @@ -201,25 +220,29 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.", "Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.", "Something went wrong!": "Noe gikk galt!", + "special character": "spesiell karakter", + "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", "Sports": "Sport", "Standard price": "Standardpris", "Street": "Gate", "Successfully updated profile!": "Vellykket oppdatert profil!", "Summary": "Sammendrag", - "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortell oss hvilken informasjon og hvilke oppdateringer du ønsker å motta, og hvordan, ved å klikke på lenken nedenfor.", "Thank you": "Takk", "Theatre": "Teater", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", + "to": "til", "Total Points": "Totale poeng", "Tourist": "Turist", "Transaction date": "Transaksjonsdato", "Transactions": "Transaksjoner", "Transportations": "Transport", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", + "TUI Points": "TUI Points", "Type of bed": "Sengtype", "Type of room": "Romtype", + "uppercase letter": "stor bokstav", "Use bonus cheque": "Bruk bonussjekk", "User information": "Brukerinformasjon", "View as list": "Vis som liste", @@ -245,9 +268,9 @@ "You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.", "You have no previous stays.": "Du har ingen tidligere opphold.", "You have no upcoming stays.": "Du har ingen kommende opphold.", - "Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!", "Your card was successfully removed!": "Kortet ditt ble fjernet!", "Your card was successfully saved!": "Kortet ditt ble lagret!", + "Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!", "Your current level": "Ditt nåværende nivå", "Your details": "Dine detaljer", "Your level": "Ditt nivå", @@ -255,22 +278,5 @@ "Zip code": "Post kode", "Zoo": "Dyrehage", "Zoom in": "Zoom inn", - "Zoom out": "Zoom ut", - "as of today": "per idag", - "by": "innen", - "characters": "tegn", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", - "hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet", - "km to city center": "km til sentrum", - "next level:": "Neste nivå:", - "night": "natt", - "nights": "netter", - "number": "antall", - "or": "eller", - "points": "poeng", - "special character": "spesiell karakter", - "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", - "to": "til", - "uppercase letter": "stor bokstav" -} \ No newline at end of file + "Zoom out": "Zoom ut" +} diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 946161111..4aa850ebb 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -14,10 +14,12 @@ "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": "från och med idag", "As our": "Som vår {level}", "As our Close Friend": "Som vår nära vän", "At latest": "Senast", "At the hotel": "På hotellet", + "Attractions": "Sevärdheter", "Back to scandichotels.com": "Tillbaka till scandichotels.com", "Bed type": "Sängtyp", "Book": "Boka", @@ -28,7 +30,10 @@ "Breakfast excluded": "Frukost ingår ej", "Breakfast included": "Frukost ingår", "Bus terminal": "Bussterminal", + "Business": "Business", + "by": "innan", "Cancel": "Avbryt", + "characters": "tecken", "Check in": "Checka in", "Check out": "Checka ut", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Kolla in kreditkorten som sparats i din profil. Betala med ett sparat kort när du är inloggad för en smidigare webbupplevelse.", @@ -70,9 +75,9 @@ "Explore all levels and benefits": "Utforska alla nivåer och fördelar", "Explore nearby": "Utforska i närheten", "Extras to your booking": "Extra tillval till din bokning", - "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.", "Fair": "Mässa", + "FAQ": "FAQ", "Find booking": "Hitta bokning", "Find hotels": "Hitta hotell", "Flexibility": "Flexibilitet", @@ -89,11 +94,15 @@ "Hotel": "Hotell", "Hotel facilities": "Hotellfaciliteter", "Hotel surroundings": "Hotellomgivning", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet", "Hotels": "Hotell", "How do you want to sleep?": "Hur vill du sova?", "How it works": "Hur det fungerar", "Image gallery": "Bildgalleri", "Join Scandic Friends": "Gå med i Scandic Friends", + "km to city center": "km till stadens centrum", "Language": "Språk", "Latest searches": "Senaste sökningarna", "Level": "Nivå", @@ -105,6 +114,7 @@ "Level 6": "Nivå 6", "Level 7": "Nivå 7", "Level up to unlock": "Levla upp för att låsa upp", + "Location": "Plats", "Locations": "Platser", "Log in": "Logga in", "Log in here": "Logga in här", @@ -119,9 +129,9 @@ "Member price": "Medlemspris", "Member price from": "Medlemspris från", "Members": "Medlemmar", + "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", "Membership ID copied to clipboard": "Medlems-ID kopierat till urklipp", - "Membership cards": "Medlemskort", "Menu": "Meny", "Modify": "Ändra", "Month": "Månad", @@ -136,6 +146,9 @@ "Nearby companies": "Närliggande företag", "New password": "Nytt lösenord", "Next": "Nästa", + "next level:": "Nästa nivå:", + "night": "natt", + "nights": "nätter", "Nights needed to level up": "Nätter som behövs för att gå upp i nivå", "No content published": "Inget innehåll publicerat", "No matching location found": "Ingen matchande plats hittades", @@ -146,12 +159,15 @@ "Non-refundable": "Ej återbetalningsbar", "Not found": "Hittades inte", "Nr night, nr adult": "{nights, number} natt, {adults, number} vuxen", + "number": "nummer", "On your journey": "På din resa", "Open": "Öppna", "Open language menu": "Öppna språkmenyn", "Open menu": "Öppna menyn", "Open my pages menu": "Öppna mina sidor menyn", + "or": "eller", "Overview": "Översikt", + "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", "Password": "Lösenord", "Pay later": "Betala senare", @@ -161,6 +177,7 @@ "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", + "points": "poäng", "Points": "Poäng", "Points being calculated": "Poäng beräknas", "Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021", @@ -169,6 +186,7 @@ "Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå", "Previous victories": "Tidigare segrar", "Public price from": "Offentligt pris från", + "Public transport": "Kollektivtrafik", "Read more": "Läs mer", "Read more about the hotel": "Läs mer om hotellet", "Remove card from member profile": "Ta bort kortet från medlemsprofilen", @@ -192,6 +210,7 @@ "Select language": "Välj språk", "Select your language": "Välj ditt språk", "Shopping": "Shopping", + "Shopping & Dining": "Shopping & Mat", "Show all amenities": "Visa alla bekvämligheter", "Show less": "Visa mindre", "Show map": "Visa karta", @@ -201,25 +220,29 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.", "Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.", "Something went wrong!": "Något gick fel!", + "special character": "speciell karaktär", + "spendable points expiring by": "{points} poäng förfaller {date}", "Sports": "Sport", "Standard price": "Standardpris", "Street": "Gata", "Successfully updated profile!": "Profilen har uppdaterats framgångsrikt!", "Summary": "Sammanfattning", - "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Berätta för oss vilken information och vilka uppdateringar du vill få och hur genom att klicka på länken nedan.", "Thank you": "Tack", "Theatre": "Teater", "There are no transactions to display": "Det finns inga transaktioner att visa", "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", + "to": "till", "Total Points": "Poäng totalt", "Tourist": "Turist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktioner", "Transportations": "Transport", "Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)", + "TUI Points": "TUI Points", "Type of bed": "Sängtyp", "Type of room": "Rumstyp", + "uppercase letter": "stor bokstav", "Use bonus cheque": "Use bonus cheque", "User information": "Användarinformation", "View as list": "Visa som lista", @@ -245,9 +268,9 @@ "You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.", "You have no previous stays.": "Du har inga tidigare vistelser.", "You have no upcoming stays.": "Du har inga planerade resor.", - "Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!", "Your card was successfully removed!": "Ditt kort har tagits bort!", "Your card was successfully saved!": "Ditt kort har sparats!", + "Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!", "Your current level": "Din nuvarande nivå", "Your details": "Dina uppgifter", "Your level": "Din nivå", @@ -255,22 +278,5 @@ "Zip code": "Postnummer", "Zoo": "Djurpark", "Zoom in": "Zooma in", - "Zoom out": "Zooma ut", - "as of today": "från och med idag", - "by": "innan", - "characters": "tecken", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", - "hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet", - "km to city center": "km till stadens centrum", - "next level:": "Nästa nivå:", - "night": "natt", - "nights": "nätter", - "number": "nummer", - "or": "eller", - "points": "poäng", - "special character": "speciell karaktär", - "spendable points expiring by": "{points} poäng förfaller {date}", - "to": "till", - "uppercase letter": "stor bokstav" -} \ No newline at end of file + "Zoom out": "Zooma ut" +} From 5e8cbb9805aa5b14c8339f619204c76eff69baaa Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 27 Sep 2024 10:24:39 +0200 Subject: [PATCH 31/31] feat(sw-217): removed default style --- components/Blocks/CardsGrid.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/Blocks/CardsGrid.tsx b/components/Blocks/CardsGrid.tsx index a55e43b94..134eeaa41 100644 --- a/components/Blocks/CardsGrid.tsx +++ b/components/Blocks/CardsGrid.tsx @@ -44,7 +44,6 @@ export default function CardsGrid({ secondaryButton={card.secondaryButton} sidePeekButton={card.sidePeekButton} image={card.image} - style="default" /> ) case CardsGridEnum.cards.LoyaltyCard: