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:
@@ -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>
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.htmlContent {
|
||||||
|
}
|
||||||
16
components/ContentType/HotelSubpage/HtmlContent/index.tsx
Normal file
16
components/ContentType/HotelSubpage/HtmlContent/index.tsx
Normal 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,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
components/ContentType/HotelSubpage/Sidebar/index.tsx
Normal file
25
components/ContentType/HotelSubpage/Sidebar/index.tsx
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.sidebar {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user