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:
@@ -9,6 +9,7 @@ import DestinationOverviewPage from "@/components/ContentType/DestinationOvervie
|
|||||||
import DestinationCityPage from "@/components/ContentType/DestinationPage/DestinationCityPage"
|
import DestinationCityPage from "@/components/ContentType/DestinationPage/DestinationCityPage"
|
||||||
import DestinationCountryPage from "@/components/ContentType/DestinationPage/DestinationCountryPage"
|
import DestinationCountryPage from "@/components/ContentType/DestinationPage/DestinationCountryPage"
|
||||||
import HotelPage from "@/components/ContentType/HotelPage"
|
import HotelPage from "@/components/ContentType/HotelPage"
|
||||||
|
import HotelSubpage from "@/components/ContentType/HotelSubpage"
|
||||||
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
|
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
|
||||||
import StartPage from "@/components/ContentType/StartPage"
|
import StartPage from "@/components/ContentType/StartPage"
|
||||||
import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
|
import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
|
||||||
@@ -27,7 +28,8 @@ export { generateMetadata } from "@/utils/generateMetadata"
|
|||||||
|
|
||||||
export default async function ContentTypePage({
|
export default async function ContentTypePage({
|
||||||
params,
|
params,
|
||||||
}: PageArgs<LangParams & ContentTypeParams & UIDParams, {}>) {
|
searchParams,
|
||||||
|
}: PageArgs<LangParams & ContentTypeParams & UIDParams, { subpage?: string }>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|
||||||
const pathname = headers().get("x-pathname") || ""
|
const pathname = headers().get("x-pathname") || ""
|
||||||
@@ -70,11 +72,20 @@ export default async function ContentTypePage({
|
|||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
const hotelPageData = await getHotelPage()
|
const hotelPageData = await getHotelPage()
|
||||||
return hotelPageData ? (
|
|
||||||
<HotelPage hotelId={hotelPageData.hotel_page_id} />
|
if (hotelPageData) {
|
||||||
) : (
|
if (searchParams.subpage) {
|
||||||
notFound()
|
return (
|
||||||
)
|
<HotelSubpage
|
||||||
|
hotelId={hotelPageData.hotel_page_id}
|
||||||
|
subpage={searchParams.subpage}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <HotelPage hotelId={hotelPageData.hotel_page_id} />
|
||||||
|
}
|
||||||
|
|
||||||
|
notFound()
|
||||||
case PageContentTypeEnum.startPage:
|
case PageContentTypeEnum.startPage:
|
||||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||||
return notFound()
|
return notFound()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { parkingSubPage } from "@/constants/routes/hotelSubpages"
|
||||||
|
|
||||||
import { OpenInNewIcon } from "@/components/Icons"
|
import { OpenInNewIcon } from "@/components/Icons"
|
||||||
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
|
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
@@ -6,6 +8,7 @@ import Link from "@/components/TempDesignSystem/Link"
|
|||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import ParkingList from "./ParkingList"
|
import ParkingList from "./ParkingList"
|
||||||
import ParkingPrices from "./ParkingPrices"
|
import ParkingPrices from "./ParkingPrices"
|
||||||
@@ -20,6 +23,7 @@ export default async function ParkingAmenity({
|
|||||||
parkingElevatorPitch,
|
parkingElevatorPitch,
|
||||||
hasExtraParkingPage,
|
hasExtraParkingPage,
|
||||||
}: ParkingAmenityProps) {
|
}: ParkingAmenityProps) {
|
||||||
|
const lang = getLang()
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -85,20 +89,25 @@ export default async function ParkingAmenity({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</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>
|
</AccordionItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
75
components/ContentType/HotelSubpage/hotelSubpage.module.css
Normal file
75
components/ContentType/HotelSubpage/hotelSubpage.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
components/ContentType/HotelSubpage/index.tsx
Normal file
61
components/ContentType/HotelSubpage/index.tsx
Normal 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 */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
components/ContentType/HotelSubpage/utils.ts
Normal file
17
components/ContentType/HotelSubpage/utils.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ export default function Link({
|
|||||||
* Decides if the link should include the current search params in the URL
|
* Decides if the link should include the current search params in the URL
|
||||||
*/
|
*/
|
||||||
keepSearchParams,
|
keepSearchParams,
|
||||||
|
appendToCurrentPath,
|
||||||
...props
|
...props
|
||||||
}: LinkProps) {
|
}: LinkProps) {
|
||||||
const currentPageSlug = usePathname()
|
const currentPageSlug = usePathname()
|
||||||
@@ -50,11 +51,24 @@ export default function Link({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const fullUrl = useMemo(() => {
|
const fullUrl = useMemo(() => {
|
||||||
if (!keepSearchParams || !searchParams.size) return href
|
let newPath = href
|
||||||
|
if (appendToCurrentPath) {
|
||||||
|
newPath = `${currentPageSlug}${newPath}`
|
||||||
|
}
|
||||||
|
|
||||||
const delimiter = href.includes("?") ? "&" : "?"
|
if (keepSearchParams && searchParams.size) {
|
||||||
return `${href}${delimiter}${searchParams}`
|
const delimiter = newPath.includes("?") ? "&" : "?"
|
||||||
}, [href, searchParams, keepSearchParams])
|
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
|
// TODO: Remove this check (and hook) and only return <Link /> when current web is deleted
|
||||||
const isExternal = useCheckIfExternalLink(href)
|
const isExternal = useCheckIfExternalLink(href)
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ export interface LinkProps
|
|||||||
trackingId?: string
|
trackingId?: string
|
||||||
trackingParams?: Record<string, string>
|
trackingParams?: Record<string, string>
|
||||||
keepSearchParams?: boolean
|
keepSearchParams?: boolean
|
||||||
|
appendToCurrentPath?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
8
constants/routes/hotelSubpages.ts
Normal file
8
constants/routes/hotelSubpages.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const parkingSubPage = {
|
||||||
|
en: "parking",
|
||||||
|
sv: "parkering",
|
||||||
|
no: "parkering",
|
||||||
|
da: "parkering",
|
||||||
|
fi: "parkkipaikka",
|
||||||
|
de: "Parkplatz",
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { removeTrailingSlash } from "@/utils/url"
|
|||||||
import { fetchAndCacheEntry, getDefaultRequestHeaders } from "./utils"
|
import { fetchAndCacheEntry, getDefaultRequestHeaders } from "./utils"
|
||||||
|
|
||||||
import type { MiddlewareMatcher } from "@/types/middleware"
|
import type { MiddlewareMatcher } from "@/types/middleware"
|
||||||
|
import { PageContentTypeEnum } from "@/types/requests/contentType"
|
||||||
|
|
||||||
export const middleware: NextMiddleware = async (request) => {
|
export const middleware: NextMiddleware = async (request) => {
|
||||||
const { nextUrl } = request
|
const { nextUrl } = request
|
||||||
@@ -19,13 +20,44 @@ export const middleware: NextMiddleware = async (request) => {
|
|||||||
const isPreview = request.nextUrl.pathname.includes("/preview")
|
const isPreview = request.nextUrl.pathname.includes("/preview")
|
||||||
const searchParams = new URLSearchParams(request.nextUrl.searchParams)
|
const searchParams = new URLSearchParams(request.nextUrl.searchParams)
|
||||||
|
|
||||||
const { contentType, uid } = await fetchAndCacheEntry(
|
let { contentType, uid } = await fetchAndCacheEntry(
|
||||||
isPreview
|
isPreview
|
||||||
? contentTypePathName.replace("/preview", "")
|
? contentTypePathName.replace("/preview", "")
|
||||||
: contentTypePathName,
|
: contentTypePathName,
|
||||||
lang
|
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) {
|
if (!contentType || !uid) {
|
||||||
throw notFound(
|
throw notFound(
|
||||||
`Unable to resolve CMS entry for locale "${lang}": ${contentTypePathName}`
|
`Unable to resolve CMS entry for locale "${lang}": ${contentTypePathName}`
|
||||||
|
|||||||
Reference in New Issue
Block a user