From dd4a2d81202849d7eb034127d792ad8ec4c5cabf Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 3 Feb 2025 10:58:53 +0000 Subject: [PATCH] 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 --- .../(public)/[contentType]/[uid]/page.tsx | 23 ++++-- .../AccordionAmenities/Parking/index.tsx | 35 +++++---- .../HotelSubpage/hotelSubpage.module.css | 75 +++++++++++++++++++ components/ContentType/HotelSubpage/index.tsx | 61 +++++++++++++++ components/ContentType/HotelSubpage/utils.ts | 17 +++++ components/TempDesignSystem/Link/index.tsx | 22 +++++- components/TempDesignSystem/Link/link.ts | 1 + constants/routes/hotelSubpages.ts | 8 ++ middlewares/cmsContent.ts | 34 ++++++++- 9 files changed, 252 insertions(+), 24 deletions(-) create mode 100644 components/ContentType/HotelSubpage/hotelSubpage.module.css create mode 100644 components/ContentType/HotelSubpage/index.tsx create mode 100644 components/ContentType/HotelSubpage/utils.ts create mode 100644 constants/routes/hotelSubpages.ts diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index 2497d9e16..5e98131ad 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -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) { + searchParams, +}: PageArgs) { 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 ? ( - - ) : ( - notFound() - ) + + if (hotelPageData) { + if (searchParams.subpage) { + return ( + + ) + } + return + } + + notFound() case PageContentTypeEnum.startPage: if (env.HIDE_FOR_NEXT_RELEASE) { return notFound() diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx index eb013c89a..0738b7645 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx @@ -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({ )} ))} + {hasExtraParkingPage && ( + + )} - {hasExtraParkingPage && ( - - )} ) } diff --git a/components/ContentType/HotelSubpage/hotelSubpage.module.css b/components/ContentType/HotelSubpage/hotelSubpage.module.css new file mode 100644 index 000000000..57997b6b3 --- /dev/null +++ b/components/ContentType/HotelSubpage/hotelSubpage.module.css @@ -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; + } +} diff --git a/components/ContentType/HotelSubpage/index.tsx b/components/ContentType/HotelSubpage/index.tsx new file mode 100644 index 000000000..98f7c72d4 --- /dev/null +++ b/components/ContentType/HotelSubpage/index.tsx @@ -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 ( + <> +
+
+ {/* breadcrumbs */} +
{/* hero image */}
+
+ +
+
+ {/* Main content */} + + {subpage} for {hotelData.name} + + {pageData.elevatorPitch} +
+ + {/* Sidebar */} +
+
+ {/* Tracking */} + + ) +} diff --git a/components/ContentType/HotelSubpage/utils.ts b/components/ContentType/HotelSubpage/utils.ts new file mode 100644 index 000000000..78cd72334 --- /dev/null +++ b/components/ContentType/HotelSubpage/utils.ts @@ -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 + } +} diff --git a/components/TempDesignSystem/Link/index.tsx b/components/TempDesignSystem/Link/index.tsx index d198650b7..c641eff90 100644 --- a/components/TempDesignSystem/Link/index.tsx +++ b/components/TempDesignSystem/Link/index.tsx @@ -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 when current web is deleted const isExternal = useCheckIfExternalLink(href) diff --git a/components/TempDesignSystem/Link/link.ts b/components/TempDesignSystem/Link/link.ts index 873aaedba..3e6b6c0b2 100644 --- a/components/TempDesignSystem/Link/link.ts +++ b/components/TempDesignSystem/Link/link.ts @@ -12,4 +12,5 @@ export interface LinkProps trackingId?: string trackingParams?: Record keepSearchParams?: boolean + appendToCurrentPath?: boolean } diff --git a/constants/routes/hotelSubpages.ts b/constants/routes/hotelSubpages.ts new file mode 100644 index 000000000..3e449e88b --- /dev/null +++ b/constants/routes/hotelSubpages.ts @@ -0,0 +1,8 @@ +export const parkingSubPage = { + en: "parking", + sv: "parkering", + no: "parkering", + da: "parkering", + fi: "parkkipaikka", + de: "Parkplatz", +} diff --git a/middlewares/cmsContent.ts b/middlewares/cmsContent.ts index 7a39a363d..8de8488a5 100644 --- a/middlewares/cmsContent.ts +++ b/middlewares/cmsContent.ts @@ -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}`