Merged in feat/SW-1066-wellness-subpage (pull request #1239)

Feat/SW-1066 wellness subpage

* feat(SW-1066): added wellness subpage


Approved-by: Fredrik Thorsson
Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-02-03 13:35:48 +00:00
parent 09011a2484
commit c25963a3b7
15 changed files with 237 additions and 61 deletions

View File

@@ -1,4 +1,5 @@
import { wellnessAndExercise } from "@/constants/routes/hotelPageParams" import { wellnessAndExercise } from "@/constants/routes/hotelPageParams"
import { wellnessSubPage } from "@/constants/routes/hotelSubpages"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link" import Link from "@/components/TempDesignSystem/Link"
@@ -14,7 +15,7 @@ import type { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelP
export default async function WellnessAndExerciseSidePeek({ export default async function WellnessAndExerciseSidePeek({
healthFacilities, healthFacilities,
wellnessExerciseButton, wellnessExerciseButton = false,
spaPage, spaPage,
}: WellnessAndExerciseSidePeekProps) { }: WellnessAndExerciseSidePeekProps) {
const intl = await getIntl() const intl = await getIntl()
@@ -42,9 +43,10 @@ export default async function WellnessAndExerciseSidePeek({
{wellnessExerciseButton && ( {wellnessExerciseButton && (
<Button fullWidth theme="base" intent="secondary" asChild> <Button fullWidth theme="base" intent="secondary" asChild>
<Link <Link
href={wellnessExerciseButton} href={`/${wellnessSubPage[lang]}`}
weight="bold" weight="bold"
color="burgundy" color="burgundy"
appendToCurrentPath
> >
{intl.formatMessage({ id: "Show wellness & exercise" })} {intl.formatMessage({ id: "Show wellness & exercise" })}
</Link> </Link>

View File

@@ -231,6 +231,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
<WellnessAndExerciseSidePeek <WellnessAndExerciseSidePeek
healthFacilities={healthFacilities} healthFacilities={healthFacilities}
spaPage={spaPage?.spa_page} spaPage={spaPage?.spa_page}
wellnessExerciseButton={displayWebPage.healthGym}
/> />
<RestaurantBarSidePeek restaurants={restaurants} /> <RestaurantBarSidePeek restaurants={restaurants} />
{activitiesCards.map((card) => ( {activitiesCards.map((card) => (

View File

@@ -0,0 +1,24 @@
import { wellnessSubPage } from "@/constants/routes/hotelSubpages"
import { getLang } from "@/i18n/serverContext"
import type { Hotel } from "@/types/hotel"
interface HotelSubpageAdditionalContentProps {
subpage: string
hotel: Hotel
}
export default function HotelSubpageAdditionalContent({
subpage,
hotel,
}: HotelSubpageAdditionalContentProps) {
const lang = getLang()
switch (subpage) {
case wellnessSubPage[lang]:
return null
default:
return null
}
}

View File

@@ -0,0 +1,2 @@
.htmlContent {
}

View File

@@ -0,0 +1,16 @@
import styles from "./htmlContent.module.css"
interface HtmlContentProps {
html: string
}
export default function HtmlContent({ html }: HtmlContentProps) {
return (
<div
className={styles.htmlContent}
dangerouslySetInnerHTML={{
__html: html,
}}
/>
)
}

View File

@@ -0,0 +1,79 @@
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import styles from "./sidebar.module.css"
import type { Hotel } from "@/types/hotel"
interface WellnessSidebarProps {
hotel: Hotel
}
export default async function WellnessSidebar({ hotel }: WellnessSidebarProps) {
const intl = await getIntl()
return (
<aside className={styles.sidebar}>
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Opening hours" })}
</Title>
{hotel.healthFacilities.map((facility) => (
<div key={facility.type}>
<Subtitle type="two" color="uiTextHighContrast" asChild>
<h4>{intl.formatMessage({ id: facility.type })}</h4>
</Subtitle>
<Body color="uiTextHighContrast">
{facility.openingDetails.openingHours.ordinary.alwaysOpen
? intl.formatMessage({ id: "Mon-Fri Always open" })
: intl.formatMessage(
{ id: "Mon-Fri {openingTime}-{closingTime}" },
{
openingTime:
facility.openingDetails.openingHours.ordinary.openingTime,
closingTime:
facility.openingDetails.openingHours.ordinary.closingTime,
}
)}
</Body>
<Body color="uiTextHighContrast">
{facility.openingDetails.openingHours.weekends.alwaysOpen
? intl.formatMessage({ id: "Sat-Sun Always open" })
: intl.formatMessage(
{ id: "Sat-Sun {openingTime}-{closingTime}" },
{
openingTime:
facility.openingDetails.openingHours.weekends.openingTime,
closingTime:
facility.openingDetails.openingHours.weekends.closingTime,
}
)}
</Body>
</div>
))}
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Address" })}
</Title>
<div>
<Body color="uiTextHighContrast">{hotel.address.streetAddress}</Body>
<Body color="uiTextHighContrast">
{hotel.address.zipCode} {hotel.address.city}
</Body>
<Body color="uiTextHighContrast">{hotel.address.country}</Body>
</div>
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Contact us" })}
</Title>
<Link
href={`tel:${hotel.contactInformation.phoneNumber}`}
color="peach80"
textDecoration="underline"
>
{hotel.contactInformation.phoneNumber}
</Link>
</aside>
)
}

View File

@@ -0,0 +1,25 @@
import { wellnessSubPage } from "@/constants/routes/hotelSubpages"
import { getLang } from "@/i18n/serverContext"
import WellnessSidebar from "./WellnessSidebar"
import type { Hotel } from "@/types/hotel"
interface HotelSubpageSidebarProps {
subpage: string
hotel: Hotel
}
export default function HotelSubpageSidebar({
subpage,
hotel,
}: HotelSubpageSidebarProps) {
const lang = getLang()
switch (subpage) {
case wellnessSubPage[lang]:
return <WellnessSidebar hotel={hotel} />
default:
return null
}
}

View File

@@ -0,0 +1,4 @@
.sidebar {
display: grid;
gap: var(--Spacing-x2);
}

View File

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

View File

@@ -1,11 +1,19 @@
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { Suspense } from "react"
import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests" import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests"
import Body from "@/components/TempDesignSystem/Text/Body" import Breadcrumbs from "@/components/Breadcrumbs"
import Hero from "@/components/Hero"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
import Title from "@/components/TempDesignSystem/Text/Title" import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import HotelSubpageAdditionalContent from "./AdditionalContent"
import HtmlContent from "./HtmlContent"
import HotelSubpageSidebar from "./Sidebar"
import { getSubpageData } from "./utils" import { getSubpageData } from "./utils"
import styles from "./hotelSubpage.module.css" import styles from "./hotelSubpage.module.css"
@@ -20,39 +28,51 @@ export default async function HotelSubpage({
subpage, subpage,
}: HotelSubpageProps) { }: HotelSubpageProps) {
const lang = getLang() const lang = getLang()
const [hotelPageData, hotel] = await Promise.all([ const [intl, hotelPageData, hotelData] = await Promise.all([
getIntl(),
getHotelPage(), getHotelPage(),
getHotel({ hotelId, language: lang }), getHotel({ hotelId, language: lang }),
]) ])
if (!hotel?.hotel || !hotelPageData) { if (!hotelData?.hotel || !hotelPageData) {
notFound() notFound()
} }
const pageData = getSubpageData(subpage, lang, hotel.additionalData)
const pageData = getSubpageData(intl, subpage, hotelData)
if (!pageData) { if (!pageData) {
notFound() notFound()
} }
const hotelData = hotel.hotel
return ( return (
<> <>
<section className={styles.hotelSubpage}> <section className={styles.hotelSubpage}>
<header className={styles.header}> <div className={styles.header}>
{/* breadcrumbs */} <Suspense fallback={<BreadcrumbsSkeleton />}>
<div className={styles.heroContainer}>{/* hero image */}</div> <Breadcrumbs variant="hotelSubpage" />
</header> </Suspense>
{pageData.heroImage && (
<div className={styles.heroWrapper}>
<Hero src={pageData.heroImage.src} alt={pageData.heroImage.alt} />
</div>
)}
</div>
<div className={styles.contentContainer}> <div className={styles.contentContainer}>
<main className={styles.mainContent}> <main className={styles.mainContent}>
{/* Main content */} <div className={styles.intro}>
<Title level="h1"> <Title level="h1">{pageData.heading}</Title>
{subpage} for {hotelData.name} <Preamble>{pageData.elevatorPitch}</Preamble>
</Title> </div>
<Body>{pageData.elevatorPitch}</Body>
{pageData.mainBody && <HtmlContent html={pageData.mainBody} />}
<HotelSubpageAdditionalContent
subpage={subpage}
hotel={hotelData.hotel}
/>
</main> </main>
{/* Sidebar */} <HotelSubpageSidebar subpage={subpage} hotel={hotelData.hotel} />
</div> </div>
</section> </section>
{/* Tracking */} {/* Tracking */}

View File

@@ -1,16 +1,34 @@
import { parkingSubPage } from "@/constants/routes/hotelSubpages" import { wellnessSubPage } from "@/constants/routes/hotelSubpages"
import { getLang } from "@/i18n/serverContext"
import type { IntlShape } from "react-intl"
import type { HotelData } from "@/types/hotel" import type { HotelData } from "@/types/hotel"
import type { Lang } from "@/constants/languages"
export function getSubpageData( export function getSubpageData(
intl: IntlShape,
subpage: string, subpage: string,
lang: Lang, hotelData: HotelData
additionalData: HotelData["additionalData"]
) { ) {
const lang = getLang()
const additionalData = hotelData.additionalData
const hotel = hotelData.hotel
switch (subpage) { switch (subpage) {
case parkingSubPage[lang]: case wellnessSubPage[lang]:
return additionalData.hotelParking const heroImage = hotel.healthFacilities.find(
(fac) => fac.content.images.length
)?.content.images[0]
return {
...additionalData.healthAndFitness,
heading: intl.formatMessage({ id: "Wellness & Exercise" }),
heroImage: heroImage
? {
src: heroImage.imageSizes.medium,
alt: heroImage.metaData.altText || "",
}
: null,
}
default: default:
return null return null
} }

View File

@@ -16,6 +16,7 @@ export const breadcrumbsVariants = cva(styles.breadcrumbs, {
[PageContentTypeEnum.hotelPage]: styles.headerWidth, [PageContentTypeEnum.hotelPage]: styles.headerWidth,
[PageContentTypeEnum.loyaltyPage]: styles.fullWidth, [PageContentTypeEnum.loyaltyPage]: styles.fullWidth,
[PageContentTypeEnum.startPage]: styles.contentWidth, [PageContentTypeEnum.startPage]: styles.contentWidth,
hotelSubpage: styles.contentWidth,
default: styles.fullWidth, default: styles.fullWidth,
}, },
}, },

View File

@@ -6,3 +6,12 @@ export const parkingSubPage = {
fi: "parkkipaikka", fi: "parkkipaikka",
de: "Parkplatz", de: "Parkplatz",
} }
export const wellnessSubPage = {
en: "wellness",
sv: "spa",
no: "spa",
da: "spa",
fi: "spa",
de: "Wellness",
}

View File

@@ -2,7 +2,7 @@ import type { Hotel } from "@/types/hotel"
export type WellnessAndExerciseSidePeekProps = { export type WellnessAndExerciseSidePeekProps = {
healthFacilities: Hotel["healthFacilities"] healthFacilities: Hotel["healthFacilities"]
wellnessExerciseButton?: string wellnessExerciseButton: boolean
spaPage?: { spaPage?: {
buttonCTA: string buttonCTA: string
url: string url: string