Merged in feat/SW-1296-hotel-subpages (pull request #1233)

feat(SW-1296): added Subpage for hotel pages and its routing

* feat(SW-1296): added Subpage for hotel pages and its routing


Approved-by: Fredrik Thorsson
This commit is contained in:
Erik Tiekstra
2025-02-03 10:58:53 +00:00
parent b2a3fca54a
commit dd4a2d8120
9 changed files with 252 additions and 24 deletions

View File

@@ -9,6 +9,7 @@ import DestinationOverviewPage from "@/components/ContentType/DestinationOvervie
import DestinationCityPage from "@/components/ContentType/DestinationPage/DestinationCityPage"
import DestinationCountryPage from "@/components/ContentType/DestinationPage/DestinationCountryPage"
import HotelPage from "@/components/ContentType/HotelPage"
import HotelSubpage from "@/components/ContentType/HotelSubpage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
import StartPage from "@/components/ContentType/StartPage"
import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
@@ -27,7 +28,8 @@ export { generateMetadata } from "@/utils/generateMetadata"
export default async function ContentTypePage({
params,
}: PageArgs<LangParams & ContentTypeParams & UIDParams, {}>) {
searchParams,
}: PageArgs<LangParams & ContentTypeParams & UIDParams, { subpage?: string }>) {
setLang(params.lang)
const pathname = headers().get("x-pathname") || ""
@@ -70,11 +72,20 @@ export default async function ContentTypePage({
return notFound()
}
const hotelPageData = await getHotelPage()
return hotelPageData ? (
<HotelPage hotelId={hotelPageData.hotel_page_id} />
) : (
notFound()
)
if (hotelPageData) {
if (searchParams.subpage) {
return (
<HotelSubpage
hotelId={hotelPageData.hotel_page_id}
subpage={searchParams.subpage}
/>
)
}
return <HotelPage hotelId={hotelPageData.hotel_page_id} />
}
notFound()
case PageContentTypeEnum.startPage:
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()

View File

@@ -1,3 +1,5 @@
import { parkingSubPage } from "@/constants/routes/hotelSubpages"
import { OpenInNewIcon } from "@/components/Icons"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Button from "@/components/TempDesignSystem/Button"
@@ -6,6 +8,7 @@ import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import ParkingList from "./ParkingList"
import ParkingPrices from "./ParkingPrices"
@@ -20,6 +23,7 @@ export default async function ParkingAmenity({
parkingElevatorPitch,
hasExtraParkingPage,
}: ParkingAmenityProps) {
const lang = getLang()
const intl = await getIntl()
return (
@@ -85,20 +89,25 @@ export default async function ParkingAmenity({
)}
</div>
))}
{hasExtraParkingPage && (
<Button
className={styles.parkingPageLink}
theme="base"
intent="secondary"
asChild
>
{/* TODO: URL Should possibly be something more dynamic */}
<Link
href={`/${parkingSubPage[lang]}`}
color="burgundy"
weight="bold"
appendToCurrentPath
>
{intl.formatMessage({ id: "About parking" })}
</Link>
</Button>
)}
</div>
{hasExtraParkingPage && (
<Button
className={styles.parkingPageLink}
theme="base"
intent="secondary"
asChild
>
{/* TODO: Add URL to separate parking page */}
<Link href="#" color="burgundy" weight="bold">
{intl.formatMessage({ id: "About parking" })}
</Link>
</Button>
)}
</AccordionItem>
)
}

View File

@@ -0,0 +1,75 @@
.hotelSubpage {
padding-bottom: var(--Spacing-x9);
}
.header {
background-color: var(--Base-Surface-Subtle-Normal);
padding: var(--Spacing-x4) 0;
}
.heroContainer {
width: 100%;
padding: var(--Spacing-x4) var(--Spacing-x2);
}
.heroContainer img {
max-width: var(--max-width-content);
margin: 0 auto;
display: block;
}
.contentContainer {
display: grid;
grid-template-areas:
"main"
"sidebar";
gap: var(--Spacing-x4);
align-items: start;
width: 100%;
padding: var(--Spacing-x4) var(--Spacing-x2) 0;
}
.mainContent {
grid-area: main;
display: grid;
width: 100%;
gap: var(--Spacing-x6);
margin: 0 auto;
max-width: var(--max-width-content);
}
@media (min-width: 768px) {
.contentContainer {
padding: var(--Spacing-x4) 0;
}
.heroContainer {
padding: var(--Spacing-x4) 0;
}
.header {
padding: var(--Spacing-x4) 0;
}
}
@media (min-width: 1367px) {
.heroContainer {
padding: var(--Spacing-x4) 0;
}
.contentContainer {
grid-template-areas: "main sidebar";
grid-template-columns: var(--max-width-text-block) 1fr;
gap: var(--Spacing-x9);
padding: var(--Spacing-x4) 0 0;
max-width: var(--max-width-content);
margin: 0 auto;
}
.mainContent {
gap: var(--Spacing-x9);
padding: 0;
max-width: none;
margin: 0;
}
}

View File

