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

View File

@@ -231,6 +231,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
<WellnessAndExerciseSidePeek
healthFacilities={healthFacilities}
spaPage={spaPage?.spa_page}
wellnessExerciseButton={displayWebPage.healthGym}
/>
<RestaurantBarSidePeek restaurants={restaurants} />
{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 {
display: grid;
gap: var(--Spacing-x4);
background-color: var(--Base-Surface-Subtle-Normal);
padding: var(--Spacing-x4) 0;
padding-bottom: var(--Spacing-x4);
}
.heroContainer {
.heroWrapper {
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;
max-width: var(--max-width-content);
margin: 0 auto;
padding: var(--Spacing-x4) 0;
}
.mainContent {
grid-area: main;
display: grid;
width: 100%;
gap: var(--Spacing-x6);
margin: 0 auto;
max-width: var(--max-width-content);
gap: var(--Spacing-x4);
}
@media (min-width: 768px) {
.contentContainer {
padding: var(--Spacing-x4) 0;
}
.heroContainer {
padding: var(--Spacing-x4) 0;
}
.header {
padding: var(--Spacing-x4) 0;
}
.intro {
display: grid;
gap: var(--Spacing-x2);
}
@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

@@ -1,11 +1,19 @@
import { notFound } from "next/navigation"
import { Suspense } from "react"
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 { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import HotelSubpageAdditionalContent from "./AdditionalContent"
import HtmlContent from "./HtmlContent"
import HotelSubpageSidebar from "./Sidebar"
import { getSubpageData } from "./utils"
import styles from "./hotelSubpage.module.css"
@@ -20,39 +28,51 @@ export default async function HotelSubpage({
subpage,
}: HotelSubpageProps) {
const lang = getLang()
const [hotelPageData, hotel] = await Promise.all([
const [intl, hotelPageData, hotelData] = await Promise.all([
getIntl(),
getHotelPage(),
getHotel({ hotelId, language: lang }),
])
if (!hotel?.hotel || !hotelPageData) {
if (!hotelData?.hotel || !hotelPageData) {
notFound()
}
const pageData = getSubpageData(subpage, lang, hotel.additionalData)
const pageData = getSubpageData(intl, subpage, hotelData)
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.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs variant="hotelSubpage" />
</Suspense>
{pageData.heroImage && (
<div className={styles.heroWrapper}>
<Hero src={pageData.heroImage.src} alt={pageData.heroImage.alt} />
</div>
)}
</div>
<div className={styles.contentContainer}>
<main className={styles.mainContent}>
{/* Main content */}
<Title level="h1">
{subpage} for {hotelData.name}
</Title>
<Body>{pageData.elevatorPitch}</Body>
<div className={styles.intro}>
<Title level="h1">{pageData.heading}</Title>
<Preamble>{pageData.elevatorPitch}</Preamble>
</div>
{pageData.mainBody && <HtmlContent html={pageData.mainBody} />}
<HotelSubpageAdditionalContent
subpage={subpage}
hotel={hotelData.hotel}
/>
</main>
{/* Sidebar */}
<HotelSubpageSidebar subpage={subpage} hotel={hotelData.hotel} />
</div>
</section>
{/* 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 { Lang } from "@/constants/languages"
export function getSubpageData(
intl: IntlShape,
subpage: string,
lang: Lang,
additionalData: HotelData["additionalData"]
hotelData: HotelData
) {
const lang = getLang()
const additionalData = hotelData.additionalData
const hotel = hotelData.hotel
switch (subpage) {
case parkingSubPage[lang]:
return additionalData.hotelParking
case wellnessSubPage[lang]:
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:
return null
}

View File

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