Merged in feat/refactor-header-footer-sitewidealert (pull request #1374)

Refactor: removed parallel routes for header, footer and sidewidealert. Langswitcher and sidewidealert now client components

* feat - removed parallel routes and made sidepeek and sitewidealerts as client components

* Langswitcher as client component

* Fixed lang switcher for current header

* Passing lang when fetching siteconfig

* Merge branch 'master' into feat/refactor-header-footer-sitewidealert

* Refactor

* Removed dead code

* Show only languages that has translation

* Refetch sitewidealert every 60 seconds

* Merge branch 'master' into feat/refactor-header-footer-sitewidealert

* Removed sidepeek parallel route from my-stay

* Added missing env.var to env.test

* Removed console.log


Approved-by: Joakim Jäderberg
This commit is contained in:
Linus Flood
2025-02-19 08:59:24 +00:00
parent c2b7d97ddd
commit 7c0f9084b6
45 changed files with 664 additions and 697 deletions
+4 -1
View File
@@ -1,5 +1,7 @@
import { getLocations, getSiteConfig } from "@/lib/trpc/memoizedRequests"
import { getLang } from "@/i18n/serverContext"
import BookingWidgetClient from "./Client"
import type { BookingWidgetProps } from "@/types/components/bookingWidget"
@@ -12,8 +14,9 @@ export default async function BookingWidget({
type,
bookingWidgetSearchParams,
}: BookingWidgetProps) {
const lang = await getLang()
const locations = await getLocations()
const siteConfig = await getSiteConfig()
const siteConfig = await getSiteConfig(lang)
if (!locations || "error" in locations || siteConfig?.bookingWidgetDisabled) {
return null
@@ -1,15 +1,37 @@
"use client"
import { usePathname } from "next/navigation"
import { trpc } from "@/lib/trpc/client"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import useLang from "@/hooks/useLang"
import Desktop from "./Desktop"
import Mobile from "./Mobile"
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
export default function LanguageSwitcher() {
const currentLanguage = useLang()
const pathName = usePathname()
type LanguageSwitcherProps = { urls: LanguageSwitcherData }
const { data: languagesResponse, isLoading } =
trpc.contentstack.languageSwitcher.get.useQuery({
pathName,
lang: currentLanguage,
})
if (isLoading) {
return <SkeletonShimmer width="12ch" />
}
if (!languagesResponse?.urls) {
return null
}
export default function LanguageSwitcher({ urls }: LanguageSwitcherProps) {
return (
<>
<Desktop urls={urls} />
<Mobile urls={urls} />
<Desktop urls={languagesResponse.urls} />
<Mobile urls={languagesResponse.urls} />
</>
)
}
@@ -74,8 +74,6 @@ export function MainMenu({
toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)
}
console.log("isMyPagesMobileMenuOpen", isMyPagesMobileMenuOpen)
return (
<div className={styles.mainMenu}>
<div
+5 -13
View File
@@ -1,10 +1,6 @@
import { homeHrefs } from "@/constants/homeHrefs"
import { env } from "@/env/server"
import {
getCurrentHeader,
getLanguageSwitcher,
getName,
} from "@/lib/trpc/memoizedRequests"
import { getCurrentHeader, getName } from "@/lib/trpc/memoizedRequests"
import { getLang } from "@/i18n/serverContext"
@@ -18,13 +14,9 @@ import styles from "./header.module.css"
export default async function Header() {
const lang = getLang()
const [data, user, languages] = await Promise.all([
getCurrentHeader(lang),
getName(),
getLanguageSwitcher(),
])
const [data, user] = await Promise.all([getCurrentHeader(lang), getName()])
if (!languages || !data?.header) {
if (!data?.header) {
return null
}
@@ -42,7 +34,7 @@ export default async function Header() {
frontpageLinkText={frontpageLinkText}
homeHref={homeHref}
links={topMenu.links}
languageSwitcher={<LanguageSwitcher urls={languages.urls} />}
languageSwitcher={<LanguageSwitcher />}
/>
<MainMenu
frontpageLinkText={frontpageLinkText}
@@ -50,7 +42,7 @@ export default async function Header() {
links={menu.links}
logo={logo}
topMenuMobileLinks={topMenuMobileLinks}
languageSwitcher={<LanguageSwitcher urls={languages.urls} />}
languageSwitcher={<LanguageSwitcher />}
myPagesMobileDropdown={<MyPagesMobileDropdown />}
bookingHref={homeHref}
user={user}
+2 -5
View File
@@ -1,4 +1,4 @@
import { getFooter, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests"
import { getFooter } from "@/lib/trpc/memoizedRequests"
import Image from "@/components/Image"
import LanguageSwitcher from "@/components/LanguageSwitcher"
@@ -17,7 +17,6 @@ export default async function FooterDetails() {
const intl = await getIntl()
// preloaded
const footer = await getFooter()
const languages = await getLanguageSwitcher()
const currentYear = new Date().getFullYear()
return (
@@ -69,9 +68,7 @@ export default async function FooterDetails() {
)
)}
</nav>
{languages?.urls ? (
<LanguageSwitcher type="footer" urls={languages.urls} />
) : null}
<LanguageSwitcher type="footer" />
</div>
</div>
</section>
+10 -2
View File
@@ -1,14 +1,22 @@
import { getFooter, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests"
import { env } from "@/env/server"
import { getFooter } from "@/lib/trpc/memoizedRequests"
import CurrentFooter from "@/components/Current/Footer"
import FooterDetails from "./Details"
import FooterNavigation from "./Navigation"
export function preload() {
void getFooter()
void getLanguageSwitcher()
}
export default function Footer() {
if (env.HIDE_FOR_NEXT_RELEASE) {
return <CurrentFooter />
}
preload()
return (
<footer>
<FooterNavigation />
@@ -21,7 +21,6 @@ import { IconName } from "@/types/components/icon"
export default function MobileMenu({
children,
languageUrls,
topLink,
isLoggedIn,
}: React.PropsWithChildren<MobileMenuProps>) {
@@ -91,7 +90,7 @@ export default function MobileMenu({
<HeaderLink href="#" iconName={IconName.Service}>
{intl.formatMessage({ id: "Customer service" })}
</HeaderLink>
<LanguageSwitcher type="mobileHeader" urls={languageUrls} />
<LanguageSwitcher type="mobileHeader" />
</footer>
</Dialog>
</Modal>
@@ -1,4 +1,4 @@
import { getHeader, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests"
import { getHeader } from "@/lib/trpc/memoizedRequests"
import { auth } from "@/auth"
import { isValidSession } from "@/utils/session"
@@ -9,17 +9,15 @@ export default async function MobileMenuWrapper({
children,
}: React.PropsWithChildren) {
// preloaded
const languages = await getLanguageSwitcher()
const header = await getHeader()
const session = await auth()
if (!languages || !header) {
if (!header) {
return null
}
return (
<MobileMenu
languageUrls={languages.urls}
topLink={header.data.topLink}
isLoggedIn={isValidSession(session)}
>
+3 -4
View File
@@ -1,4 +1,4 @@
import { getHeader, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests"
import { getHeader } from "@/lib/trpc/memoizedRequests"
import { auth } from "@/auth"
import LanguageSwitcher from "@/components/LanguageSwitcher"
@@ -18,12 +18,11 @@ export default async function TopMenu() {
// cached
const intl = await getIntl()
// both preloaded
const languages = await getLanguageSwitcher()
const header = await getHeader()
const session = await auth()
const isLoggedIn = isValidSession(session)
if (!languages || !header) {
if (!header) {
return null
}
@@ -32,7 +31,7 @@ export default async function TopMenu() {
<div className={styles.content}>
<TopLink isLoggedIn={isLoggedIn} topLink={header.data.topLink} />
<div className={styles.options}>
<LanguageSwitcher type="desktopHeader" urls={languages.urls} />
<LanguageSwitcher type="desktopHeader" />
<Caption type="regular" color="textMediumContrast" asChild>
<HeaderLink href="#" iconName={IconName.Search}>
+16 -7
View File
@@ -1,20 +1,29 @@
import { Suspense } from "react"
import {
getHeader,
getLanguageSwitcher,
getName,
} from "@/lib/trpc/memoizedRequests"
import { env } from "@/env/server"
import { getHeader, getName } from "@/lib/trpc/memoizedRequests"
import CurrentHeader from "@/components/Current/Header"
import HeaderFallback from "../Current/Header/HeaderFallback"
import MainMenu from "./MainMenu"
import TopMenu, { TopMenuSkeleton } from "./TopMenu"
import styles from "./header.module.css"
export default function Header() {
void getHeader()
void getLanguageSwitcher()
void getName()
if (env.HIDE_FOR_NEXT_RELEASE) {
return (
<Suspense fallback={<HeaderFallback />}>
<CurrentHeader />
</Suspense>
)
}
void getHeader()
return (
<header className={styles.header}>
<Suspense fallback={<TopMenuSkeleton />}>
@@ -0,0 +1,126 @@
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import stringify from "json-stable-stringify-without-jsonify"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import { getValidDates } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates"
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
import HotelInfoCard, {
HotelInfoCardSkeleton,
} from "@/components/HotelReservation/SelectRate/HotelInfoCard"
import {
preload,
RoomsContainer,
} from "@/components/HotelReservation/SelectRate/RoomsContainer"
import { RoomsContainerSkeleton } from "@/components/HotelReservation/SelectRate/RoomsContainer/RoomsContainerSkeleton"
import TrackingSDK from "@/components/TrackingSDK"
import { setLang } from "@/i18n/serverContext"
import { convertSearchParamsToObj } from "@/utils/url"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import {
TrackingChannelEnum,
type TrackingSDKHotelInfo,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type { LangParams, PageArgs } from "@/types/params"
export default async function SelectRatePage({
params,
searchParams,
}: PageArgs<LangParams & { section: string }, SelectRateSearchParams>) {
setLang(params.lang)
const searchDetails = await getHotelSearchDetails({ searchParams })
if (!searchDetails?.hotel) {
return notFound()
}
const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } =
searchDetails
const { fromDate, toDate } = getValidDates(
selectHotelParams.fromDate,
selectHotelParams.toDate
)
preload(
hotel.id,
params.lang,
fromDate.format("YYYY-MM-DD"),
toDate.format("YYYY-MM-DD"),
adultsInRoom,
childrenInRoom
)
const hotelData = await getHotel({
hotelId: hotel.id,
isCardOnlyPayment: false,
language: params.lang,
})
const arrivalDate = fromDate.toDate()
const departureDate = toDate.toDate()
const pageTrackingData: TrackingSDKPageData = {
pageId: "select-rate",
domainLanguage: params.lang,
channel: TrackingChannelEnum["hotelreservation"],
pageName: "hotelreservation|select-rate",
siteSections: "hotelreservation|select-rate",
pageType: "bookingroomsandratespage",
siteVersion: "new-web",
}
const hotelsTrackingData: TrackingSDKHotelInfo = {
searchTerm: selectHotelParams.city ?? hotel?.name,
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
departureDate: format(departureDate, "yyyy-MM-dd"),
noOfAdults: adultsInRoom[0], // TODO: Handle multiple rooms
noOfChildren: childrenInRoom?.length,
ageOfChildren: childrenInRoom?.map((c) => c.age).join(","),
childBedPreference: childrenInRoom
?.map((c) => ChildBedMapEnum[c.bed])
.join("|"),
noOfRooms: 1, // // TODO: Handle multiple rooms
duration: differenceInCalendarDays(departureDate, arrivalDate),
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
searchType: "hotel",
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
country: hotelData?.hotel.address.country,
hotelID: hotel?.id,
region: hotelData?.hotel.address.city,
}
const hotelId = +hotel.id
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
const suspenseKey = stringify(searchParams)
return (
<>
<Suspense fallback={<HotelInfoCardSkeleton />}>
<HotelInfoCard hotelData={hotelData} />
</Suspense>
<Suspense key={suspenseKey} fallback={<RoomsContainerSkeleton />}>
<RoomsContainer
adultArray={adultsInRoom}
booking={booking}
childArray={childrenInRoom}
fromDate={arrivalDate}
hotelId={hotelId}
lang={params.lang}
toDate={departureDate}
/>
</Suspense>
<Suspense key={suspenseKey} fallback={null}>
<TrackingSDK
pageData={pageTrackingData}
hotelInfo={hotelsTrackingData}
/>
</Suspense>
</>
)
}
@@ -7,11 +7,7 @@ import HotelSidePeek from "@/components/SidePeeks/HotelSidePeek"
import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek"
import useLang from "@/hooks/useLang"
import type { HotelReservationSidePeekProps } from "@/types/components/hotelReservation/sidePeek"
export default function HotelReservationSidePeek({
hotel,
}: HotelReservationSidePeekProps) {
export default function HotelReservationSidePeek() {
const activeSidePeek = useSidePeekStore((state) => state.activeSidePeek)
const hotelId = useSidePeekStore((state) => state.hotelId)
const roomTypeCode = useSidePeekStore((state) => state.roomTypeCode)
@@ -27,7 +23,6 @@ export default function HotelReservationSidePeek({
},
{
enabled: !!hotelId,
initialData: hotel ?? undefined,
}
)
+21 -5
View File
@@ -1,9 +1,11 @@
"use client"
import { usePathname } from "next/navigation"
import { useRef } from "react"
import { useIntl } from "react-intl"
import { languages } from "@/constants/languages"
import { trpc } from "@/lib/trpc/client"
import useDropdownStore from "@/stores/main-menu"
import { ChevronDownSmallIcon, GlobeIcon } from "@/components/Icons"
@@ -11,6 +13,7 @@ import useClickOutside from "@/hooks/useClickOutside"
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
import useLang from "@/hooks/useLang"
import SkeletonShimmer from "../SkeletonShimmer"
import Caption from "../TempDesignSystem/Text/Caption"
import LanguageSwitcherContainer from "./LanguageSwitcherContainer"
import LanguageSwitcherContent from "./LanguageSwitcherContent"
@@ -24,10 +27,7 @@ import {
LanguageSwitcherTypesEnum,
} from "@/types/components/languageSwitcher/languageSwitcher"
export default function LanguageSwitcher({
urls,
type,
}: LanguageSwitcherProps) {
export default function LanguageSwitcher({ type }: LanguageSwitcherProps) {
const intl = useIntl()
const currentLanguage = useLang()
const {
@@ -37,6 +37,14 @@ export default function LanguageSwitcher({
isHeaderLanguageSwitcherOpen,
} = useDropdownStore()
const pathName = usePathname()
const { data: languagesResponse, isLoading } =
trpc.contentstack.languageSwitcher.get.useQuery({
pathName,
lang: currentLanguage,
})
const languageSwitcherRef = useRef<HTMLDivElement>(null)
const isFooter = type === LanguageSwitcherTypesEnum.Footer
const isHeader = !isFooter
@@ -80,6 +88,14 @@ export default function LanguageSwitcher({
const closeMsg = intl.formatMessage({ id: "Close language menu" })
const openMsg = intl.formatMessage({ id: "Open language menu" })
if (isLoading) {
return <SkeletonShimmer width="12ch" />
}
if (!languagesResponse?.urls) {
return null
}
return (
<div className={classNames} ref={languageSwitcherRef}>
<button
@@ -105,7 +121,7 @@ export default function LanguageSwitcher({
{isLanguageSwitcherOpen ? (
<LanguageSwitcherContainer type={type}>
<LanguageSwitcherContent
urls={urls}
urls={languagesResponse.urls}
onLanguageSwitch={() => toggleDropdown(dropdownType)}
/>
</LanguageSwitcherContainer>
-70
View File
@@ -1,70 +0,0 @@
"use client"
import { useCallback, useEffect, useRef } from "react"
import { StickyElementNameEnum } from "@/stores/sticky-position"
import Alert from "@/components/TempDesignSystem/Alert"
import useStickyPosition from "@/hooks/useStickyPosition"
import { debounce } from "@/utils/debounce"
import styles from "./sitewideAlert.module.css"
import { AlertTypeEnum } from "@/types/enums/alert"
import type { SitewideAlertProps } from "./sitewideAlert"
export default function SiteWideAlertClient({ alert }: SitewideAlertProps) {
const alertRef = useRef<HTMLDivElement>(null)
const isAlarm = alert.type === AlertTypeEnum.Alarm
useStickyPosition({
ref: isAlarm ? alertRef : undefined,
name: StickyElementNameEnum.SITEWIDE_ALERT,
})
const updateHeight = useCallback(() => {
if (alertRef.current) {
const height = alertRef.current.offsetHeight
document.documentElement.style.setProperty(
"--sitewide-alert-height",
`${height}px`
)
}
}, [])
useEffect(() => {
const alertElement = alertRef.current
const debouncedResizeHandler = debounce(() => {
updateHeight()
}, 100)
const observer = new ResizeObserver(debouncedResizeHandler)
if (alertElement) {
observer.observe(alertElement)
}
return () => {
if (alertElement) {
observer.unobserve(alertElement)
}
observer.disconnect()
}
}, [updateHeight])
return (
<div
ref={alertRef}
className={`${styles.sitewideAlert} ${isAlarm ? styles.alarm : ""}`}
>
<Alert
variant="banner"
type={alert.type}
link={alert.link}
phoneContact={alert.phoneContact}
sidepeekCtaText={alert.sidepeekButton?.cta_text}
sidepeekContent={alert.sidepeekContent}
heading={alert.heading}
text={alert.text}
/>
</div>
)
}
+76 -9
View File
@@ -1,17 +1,84 @@
import { getSiteConfig } from "@/lib/trpc/memoizedRequests"
"use client"
import SiteWideAlertClient from "./Client"
import { useCallback, useEffect, useRef } from "react"
export function preload() {
void getSiteConfig()
}
import { trpc } from "@/lib/trpc/client"
import { StickyElementNameEnum } from "@/stores/sticky-position"
export default async function SitewideAlert() {
const siteConfig = await getSiteConfig()
import Alert from "@/components/TempDesignSystem/Alert"
import useLang from "@/hooks/useLang"
import useStickyPosition from "@/hooks/useStickyPosition"
import { debounce } from "@/utils/debounce"
if (!siteConfig?.sitewideAlert) {
import styles from "./sitewideAlert.module.css"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function SiteWideAlert() {
const alertRef = useRef<HTMLDivElement>(null)
const lang = useLang()
const { data: siteConfig, isLoading } =
trpc.contentstack.base.siteConfig.useQuery(
{ lang },
{ refetchInterval: 60_000 }
)
const alert = siteConfig?.sitewideAlert
const isAlarm = alert?.type === AlertTypeEnum.Alarm
useStickyPosition({
ref: isAlarm ? alertRef : undefined,
name: StickyElementNameEnum.SITEWIDE_ALERT,
})
const updateHeight = useCallback(() => {
if (alertRef.current) {
const height = alertRef.current.offsetHeight
document.documentElement.style.setProperty(
"--sitewide-alert-height",
`${height}px`
)
}
}, [])
useEffect(() => {
const alertElement = alertRef.current
const debouncedResizeHandler = debounce(() => {
updateHeight()
}, 100)
const observer = new ResizeObserver(debouncedResizeHandler)
if (alertElement) {
observer.observe(alertElement)
}
return () => {
if (alertElement) {
observer.unobserve(alertElement)
}
observer.disconnect()
}
}, [updateHeight])
if (isLoading || !alert) {
return null
}
return <SiteWideAlertClient alert={siteConfig.sitewideAlert} />
return (
<div
ref={alertRef}
className={`${styles.sitewideAlert} ${isAlarm ? styles.alarm : ""}`}
>
<Alert
variant="banner"
type={alert.type}
link={alert.link}
phoneContact={alert.phoneContact}
sidepeekCtaText={alert.sidepeekButton?.cta_text}
sidepeekContent={alert.sidepeekContent}
heading={alert.heading}
text={alert.text}
/>
</div>
)
}
@@ -1,5 +0,0 @@
import type { Alert } from "@/types/trpc/routers/contentstack/siteConfig"
export interface SitewideAlertProps {
alert: Alert
}