@@ -0,0 +1,61 @@
import { notFound } from "next/navigation"
import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getLang } from "@/i18n/serverContext"
import { getSubpageData } from "./utils"
import styles from "./hotelSubpage.module.css"
interface HotelSubpageProps {
hotelId: string
subpage: string
}
export default async function HotelSubpage({
hotelId,
subpage,
}: HotelSubpageProps) {
const lang = getLang()
const [hotelPageData, hotel] = await Promise.all([
getHotelPage(),
getHotel({ hotelId, language: lang }),
])
if (!hotel?.hotel || !hotelPageData) {
notFound()
}
const pageData = getSubpageData(subpage, lang, hotel.additionalData)
if (!pageData) {
notFound()
}
const hotelData = hotel.hotel
return (
<>
<section className={styles.hotelSubpage}>
<header className={styles.header}>
{/* breadcrumbs */}
<div className={styles.heroContainer}>{/* hero image */}</div>
</header>
<div className={styles.contentContainer}>
<main className={styles.mainContent}>
{/* Main content */}
<Title level="h1">
{subpage} for {hotelData.name}
</Title>
<Body>{pageData.elevatorPitch}</Body>
</main>
{/* Sidebar */}
</div>
</section>
{/* Tracking */}
</>
)
}

View File

@@ -0,0 +1,17 @@
import { parkingSubPage } from "@/constants/routes/hotelSubpages"
import type { HotelData } from "@/types/hotel"
import type { Lang } from "@/constants/languages"
export function getSubpageData(
subpage: string,
lang: Lang,
additionalData: HotelData["additionalData"]
) {
switch (subpage) {
case parkingSubPage[lang]:
return additionalData.hotelParking
default:
return null
}
}

View File

@@ -29,6 +29,7 @@ export default function Link({
* Decides if the link should include the current search params in the URL
*/
keepSearchParams,
appendToCurrentPath,
...props
}: LinkProps) {
const currentPageSlug = usePathname()
@@ -50,11 +51,24 @@ export default function Link({
})
const fullUrl = useMemo(() => {
if (!keepSearchParams || !searchParams.size) return href
let newPath = href
if (appendToCurrentPath) {
newPath = `${currentPageSlug}${newPath}`
}
const delimiter = href.includes("?") ? "&" : "?"
return `${href}${delimiter}${searchParams}`
}, [href, searchParams, keepSearchParams])
if (keepSearchParams && searchParams.size) {
const delimiter = newPath.includes("?") ? "&" : "?"
return `${newPath}${delimiter}${searchParams}`
}
return newPath
}, [
href,
searchParams,
keepSearchParams,
appendToCurrentPath,
currentPageSlug,
])
// TODO: Remove this check (and hook) and only return <Link /> when current web is deleted
const isExternal = useCheckIfExternalLink(href)

View File

@@ -12,4 +12,5 @@ export interface LinkProps
trackingId?: string
trackingParams?: Record<string, string>
keepSearchParams?: boolean
appendToCurrentPath?: boolean
}

View File

@@ -0,0 +1,8 @@
export const parkingSubPage = {
en: "parking",
sv: "parkering",
no: "parkering",
da: "parkering",
fi: "parkkipaikka",
de: "Parkplatz",
}

View File

@@ -8,6 +8,7 @@ import { removeTrailingSlash } from "@/utils/url"
import { fetchAndCacheEntry, getDefaultRequestHeaders } from "./utils"
import type { MiddlewareMatcher } from "@/types/middleware"
import { PageContentTypeEnum } from "@/types/requests/contentType"
export const middleware: NextMiddleware = async (request) => {
const { nextUrl } = request
@@ -19,13 +20,44 @@ export const middleware: NextMiddleware = async (request) => {
const isPreview = request.nextUrl.pathname.includes("/preview")
const searchParams = new URLSearchParams(request.nextUrl.searchParams)
const { contentType, uid } = await fetchAndCacheEntry(
let { contentType, uid } = await fetchAndCacheEntry(
isPreview
? contentTypePathName.replace("/preview", "")
: contentTypePathName,
lang
)
if (!contentType || !uid) {
// Routes to subpages we need to check if the parent of the incomingPathName exists.
// Then we considered the incomingPathName to be a subpage. These subpages do not live in the CMS.
const incomingPathName = isPreview
? contentTypePathName.replace("/preview", "")
: contentTypePathName
const incomingPathNameParts = incomingPathName.split("/")
// If the incomingPathName has 2 or more parts, it could possibly be a subpage.
if (incomingPathNameParts.length >= 2) {
const subpage = incomingPathNameParts.pop()
if (subpage) {
const parentPageResult = await fetchAndCacheEntry(
incomingPathNameParts.join("/"),
lang
)
if (parentPageResult.uid) {
switch (parentPageResult.contentType) {
case PageContentTypeEnum.hotelPage:
// E.g. Dedicated pages for restaurant, parking etc.
contentType = parentPageResult.contentType
uid = parentPageResult.uid
searchParams.set("subpage", subpage)
break
}
}
}
}
}
if (!contentType || !uid) {
throw notFound(
`Unable to resolve CMS entry for locale "${lang}": ${contentTypePathName}`