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

@@ -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
}