Merged in feature/hardcoded-mypages-links (pull request #1325)

Feature/hardcoded mypages links

* feat: wip use hardcoded links

* Merge branch 'master' of bitbucket.org:scandic-swap/web into feature/hardcoded-mypages-links

* feat: use hardcoded links for my pages to support dynamic links

* cleanup

* code fixes

* refactor: restructure MyPagesMobileDropdown component for improved readability

* use util timeout function


Approved-by: Christian Andolf
Approved-by: Linus Flood
This commit is contained in:
Joakim Jäderberg
2025-02-13 09:28:30 +00:00
parent eabe45b73c
commit 6f51130e48
30 changed files with 500 additions and 556 deletions

View File

@@ -4,8 +4,6 @@ import { serverClient } from "@/lib/trpc/server"
import Blocks from "@/components/Blocks"
import SectionHeader from "@/components/Section/Header"
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
import Title from "@/components/TempDesignSystem/Text/Title"
import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext"

View File

@@ -1,5 +1,4 @@
import { Suspense } from "react"
import { setTimeout } from "timers/promises"
import { TIER_TO_FRIEND_MAP } from "@/constants/membershipLevels"
import { env } from "@/env/server"
@@ -8,6 +7,7 @@ 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 { timeout } from "@/utils/timeout"
import { TierLevelCard, TierLevelCardSkeleton } from "./Card/TierLevelCard"
import { LevelUpgradeButton } from "./LevelUpgradeButton"
@@ -60,7 +60,7 @@ function TierLevelCardsSkeleton() {
async function TierLevelCards() {
console.log("[SAS] Fetching tier level cards")
await setTimeout(2_000)
await timeout(2_000)
console.log("[SAS] AFTER Fetching tier level cards")
const user = await getProfile()

View File

@@ -112,7 +112,7 @@ export function MainMenu({
className={`${styles.listWrapper} ${isHamburgerMenuOpen ? styles.isOpen : ""}`}
>
<ul className={styles.linkRow}>
{!isThreeStaticPagesPathnames && !!user ? (
{user ? (
<li className={styles.mobileLinkRow}>
<Link
className={styles.mobileLinkButton}

View File

@@ -1,5 +1,4 @@
"use client"
import { Fragment } from "react"
import { useIntl } from "react-intl"
import { logout } from "@/constants/routes/handleAuth"
@@ -12,71 +11,129 @@ import useLang from "@/hooks/useLang"
import styles from "./my-pages-mobile-dropdown.module.css"
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
import type { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query"
import type { ReactNode } from "react"
type Navigation = Awaited<ReturnType<(typeof navigationQueryRouter)["get"]>>
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
import type { MyPagesLink } from "@/components/MyPages/menuItems"
export default function MyPagesMobileDropdown({
navigation,
primaryLinks,
secondaryLinks,
}: {
navigation: Navigation
primaryLinks: MyPagesLink[]
secondaryLinks: MyPagesLink[]
}) {
const intl = useIntl()
const lang = useLang()
const { toggleDropdown, isMyPagesMobileMenuOpen } = useDropdownStore()
if (!navigation) {
if (primaryLinks.length === 0 && secondaryLinks.length === 0) {
return null
}
const handleOnClick = () => toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)
return (
<nav
className={`${styles.navigationMenu} ${isMyPagesMobileMenuOpen ? styles.navigationMenuIsOpen : ""}`}
>
<Title className={styles.heading} textTransform="capitalize" level="h5">
{navigation.title}
<Title textTransform="capitalize" level="h5">
<div className={styles.heading}>
{intl.formatMessage({ id: "My pages" })}
</div>
</Title>
{navigation.menuItems.map((menuItem, idx) => (
<Fragment key={`${menuItem.display_sign_out_link}-${idx}`}>
<div className={styles.dividerWrapper}>
<Divider color="subtle" />
</div>
<ul className={styles.dropdownWrapper}>
<ul className={styles.dropdownLinks}>
{menuItem.links.map((link) => (
<li key={link.uid}>
<Link
href={link.originalUrl || link.url}
partialMatch
size={menuItem.display_sign_out_link ? "small" : "regular"}
variant="myPageMobileDropdown"
color="burgundy"
onClick={() =>
toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)
}
>
{link.linkText}
</Link>
</li>
))}
{menuItem.display_sign_out_link && lang ? (
<li>
<Link
href={logout[lang]}
prefetch={false}
size="small"
color="burgundy"
variant="myPageMobileDropdown"
>
{intl.formatMessage({ id: "Log out" })}
</Link>
</li>
) : null}
</ul>
</ul>
</Fragment>
))}
<List>
<PrimaryLinks
primaryLinks={primaryLinks}
handleOnClick={handleOnClick}
/>
</List>
<List>
<SecondaryLinks
secondaryLinks={secondaryLinks}
handleOnClick={handleOnClick}
/>
</List>
</nav>
)
}
function List({ children }: { children: ReactNode }) {
return (
<>
<div className={styles.dividerWrapper}>
<Divider color="subtle" />
</div>
<ul className={styles.dropdownWrapper}>
<ul className={styles.dropdownLinks}>{children}</ul>
</ul>
</>
)
}
function PrimaryLinks({
primaryLinks,
handleOnClick,
}: {
primaryLinks: MyPagesLink[]
handleOnClick: () => void
}) {
return (
<>
{primaryLinks.map((link, i) => (
<li key={link.href + i}>
<Link
href={link.href}
partialMatch
size={"regular"}
variant="myPageMobileDropdown"
color="burgundy"
onClick={handleOnClick}
>
{link.text}
</Link>
</li>
))}
</>
)
}
function SecondaryLinks({
secondaryLinks,
handleOnClick,
}: {
secondaryLinks: MyPagesLink[]
handleOnClick: () => void
}) {
const intl = useIntl()
const lang = useLang()
return (
<>
{secondaryLinks.map((link, i) => (
<li key={link.href + i}>
<Link
href={link.href}
partialMatch
size={"small"}
variant="myPageMobileDropdown"
color="burgundy"
onClick={handleOnClick}
>
{link.text}
</Link>
</li>
))}
<li>
<Link
href={logout[lang]}
prefetch={false}
size="small"
color="burgundy"
variant="myPageMobileDropdown"
>
{intl.formatMessage({ id: "Log out" })}
</Link>
</li>
</>
)
}

View File

@@ -3,10 +3,13 @@ import { env } from "@/env/server"
import {
getCurrentHeader,
getLanguageSwitcher,
getMyPagesNavigation,
getName,
} from "@/lib/trpc/memoizedRequests"
import {
getPrimaryLinks,
getSecondaryLinks,
} from "@/components/MyPages/menuItems"
import { getLang } from "@/i18n/serverContext"
import LanguageSwitcher from "./LanguageSwitcher"
@@ -18,14 +21,17 @@ import TopMenu from "./TopMenu"
import styles from "./header.module.css"
export default async function Header() {
const [data, user, languages, navigation] = await Promise.all([
getCurrentHeader(getLang()),
getName(),
getLanguageSwitcher(),
getMyPagesNavigation(),
])
const lang = getLang()
const [data, user, languages, primaryLinks, secondaryLinks] =
await Promise.all([
getCurrentHeader(lang),
getName(),
getLanguageSwitcher(),
getPrimaryLinks({ lang }),
getSecondaryLinks({ lang }),
])
if (!navigation || !languages || !data?.header) {
if (!languages || !data?.header) {
return null
}
@@ -53,7 +59,10 @@ export default async function Header() {
topMenuMobileLinks={topMenuMobileLinks}
languageSwitcher={<LanguageSwitcher urls={languages.urls} />}
myPagesMobileDropdown={
<MyPagesMobileDropdown navigation={navigation} />
<MyPagesMobileDropdown
primaryLinks={primaryLinks}
secondaryLinks={secondaryLinks}
/>
}
bookingHref={homeHref}
user={user}

View File

@@ -10,7 +10,7 @@ import SkeletonShimmer from "@/components/SkeletonShimmer"
import Body from "@/components/TempDesignSystem/Text/Body"
import useClickOutside from "@/hooks/useClickOutside"
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
import { getInitials } from "@/utils/user"
import {type FriendsMembership,getInitials } from "@/utils/user"
import Avatar from "../Avatar"
import MainMenuButton from "../MainMenuButton"
@@ -19,11 +19,22 @@ import MyPagesMenuContent from "../MyPagesMenuContent"
import styles from "./myPagesMenu.module.css"
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
import type { MyPagesMenuProps } from "@/types/components/header/myPagesMenu"
import type { User } from "@/types/user"
import type { MyPagesLink } from "@/components/MyPages/menuItems"
import type { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output"
export type MyPagesMenuProps = {
primaryLinks: MyPagesLink[]
secondaryLinks: MyPagesLink[]
user: Pick<User, "firstName" | "lastName">
membership?: FriendsMembership | null
membershipLevel: LoyaltyLevel | null
}
export default function MyPagesMenu({
membership,
navigation,
primaryLinks,
secondaryLinks,
user,
membershipLevel,
}: MyPagesMenuProps) {
@@ -65,7 +76,8 @@ export default function MyPagesMenu({
<div className={styles.dropdown}>
<MyPagesMenuContent
membershipLevel={membershipLevel}
navigation={navigation}
primaryLinks={primaryLinks}
secondaryLinks={secondaryLinks}
user={user}
membership={membership}
toggleOpenStateFn={() =>

View File

@@ -14,17 +14,19 @@ import { useTrapFocus } from "@/hooks/useTrapFocus"
import styles from "./myPagesMenuContent.module.css"
import type { MyPagesMenuContentProps } from "@/types/components/header/myPagesMenu"
import type { MyPagesMenuProps } from "../MyPagesMenu"
type Props = MyPagesMenuProps & { toggleOpenStateFn: () => void }
export default function MyPagesMenuContent({
membership,
navigation,
primaryLinks,
secondaryLinks,
toggleOpenStateFn,
user,
membershipLevel,
}: MyPagesMenuContentProps) {
}: Props) {
const intl = useIntl()
const lang = useLang()
const myPagesMenuContentRef = useTrapFocus()
const membershipPoints = membership?.currentPoints
@@ -32,7 +34,8 @@ export default function MyPagesMenuContent({
membershipLevel && membershipPoints
? `${styles.intro}`
: `${styles.intro} ${styles.noMembership}`
if (!navigation) {
if (primaryLinks.length === 0 && secondaryLinks.length === 0) {
return null
}
@@ -59,40 +62,81 @@ export default function MyPagesMenuContent({
</div>
<ul className={styles.groups}>
{navigation.menuItems.map((menuItem, idx) => (
<li key={`${menuItem.display_sign_out_link}-${idx}`}>
<Divider color="subtle" className={styles.divider} />
<ul className={styles.menuItems}>
{menuItem.links.map((link) => (
<li key={link.uid}>
<Link
href={link.originalUrl || link.url}
onClick={toggleOpenStateFn}
variant="menu"
weight={menuItem.display_sign_out_link ? undefined : "bold"}
className={styles.link}
>
{link.linkText}
<ArrowRightIcon className={styles.arrow} color="burgundy" />
</Link>
</li>
))}
{menuItem.display_sign_out_link ? (
<li>
<Link
href={logout[lang]}
prefetch={false}
variant="menu"
className={styles.link}
>
{intl.formatMessage({ id: "Log out" })}
</Link>
</li>
) : null}
</ul>
</li>
))}
<li>
<Divider color="subtle" className={styles.divider} />
<PrimaryLinks
primaryLinks={primaryLinks}
toggleOpenStateFn={toggleOpenStateFn}
/>
<Divider color="subtle" className={styles.divider} />
<SecondaryLinks
secondaryLinks={secondaryLinks}
toggleOpenStateFn={toggleOpenStateFn}
/>
</li>
</ul>
</nav>
)
}
function PrimaryLinks({
primaryLinks,
toggleOpenStateFn,
}: Pick<Props, "primaryLinks"> & { toggleOpenStateFn: () => void }) {
return (
<ul className={styles.menuItems}>
{primaryLinks.map((link, i) => (
<li key={link.href + i}>
<Link
href={link.href}
onClick={toggleOpenStateFn}
variant="menu"
weight={"bold"}
className={styles.link}
>
{link.text}
<ArrowRightIcon className={styles.arrow} color="burgundy" />
</Link>
</li>
))}
</ul>
)
}
function SecondaryLinks({
secondaryLinks,
toggleOpenStateFn,
}: Pick<Props, "secondaryLinks"> & { toggleOpenStateFn: () => void }) {
const intl = useIntl()
const lang = useLang()
return (
<ul className={styles.menuItems}>
{secondaryLinks.map((link, i) => (
<li key={link.href + i}>
<Link
href={link.href}
onClick={toggleOpenStateFn}
variant="menu"
className={styles.link}
>
{link.text}
<ArrowRightIcon className={styles.arrow} color="burgundy" />
</Link>
</li>
))}
<li>
<Link
href={logout[lang]}
prefetch={false}
variant="menu"
className={styles.link}
>
{intl.formatMessage({ id: "Log out" })}
</Link>
</li>
</ul>
)
}

View File

@@ -1,13 +1,14 @@
import { MembershipLevelEnum } from "@/constants/membershipLevels"
import {
getMembershipLevelSafely,
getMyPagesNavigation,
getName,
} from "@/lib/trpc/memoizedRequests"
import { getMembershipLevelSafely, getName } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server"
import LoginButton from "@/components/LoginButton"
import {
getPrimaryLinks,
getSecondaryLinks,
} from "@/components/MyPages/menuItems"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import Avatar from "../Avatar"
import MyPagesMenu, { MyPagesMenuSkeleton } from "../MyPagesMenu"
@@ -18,12 +19,15 @@ import MyPagesMobileMenu, {
import styles from "./myPagesMenuWrapper.module.css"
export default async function MyPagesMenuWrapper() {
const [intl, myPagesNavigation, user, membership] = await Promise.all([
getIntl(),
getMyPagesNavigation(),
getName(),
getMembershipLevelSafely(),
])
const lang = getLang()
const [intl, user, membership, primaryLinks, secondaryLinks] =
await Promise.all([
getIntl(),
getName(),
getMembershipLevelSafely(),
getPrimaryLinks({ lang }),
getSecondaryLinks({ lang }),
])
const membershipLevel = membership?.membershipLevel
? await serverClient().contentstack.loyaltyLevels.byLevel({
@@ -38,13 +42,15 @@ export default async function MyPagesMenuWrapper() {
<MyPagesMenu
membershipLevel={membershipLevel}
membership={membership}
navigation={myPagesNavigation}
primaryLinks={primaryLinks}
secondaryLinks={secondaryLinks}
user={user}
/>
<MyPagesMobileMenu
membershipLevel={membershipLevel}
membership={membership}
navigation={myPagesNavigation}
primaryLinks={primaryLinks}
secondaryLinks={secondaryLinks}
user={user}
/>
</>

View File

@@ -17,12 +17,13 @@ import MyPagesMenuContent from "../MyPagesMenuContent"
import styles from "./myPagesMobileMenu.module.css"
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
import type { MyPagesMenuProps } from "@/types/components/header/myPagesMenu"
import type { MyPagesMenuProps } from "../MyPagesMenu"
export default function MyPagesMobileMenu({
membershipLevel,
membership,
navigation,
primaryLinks,
secondaryLinks,
user,
}: MyPagesMenuProps) {
const intl = useIntl()
@@ -65,7 +66,8 @@ export default function MyPagesMobileMenu({
<MyPagesMenuContent
membershipLevel={membershipLevel}
membership={membership}
navigation={navigation}
primaryLinks={primaryLinks}
secondaryLinks={secondaryLinks}
user={user}
toggleOpenStateFn={() =>
toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)

View File

@@ -1,8 +1,9 @@
import { Fragment } from "react"
import { logout } from "@/constants/routes/handleAuth"
import { getMyPagesNavigation } from "@/lib/trpc/memoizedRequests"
import {
getPrimaryLinks,
getSecondaryLinks,
} from "@/components/MyPages/menuItems"
import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
@@ -12,50 +13,85 @@ import { getLang } from "@/i18n/serverContext"
import styles from "./sidebar.module.css"
export default async function SidebarMyPages() {
const navigation = await getMyPagesNavigation()
const intl = await getIntl()
return (
<aside className={styles.sidebar}>
<nav className={styles.nav}>
<Subtitle type="two" color="baseTextHighContrast">
{navigation?.title}
{intl.formatMessage({ id: "My pages" })}
</Subtitle>
{navigation?.menuItems.map((menuItem, idx) => (
<Fragment key={`${menuItem.display_sign_out_link}-${idx}`}>
<Divider color="beige" />
<ul className={styles.list}>
{menuItem.links.map((link) => (
<li key={link.uid}>
<Link
color="burgundy"
href={link.originalUrl || link.url}
partialMatch
prefetch={true}
size={menuItem.display_sign_out_link ? "small" : "regular"}
variant="sidebar"
>
{link.linkText}
</Link>
</li>
))}
{menuItem.display_sign_out_link ? (
<li>
<Link
color="burgundy"
href={logout[getLang()]}
prefetch={false}
size="small"
variant="sidebar"
>
{intl.formatMessage({ id: "Log out" })}
</Link>
</li>
) : null}
</ul>
</Fragment>
))}
<PrimaryLinks />
<SecondaryLinks />
</nav>
</aside>
)
}
async function PrimaryLinks() {
const lang = getLang()
const links = await getPrimaryLinks({ lang })
return (
<>
<Divider color="beige" />
<ul className={styles.list}>
{links.map((link) => (
<li key={link.href}>
<Link
color="burgundy"
href={link.href}
partialMatch
prefetch={true}
size={"regular"}
variant="sidebar"
>
{link.text}
</Link>
</li>
))}
</ul>
</>
)
}
async function SecondaryLinks() {
const lang = getLang()
const links = await getSecondaryLinks({ lang })
const intl = await getIntl()
return (
<>
<Divider color="beige" />
<ul className={styles.list}>
{links.map((link) => (
<li key={link.href}>
<Link
color="burgundy"
href={link.href}
partialMatch
prefetch={true}
size={"small"}
variant="sidebar"
>
{link.text}
</Link>
</li>
))}
<li>
<Link
color="burgundy"
href={logout[lang]}
partialMatch
prefetch={false}
size={"small"}
variant="sidebar"
>
{intl.formatMessage({ id: "Log out" })}
</Link>
</li>
</ul>
</>
)
}

View File

@@ -0,0 +1,101 @@
import * as routes from "@/constants/routes/myPages"
import { env } from "@/env/server"
import { getIntl } from "@/i18n"
import { safeTry } from "@/utils/safeTry"
import type { ReactNode } from "react"
import type { Lang } from "../../constants/languages"
export type MyPagesLink = {
text: ReactNode
href: string
}
type Args = {
lang: Lang
}
export async function getPrimaryLinks({ lang }: Args): Promise<MyPagesLink[]> {
const intl = await getIntl()
const scandicSasPromise = safeTry(isScandicXSASActive())
const teamMemberPromise = safeTry(showTeamMemberCard())
const [showSASLink] = await scandicSasPromise
const [showTeamMemberLink] = await teamMemberPromise
const menuItems: MyPagesLink[] = [
{
text: intl.formatMessage({ id: "Overview" }),
href: routes.overview[lang],
},
{
text: intl.formatMessage({ id: "My Points" }),
href: routes.points[lang],
},
{
text: intl.formatMessage({ id: "My Stays" }),
href: routes.stays[lang],
},
{
text: intl.formatMessage({ id: "My Benefits" }),
href: routes.benefits[lang],
},
]
if (showSASLink) {
menuItems.push({
text: intl.formatMessage({ id: "Scandic ♥ SAS" }),
href: routes.scandicXSAS[lang],
})
}
if (showTeamMemberLink) {
menuItems.push({
text: intl.formatMessage({ id: "Team Member Card" }),
href: "#",
})
}
return menuItems
}
export async function getSecondaryLinks({
lang,
}: Args): Promise<MyPagesLink[]> {
const intl = await getIntl()
const menuItems: MyPagesLink[] = [
{
text: intl.formatMessage({ id: "About Scandic Friends" }),
href: routes.scandicFriends[lang],
},
{
text: intl.formatMessage({ id: "My Profile" }),
href: routes.profile[lang],
},
]
return menuItems
}
async function isScandicXSASActive() {
async function checkIfLinked() {
// TODO: Implement this check
return true
}
const isLinked = await checkIfLinked()
return env.SAS_ENABLED && isLinked
}
async function showTeamMemberCard() {
async function getIsTeamMember() {
// TODO: Implement this check
return false
}
const isTeamMember = await getIsTeamMember()
return isTeamMember
}

View File

@@ -7,14 +7,24 @@
* These are routes that define code entries for My pages
*/
/** @type {import('@/types/routes').LangRoute} */
export const scandicFriends = {
da: "/da/scandic-friends",
de: "/de/scandic-friends",
en: "/en/scandic-friends",
fi: "/fi/scandic-friends",
no: "/no/scandic-friends",
sv: "/sv/scandic-friends",
}
/** @type {import('@/types/routes').LangRoute} */
export const myPages = {
da: "/da/scandic-friends/mine-sider",
de: "/de/scandic-friends/mein-bereich",
en: "/en/scandic-friends/my-pages",
fi: "/fi/scandic-friends/omat-sivut",
no: "/no/scandic-friends/mine-sider",
sv: "/sv/scandic-friends/mina-sidor",
da: `${scandicFriends.da}/mine-sider`,
de: `${scandicFriends.de}/mein-bereich`,
en: `${scandicFriends.en}/my-pages`,
fi: `${scandicFriends.fi}/omat-sivut`,
no: `${scandicFriends.no}/mine-sider`,
sv: `${scandicFriends.sv}/mina-sidor`,
}
/** @type {import('@/types/routes').LangRoute} */
@@ -77,3 +87,13 @@ export const stays = {
no: `${myPages.no}/opphold`,
sv: `${myPages.sv}/vistelser`,
}
/** @type {import('@/types/routes').LangRoute} */
export const scandicXSAS = {
da: `${myPages.da}/scandic-x-sas`,
de: `${myPages.de}/scandic-x-sas`,
en: `${myPages.en}/scandic-x-sas`,
fi: `${myPages.fi}/scandic-x-sas`,
no: `${myPages.no}/scandic-x-sas`,
sv: `${myPages.sv}/scandic-x-sas`,
}

View File

@@ -6,6 +6,7 @@
"A photo of the room": "Et foto af værelset",
"ACCE": "Tilgængelighed",
"ALLG": "Allergi",
"About Scandic Friends": "Om Scandic Friends",
"About accessibility": "Om tilgængelighed",
"About meetings & conferences": "Om møder og konferencer",
"About parking": "Om parkering",
@@ -344,6 +345,10 @@
"Multiroom with voucher error": "Det er ikke muligt at booke flere værelser med gavekort.",
"Museum": "Museum",
"Museums": "Museer",
"My Benefits": "Mine Fordele",
"My Points": "Mine Point",
"My Profile": "Min profil",
"My Stays": "Mine Ophold",
"My communication preferences": "Mine kommunikationspræferencer",
"My membership cards": "Mine medlemskort",
"My pages": "Mine sider",
@@ -486,6 +491,7 @@
"Save card to profile": "Save card to profile",
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS",
"Search": "Søge",
"See all": "Se alle",
"See all photos": "Se alle billeder",

View File

@@ -6,6 +6,7 @@
"A photo of the room": "Ein Foto des Zimmers",
"ACCE": "Zugänglichkeit",
"ALLG": "Allergie",
"About Scandic Friends": "Über Scandic Friends",
"About accessibility": "Über Barrierefreiheit",
"About meetings & conferences": "Über Meetings & Konferenzen",
"About parking": "Über das Parken",
@@ -345,6 +346,10 @@
"Multiroom with voucher error": "Die Buchung mehrerer Zimmer ist nicht mit einem Gutschein möglich",
"Museum": "Museum",
"Museums": "Museen",
"My Benefits": "Meine Vorteile",
"My Points": "Meine Punkte",
"My Profile": "Mein Profil",
"My Stays": "Meine Aufenthalte",
"My communication preferences": "Meine Kommunikationseinstellungen",
"My membership cards": "Meine Mitgliedskarten",
"My pages": "Meine Seiten",
@@ -487,6 +492,7 @@
"Save card to profile": "Save card to profile",
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS",
"Search": "Suchen",
"See all": "Alle anzeigen",
"See all photos": "Alle Fotos ansehen",

View File

@@ -7,6 +7,7 @@
"A photo of the room": "A photo of the room",
"ACCE": "Accessibility",
"ALLG": "Allergy",
"About Scandic Friends": "About Scandic Friends",
"About accessibility": "About accessibility",
"About meetings & conferences": "About meetings & conferences",
"About parking": "About parking",
@@ -349,6 +350,10 @@
"Multiroom with voucher error": "Multiroom booking is not compatible with a code or voucher",
"Museum": "Museum",
"Museums": "Museums",
"My Benefits": "My Benefits",
"My Points": "My points",
"My Profile": "My Profile",
"My Stays": "My Stays",
"My communication preferences": "My communication preferences",
"My membership cards": "My membership cards",
"My pages": "My pages",
@@ -492,6 +497,7 @@
"Save card to profile": "Save card to profile",
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS",
"Search": "Search",
"See all": "See all",
"See all photos": "See all photos",

View File

@@ -6,6 +6,7 @@
"A photo of the room": "Kuva huoneesta",
"ACCE": "Saavutettavuus",
"ALLG": "Allergia",
"About Scandic Friends": "Scandic Friends -ohjelmasta",
"About accessibility": "Tietoja saavutettavuudesta",
"About meetings & conferences": "Tietoja kokouksista ja konferensseista",
"About parking": "Tietoja pysäköinnistä",
@@ -344,6 +345,10 @@
"Multiroom with voucher error": "Lahjakortilla ei voi varata monta huonetta",
"Museum": "Museo",
"Museums": "Museot",
"My Benefits": "Etuni",
"My Points": "Pisteeni",
"My Profile": "Profiilini",
"My Stays": "Varaukseni",
"My communication preferences": "Viestintämieltymykseni",
"My membership cards": "Jäsenkorttini",
"My pages": "Omat sivut",
@@ -487,6 +492,7 @@
"Save card to profile": "Save card to profile",
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS",
"Search": "Haku",
"See all": "Katso kaikki",
"See all photos": "Katso kaikki kuvat",

View File

@@ -6,6 +6,7 @@
"A photo of the room": "Et bilde av rommet",
"ACCE": "Tilgjengelighet",
"ALLG": "Allergi",
"About Scandic Friends": "Om Scandic Friends",
"About accessibility": "Om tilgjengelighet",
"About meetings & conferences": "Om møter og konferanser",
"About parking": "Om parkering",
@@ -343,6 +344,10 @@
"Multiroom with voucher error": "Bestilling av flere rom er ikke mulig når du bestiller med voucher",
"Museum": "Museum",
"Museums": "Museums",
"My Benefits": "Mine fordeler",
"My Points": "Mine poeng",
"My Profile": "Min profil",
"My Stays": "Mine opphold",
"My communication preferences": "Mine kommunikasjonspreferanser",
"My membership cards": "Mine medlemskort",
"My pages": "Mine sider",
@@ -485,6 +490,7 @@
"Save card to profile": "Save card to profile",
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS",
"Search": "Søk",
"See all": "Se alle",
"See all photos": "Se alle bilder",

View File

@@ -6,6 +6,7 @@
"A photo of the room": "Ett foto av rummet",
"ACCE": "Tillgänglighet",
"ALLG": "Allergi",
"About Scandic Friends": "Om Scandic Friends",
"About accessibility": "Om tillgänglighet",
"About meetings & conferences": "Om möten & konferenser",
"About parking": "Om parkering",
@@ -343,6 +344,10 @@
"Multiroom with voucher error": "Flera rum kan inte bokas samtidigt med presentkort",
"Museum": "Museum",
"Museums": "Museer",
"My Benefits": "Mina förmåner",
"My Points": "Mina poäng",
"My Profile": "Min profil",
"My Stays": "Mina vistelser",
"My communication preferences": "Mina kommunikationspreferenser",
"My membership cards": "Mina medlemskort",
"My pages": "Mina sidor",
@@ -485,6 +490,7 @@
"Save card to profile": "Save card to profile",
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS",
"Search": "Sök",
"See all": "Se alla",
"See all photos": "Se alla foton",

View File

@@ -120,12 +120,6 @@ export const getCurrentFooter = cache(async function getMemoizedCurrentFooter(
return serverClient().contentstack.base.currentFooter({ lang })
})
export const getMyPagesNavigation = cache(
async function getMemoizedMyPagesNavigation() {
return serverClient().contentstack.myPages.navigation.get()
}
)
export const getLanguageSwitcher = cache(
async function getMemoizedLanguageSwitcher() {
return serverClient().contentstack.languageSwitcher.get()

View File

@@ -14,7 +14,6 @@ import { languageSwitcherRouter } from "./languageSwitcher"
import { loyaltyLevelRouter } from "./loyaltyLevel"
import { loyaltyPageRouter } from "./loyaltyPage"
import { metadataRouter } from "./metadata"
import { myPagesRouter } from "./myPages"
import { partnerRouter } from "./partner"
import { rewardRouter } from "./reward"
import { startPageRouter } from "./startPage"
@@ -32,7 +31,6 @@ export const contentstackRouter = router({
destinationOverviewPage: destinationOverviewPageRouter,
destinationCountryPage: destinationCountryPageRouter,
destinationCityPage: destinationCityPageRouter,
myPages: myPagesRouter,
metadata: metadataRouter,
rewards: rewardRouter,
loyaltyLevels: loyaltyLevelRouter,

View File

@@ -1,7 +0,0 @@
import { router } from "@/server/trpc"
import { navigationRouter } from "./navigation"
export const myPagesRouter = router({
navigation: navigationRouter,
})

View File

@@ -1,5 +0,0 @@
import { mergeRouters } from "@/server/trpc"
import { navigationQueryRouter } from "./query"
export const navigationRouter = mergeRouters(navigationQueryRouter)

View File

@@ -1,90 +0,0 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
import { linkRefsUnionSchema, linkUnionSchema } from "../../schemas/pageLinks"
import { systemSchema } from "../../schemas/system"
const pageConnection = z.object({
edges: z.array(
z.object({
node: linkUnionSchema,
})
),
})
const pageConnectionRefs = z.object({
edges: z.array(
z.object({
node: linkRefsUnionSchema,
})
),
})
export const navigationRefsPayloadSchema = z.object({
all_navigation_my_pages: z.object({
items: z.array(
z.object({
menu_items: z.array(
z.object({
links: z.array(
z.object({
page: pageConnectionRefs,
})
),
})
),
system: systemSchema,
})
),
}),
})
export type GetNavigationMyPagesRefsData = z.infer<
typeof navigationRefsPayloadSchema
>
const menuItems = z.array(
z.object({
display_sign_out_link: z.boolean(),
links: z.array(
z.object({
link_text: z.string().default(""),
page: pageConnection,
})
),
})
)
export type MenuItems = z.infer<typeof menuItems>
export const navigationPayloadSchema = z.object({
all_navigation_my_pages: z.object({
items: z.array(
z.object({
menu_items: menuItems,
title: z.string(),
})
),
}),
})
export type GetNavigationMyPagesData = z.infer<typeof navigationPayloadSchema>
const link = z.object({
lang: z.nativeEnum(Lang),
linkText: z.string(),
uid: z.string(),
url: z.string(),
originalUrl: z.string().optional(),
})
export const getNavigationSchema = z.object({
menuItems: z.array(
z.object({
display_sign_out_link: z.boolean(),
links: z.array(link),
})
),
title: z.string(),
})

View File

@@ -1,200 +0,0 @@
import { metrics } from "@opentelemetry/api"
import {
GetNavigationMyPages,
GetNavigationMyPagesRefs,
} from "@/lib/graphql/Query/AccountPage/Navigation.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { contentstackBaseProcedure, router } from "@/server/trpc"
import {
generateRefsResponseTag,
generateTag,
generateTags,
} from "@/utils/generateTag"
import {
type GetNavigationMyPagesData,
type GetNavigationMyPagesRefsData,
getNavigationSchema,
navigationPayloadSchema,
navigationRefsPayloadSchema,
} from "./output"
import { getConnections, mapMenuItems } from "./utils"
const meter = metrics.getMeter("trpc.navigationMyPages")
const getNavigationMyPagesRefsCounter = meter.createCounter(
"trpc.contentstack.navigationMyPages.refs.get"
)
const getNavigationMyPagesRefsSuccessCounter = meter.createCounter(
"trpc.contentstack.navigationMyPages.refs.get-success"
)
const getNavigationMyPagesRefsFailCounter = meter.createCounter(
"trpc.contentstack.navigationMyPages.refs.get-fail"
)
const getNavigationMyPagesCounter = meter.createCounter(
"trpc.contentstack.navigationMyPages.get"
)
const getNavigationMyPagesSuccessCounter = meter.createCounter(
"trpc.contentstack.navigationMyPages.get-success"
)
const getNavigationMyPagesFailCounter = meter.createCounter(
"trpc.contentstack.navigationMyPages.get-fail"
)
export const navigationQueryRouter = router({
get: contentstackBaseProcedure.query(async function ({ ctx }) {
const { lang } = ctx
getNavigationMyPagesRefsCounter.add(1, { lang })
console.info(
"contentstack.myPages.navigation.refs start",
JSON.stringify({ query: { lang } })
)
const refsResponse = await request<GetNavigationMyPagesRefsData>(
GetNavigationMyPagesRefs,
{ locale: lang },
{
cache: "force-cache",
next: {
tags: [generateRefsResponseTag(lang, "navigation_my_pages")],
},
}
)
if (!refsResponse.data) {
const notFoundError = notFound(refsResponse)
getNavigationMyPagesRefsFailCounter.add(1, {
lang,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
"contentstack.myPages.navigation.refs not found error",
JSON.stringify({
query: {
lang,
},
error: { code: notFoundError.code },
})
)
throw notFoundError
}
const validatedMyPagesNavigationRefs =
navigationRefsPayloadSchema.safeParse(refsResponse.data)
if (!validatedMyPagesNavigationRefs.success) {
getNavigationMyPagesRefsFailCounter.add(1, {
lang,
error_type: "validation_error",
error: JSON.stringify(validatedMyPagesNavigationRefs.error),
})
console.error(
"contentstack.myPages.navigation.refs validation error",
JSON.stringify({
query: {
lang,
},
error: validatedMyPagesNavigationRefs.error,
})
)
return null
}
getNavigationMyPagesRefsSuccessCounter.add(1, { lang })
console.info(
"contentstack.myPages.navigation.refs success",
JSON.stringify({ query: { lang } })
)
const connections = getConnections(validatedMyPagesNavigationRefs.data)
const tags = [
generateTags(lang, connections),
generateTag(
lang,
validatedMyPagesNavigationRefs.data.all_navigation_my_pages.items[0]
.system.uid
),
].flat()
getNavigationMyPagesCounter.add(1)
console.info(
"contentstack.myPages.navigation start",
JSON.stringify({ query: { lang } })
)
const response = await request<GetNavigationMyPagesData>(
GetNavigationMyPages,
{ locale: lang },
{
cache: "force-cache",
next: { tags },
}
)
if (!response.data) {
const notFoundError = notFound(response)
getNavigationMyPagesFailCounter.add(1, {
lang,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error("contentstack.myPages.navigation not found error", {
query: {
lang,
},
error: { code: notFoundError.code },
})
throw notFoundError
}
const validatedMyPagesNavigation = navigationPayloadSchema.safeParse(
response.data
)
if (!validatedMyPagesNavigation.success) {
getNavigationMyPagesFailCounter.add(1, {
lang,
error_type: "validation_error",
error: JSON.stringify(validatedMyPagesNavigation.error),
})
console.error(
"contentstack.myPages.navigation.payload validation error",
JSON.stringify({
query: { lang },
error: validatedMyPagesNavigation.error,
})
)
return null
}
const menuItem =
validatedMyPagesNavigation.data.all_navigation_my_pages.items[0]
const nav = {
menuItems: mapMenuItems(menuItem.menu_items),
title: menuItem.title,
}
const validatedNav = getNavigationSchema.safeParse(nav)
if (!validatedNav.success) {
getNavigationMyPagesFailCounter.add(1, {
lang,
error_type: "validation_error",
error: JSON.stringify(validatedNav.error),
})
console.error(
"contentstack.myPages.navigation validation error",
JSON.stringify({
query: { lang },
error: validatedNav.error,
})
)
console.error(validatedNav.error)
return null
}
getNavigationMyPagesSuccessCounter.add(1, { lang })
console.info(
"contentstack.myPages.navigation success",
JSON.stringify({ query: { lang } })
)
return validatedNav.data
}),
})

View File

@@ -1,52 +0,0 @@
import { removeMultipleSlashes } from "@/utils/url"
import { ContentEnum } from "@/types/enums/content"
import type { Edges } from "@/types/requests/utils/edges"
import type { NodeRefs } from "@/types/requests/utils/refs"
import type { GetNavigationMyPagesRefsData, MenuItems } from "./output"
export function getConnections(refs: GetNavigationMyPagesRefsData) {
const connections: Edges<NodeRefs>[] = []
refs.all_navigation_my_pages.items.forEach((ref) => {
ref.menu_items.forEach((menuItem) => {
menuItem.links.map((link) => {
connections.push(link.page)
})
})
})
return connections
}
export function mapMenuItems(menuItems: MenuItems) {
return menuItems.map((menuItem) => {
return {
...menuItem,
links: menuItem.links
.filter((link) => {
// If content is unpublished or in other way inaccessible in Contentstack
// there will be no edges, filter out those links.
return !!link.page.edges[0]
})
.map((link) => {
const page = link.page.edges[0].node
let originalUrl = undefined
if (
page.__typename === ContentEnum.blocks.ContentPage ||
page.__typename === ContentEnum.blocks.LoyaltyPage
) {
if (page.web.original_url) {
originalUrl = page.web.original_url
}
}
return {
lang: page.system.locale,
linkText: link.link_text ? link.link_text : page.title,
uid: page.system.uid,
url: removeMultipleSlashes(`/${page.system.locale}/${page.url}`),
originalUrl,
}
}),
}
})
}

View File

@@ -1,8 +1,9 @@
import { setTimeout } from "timers/promises"
import { z } from "zod"
import { protectedProcedure } from "@/server/trpc"
import { timeout } from "@/utils/timeout"
import { getSasToken } from "./getSasToken"
const outputSchema = z.object({
@@ -15,7 +16,7 @@ export const linkAccount = protectedProcedure
const sasAuthToken = getSasToken()
console.log("[SAS] link account")
await setTimeout(1000)
await timeout(1000)
//TODO: Call actual API here
console.log("[SAS] link account done")

View File

@@ -1,16 +1,17 @@
import { TRPCError } from "@trpc/server"
import { setTimeout } from "timers/promises"
import { z } from "zod"
import { protectedProcedure } from "@/server/trpc"
import { timeout } from "@/utils/timeout"
const outputSchema = z.object({})
export const performLevelUpgrade = protectedProcedure
.output(outputSchema)
.mutation(async function ({ ctx, input }) {
console.log("[SAS] perform upgrade")
await setTimeout(1000)
await timeout(1000)
//TODO: Call actual API here
throw new TRPCError({

View File

@@ -1,9 +1,10 @@
import { TRPCError } from "@trpc/server"
import { setTimeout } from "timers/promises"
import { z } from "zod"
import { protectedProcedure } from "@/server/trpc"
import { timeout } from "@/utils/timeout"
const outputSchema = z.object({
// unlinked: z.boolean(),
})
@@ -12,7 +13,7 @@ export const unlinkAccount = protectedProcedure
.output(outputSchema)
.mutation(async function ({ ctx, input }) {
console.log("[SAS] unlink account")
await setTimeout(1000)
await timeout(1000)
//TODO: Call actual API here
throw new TRPCError({

View File

@@ -1,21 +0,0 @@
import { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query"
import { FriendsMembership } from "@/utils/user"
import type { User } from "@/types/user"
import type { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output"
type MyPagesNavigation = Awaited<
ReturnType<(typeof navigationQueryRouter)["get"]>
>
export interface MyPagesMenuProps {
navigation: MyPagesNavigation
user: Pick<User, "firstName" | "lastName">
membership?: FriendsMembership | null
membershipLevel: LoyaltyLevel | null
}
export interface MyPagesMenuContentProps extends MyPagesMenuProps {
toggleOpenStateFn: () => void
}

3
utils/timeout.ts Normal file
View File

@@ -0,0 +1,3 @@
export function timeout(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}