feat(SW-435): Added hotel subpage for reviews, refactored subpages
Approved-by: Matilda Landström
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
||||||
|
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { appendSlugToPathname } from "@/utils/appendSlugToPathname"
|
||||||
|
|
||||||
|
import styles from "./tripAdvisorLink.module.css"
|
||||||
|
|
||||||
|
import type { HotelTripAdvisor } from "@/types/hotel"
|
||||||
|
|
||||||
|
interface TripAdvisorLinkProps {
|
||||||
|
tripAdvisor: NonNullable<HotelTripAdvisor>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function TripAdvisorLink({
|
||||||
|
tripAdvisor,
|
||||||
|
}: TripAdvisorLinkProps) {
|
||||||
|
const intl = await getIntl()
|
||||||
|
const { rating, numberOfReviews, reviews } = tripAdvisor
|
||||||
|
const hasTripAdvisorData = !!(rating && numberOfReviews)
|
||||||
|
|
||||||
|
if (!hasTripAdvisorData) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedTripAdvisorText = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{rating} ({count} reviews on Tripadvisor)",
|
||||||
|
},
|
||||||
|
{ rating, count: numberOfReviews }
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasTripAdvisorIframeSrc = !!reviews.widgetScriptEmbedUrlIframe
|
||||||
|
const tripAdvisorHref = hasTripAdvisorIframeSrc
|
||||||
|
? appendSlugToPathname("reviews")
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (!tripAdvisorHref) {
|
||||||
|
return (
|
||||||
|
<span className={styles.tripAdvisorText}>
|
||||||
|
<TripadvisorIcon color="CurrentColor" />
|
||||||
|
{formattedTripAdvisorText}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
variant="icon"
|
||||||
|
textDecoration="underline"
|
||||||
|
color="peach80"
|
||||||
|
href={tripAdvisorHref}
|
||||||
|
>
|
||||||
|
<TripadvisorIcon color="CurrentColor" />
|
||||||
|
{formattedTripAdvisorText}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.tripAdvisorText {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
align-items: center;
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getSingleDecimal } from "@/utils/numberFormatting"
|
import { getSingleDecimal } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import TripAdvisorLink from "./TripAdvisorLink"
|
||||||
|
|
||||||
import styles from "./introSection.module.css"
|
import styles from "./introSection.module.css"
|
||||||
|
|
||||||
import { SidepeekSlugs } from "@/types/components/hotelPage/hotelPage"
|
import { SidepeekSlugs } from "@/types/components/hotelPage/hotelPage"
|
||||||
@@ -29,17 +30,6 @@ export default async function IntroSection({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const formattedLocationText = `${streetAddress}, ${city} (${formattedDistanceText})`
|
const formattedLocationText = `${streetAddress}, ${city} (${formattedDistanceText})`
|
||||||
const hasTripAdvisorData = !!(
|
|
||||||
tripAdvisor?.rating && tripAdvisor?.numberOfReviews
|
|
||||||
)
|
|
||||||
const formattedTripAdvisorText = hasTripAdvisorData
|
|
||||||
? intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "{rating} ({count} reviews on Tripadvisor)",
|
|
||||||
},
|
|
||||||
{ rating: tripAdvisor.rating, count: tripAdvisor.numberOfReviews }
|
|
||||||
)
|
|
||||||
: ""
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.introSection}>
|
<section className={styles.introSection}>
|
||||||
@@ -59,12 +49,8 @@ export default async function IntroSection({
|
|||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
<p className={styles.bodyText}>{formattedLocationText}</p>
|
<p className={styles.bodyText}>{formattedLocationText}</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
{formattedTripAdvisorText && (
|
|
||||||
<span className={styles.tripAdvisorText}>
|
{tripAdvisor ? <TripAdvisorLink tripAdvisor={tripAdvisor} /> : null}
|
||||||
<TripadvisorIcon color="Icon/Interactive/Secondary" />
|
|
||||||
{formattedTripAdvisorText}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.subtitleContent}>
|
<div className={styles.subtitleContent}>
|
||||||
<Typography variant="Body/Lead text">
|
<Typography variant="Body/Lead text">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
.mainContent {
|
.mainContent {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
|
justify-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitleContent {
|
.subtitleContent {
|
||||||
@@ -15,18 +16,6 @@
|
|||||||
gap: var(--Spacing-x-one-and-half);
|
gap: var(--Spacing-x-one-and-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tripAdvisorText {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--Space-x05);
|
|
||||||
align-items: center;
|
|
||||||
color: var(--Text-Secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tripAdvisorText svg,
|
|
||||||
.tripAdvisorText svg * {
|
|
||||||
fill: var(--Icon-Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: var(--Text-Heading);
|
color: var(--Text-Heading);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
.accessibilitySubpage {
|
||||||
|
padding-bottom: var(--Space-x8);
|
||||||
|
color: var(--Text-Default);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
width: var(--max-width-content);
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
max-width: var(--max-width-text-block);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: var(--Text-Heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accessibilityInformation {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accessibilityGroup {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem::before {
|
||||||
|
content: url("/_static/icons/heart.svg");
|
||||||
|
position: relative;
|
||||||
|
height: 8px;
|
||||||
|
top: 3px;
|
||||||
|
margin-right: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
|
import HeroHeader from "../HeroHeader"
|
||||||
|
import HtmlContent from "../HtmlContent"
|
||||||
|
|
||||||
|
import styles from "./accessibilitySubpage.module.css"
|
||||||
|
|
||||||
|
import type { AdditionalData } from "@/types/hotel"
|
||||||
|
|
||||||
|
interface AccessibilitySubpageProps {
|
||||||
|
hotelName: string
|
||||||
|
additionalData: AdditionalData
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AccessibilitySubpage({
|
||||||
|
hotelName,
|
||||||
|
additionalData,
|
||||||
|
}: AccessibilitySubpageProps) {
|
||||||
|
const intl = await getIntl()
|
||||||
|
const { hotelSpecialNeeds, specialNeedGroups, accessibility } = additionalData
|
||||||
|
const { mainBody, elevatorPitch } = hotelSpecialNeeds
|
||||||
|
const heroImage = accessibility?.heroImages[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className={styles.accessibilitySubpage}>
|
||||||
|
<HeroHeader
|
||||||
|
breadcrumbsTitle={intl.formatMessage({
|
||||||
|
defaultMessage: "Accessibility",
|
||||||
|
})}
|
||||||
|
heroImage={heroImage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.contentContainer}>
|
||||||
|
<Typography variant="Title/md">
|
||||||
|
<h1 className={styles.heading}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Accessibility at {hotel}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hotel: hotelName,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Divider color="baseSurfaceSubtleHover" className={styles.divider} />
|
||||||
|
|
||||||
|
<main className={styles.mainContent}>
|
||||||
|
{elevatorPitch ? (
|
||||||
|
<div className={styles.intro}>
|
||||||
|
<Typography variant="Body/Lead text">
|
||||||
|
<p>{elevatorPitch}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{mainBody ? <HtmlContent html={mainBody} /> : null}
|
||||||
|
|
||||||
|
<div className={styles.accessibilityInformation}>
|
||||||
|
{specialNeedGroups.map((accessibilityGroup) => (
|
||||||
|
<div
|
||||||
|
key={accessibilityGroup.name}
|
||||||
|
className={styles.accessibilityGroup}
|
||||||
|
>
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<h2>{accessibilityGroup.name}</h2>
|
||||||
|
</Typography>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{accessibilityGroup.specialNeeds.map((groupItem) => (
|
||||||
|
<Typography
|
||||||
|
key={groupItem.name}
|
||||||
|
variant="Body/Paragraph/mdRegular"
|
||||||
|
>
|
||||||
|
<li className={styles.listItem}>
|
||||||
|
{groupItem.details
|
||||||
|
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||||
|
`${groupItem.name}: ${groupItem.details}`
|
||||||
|
: groupItem.name}
|
||||||
|
</li>
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
.container {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.accessibilityGroup {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list > li::before {
|
|
||||||
content: url("/_static/icons/heart.svg");
|
|
||||||
position: relative;
|
|
||||||
height: 8px;
|
|
||||||
top: 3px;
|
|
||||||
margin-right: var(--Spacing-x1);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
||||||
|
|
||||||
import styles from "./accessibilityAdditionalContent.module.css"
|
|
||||||
|
|
||||||
import type { AdditionalData } from "@/types/hotel"
|
|
||||||
|
|
||||||
interface AccessibilityAdditionalContentProps {
|
|
||||||
additionalData: AdditionalData
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AccessibilityAdditionalContent({
|
|
||||||
additionalData,
|
|
||||||
}: AccessibilityAdditionalContentProps) {
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
{additionalData.specialNeedGroups.map((accessibilityGroup) => (
|
|
||||||
<div
|
|
||||||
key={accessibilityGroup.name}
|
|
||||||
className={styles.accessibilityGroup}
|
|
||||||
>
|
|
||||||
<Subtitle type="two" color="uiTextHighContrast" asChild>
|
|
||||||
<h2>{accessibilityGroup.name}</h2>
|
|
||||||
</Subtitle>
|
|
||||||
<ul className={styles.list}>
|
|
||||||
{accessibilityGroup.specialNeeds.map((groupItem) => (
|
|
||||||
<Body key={groupItem.name} asChild>
|
|
||||||
<li>
|
|
||||||
{groupItem.details
|
|
||||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
|
||||||
`${groupItem.name}: ${groupItem.details}`
|
|
||||||
: groupItem.name}
|
|
||||||
</li>
|
|
||||||
</Body>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import ParkingInformation from "@/components/ParkingInformation"
|
|
||||||
|
|
||||||
import styles from "./parkingAdditionalContent.module.css"
|
|
||||||
|
|
||||||
import type { Hotel } from "@/types/hotel"
|
|
||||||
|
|
||||||
interface ParkingAdditionalContentProps {
|
|
||||||
hotel: Hotel
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function ParkingAdditionalContent({
|
|
||||||
hotel,
|
|
||||||
}: ParkingAdditionalContentProps) {
|
|
||||||
const parking = hotel.parking
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.additionalContent}>
|
|
||||||
{parking.map((data) => (
|
|
||||||
<ParkingInformation
|
|
||||||
key={data.type}
|
|
||||||
parking={data}
|
|
||||||
showExternalParkingButton={false}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
.additionalContent {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Spacing-x4);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import AccessibilityAdditionalContent from "./Accessibility"
|
|
||||||
import ParkingAdditionalContent from "./Parking"
|
|
||||||
|
|
||||||
import type { AdditionalData, Hotel } from "@/types/hotel"
|
|
||||||
|
|
||||||
interface HotelSubpageAdditionalContentProps {
|
|
||||||
subpage: string
|
|
||||||
hotel: Hotel
|
|
||||||
additionalData: AdditionalData
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HotelSubpageAdditionalContent({
|
|
||||||
subpage,
|
|
||||||
hotel,
|
|
||||||
additionalData,
|
|
||||||
}: HotelSubpageAdditionalContentProps) {
|
|
||||||
switch (subpage) {
|
|
||||||
case additionalData.hotelParking.nameInUrl:
|
|
||||||
return <ParkingAdditionalContent hotel={hotel} />
|
|
||||||
case additionalData.healthAndFitness.nameInUrl:
|
|
||||||
return null
|
|
||||||
case additionalData.hotelSpecialNeeds.nameInUrl:
|
|
||||||
return <AccessibilityAdditionalContent additionalData={additionalData} />
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.header {
|
||||||
|
display: grid;
|
||||||
|
background-color: var(--Base-Surface-Subtle-Normal);
|
||||||
|
padding-bottom: var(--Space-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroWrapper {
|
||||||
|
width: var(--max-width-content);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Suspense } from "react"
|
||||||
|
|
||||||
|
import Breadcrumbs from "@/components/Breadcrumbs"
|
||||||
|
import Hero from "@/components/Hero"
|
||||||
|
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||||
|
|
||||||
|
import styles from "./heroHeader.module.css"
|
||||||
|
|
||||||
|
import type { ApiImage } from "@/types/hotel"
|
||||||
|
|
||||||
|
interface HeroHeaderProps {
|
||||||
|
breadcrumbsTitle: string
|
||||||
|
heroImage?: ApiImage
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function HeroHeader({
|
||||||
|
breadcrumbsTitle,
|
||||||
|
heroImage,
|
||||||
|
}: HeroHeaderProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Suspense fallback={<BreadcrumbsSkeleton size="contentWidth" />}>
|
||||||
|
<Breadcrumbs subpageTitle={breadcrumbsTitle} size="contentWidth" />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
|
{heroImage ? (
|
||||||
|
<div className={styles.heroWrapper}>
|
||||||
|
<Hero
|
||||||
|
src={heroImage.imageSizes.medium}
|
||||||
|
alt={heroImage.metaData.altText || ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
.ul,
|
.ul,
|
||||||
.ol {
|
.ol {
|
||||||
padding: var(--Spacing-x2) var(--Spacing-x0);
|
padding: var(--Space-x2) var(--Space-x0);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Space-x1);
|
||||||
margin-left: var(--Spacing-x2);
|
margin-left: var(--Space-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol > li::marker {
|
.ol > li::marker {
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.li {
|
.li {
|
||||||
margin-left: var(--Spacing-x3);
|
margin-left: var(--Space-x3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.li > p {
|
.li > p {
|
||||||
|
|||||||
@@ -5,17 +5,15 @@ import Grids from "@/components/TempDesignSystem/Grids"
|
|||||||
import MeetingRoomCard from "@/components/TempDesignSystem/MeetingRoomCard"
|
import MeetingRoomCard from "@/components/TempDesignSystem/MeetingRoomCard"
|
||||||
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||||
|
|
||||||
import styles from "./meetingsAdditionalContent.module.css"
|
import styles from "./meetingRooms.module.css"
|
||||||
|
|
||||||
import type { MeetingRooms } from "@/types/components/hotelPage/meetingRooms"
|
import type { MeetingRooms } from "@/types/components/hotelPage/meetingRooms"
|
||||||
|
|
||||||
interface MeetingsAdditionalContentProps {
|
interface MeetingRoomsProps {
|
||||||
rooms: MeetingRooms
|
rooms: MeetingRooms
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MeetingsAdditionalContent({
|
export default function MeetingRooms({ rooms }: MeetingRoomsProps) {
|
||||||
rooms,
|
|
||||||
}: MeetingsAdditionalContentProps) {
|
|
||||||
const showToggleButton = rooms.length > 3
|
const showToggleButton = rooms.length > 3
|
||||||
const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton)
|
const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton)
|
||||||
|
|
||||||
@@ -8,6 +8,6 @@
|
|||||||
|
|
||||||
.section {
|
.section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x4);
|
gap: var(--Space-x4);
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { getMeetingRooms } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
import { meetingPackageDestinationByHotelId } from "@/components/MeetingPackageWidget/utils"
|
||||||
|
import StickyMeetingPackageWidget from "@/components/StickyMeetingPackageWidget"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
import { safeTry } from "@/utils/safeTry"
|
||||||
|
|
||||||
|
import HeroHeader from "../HeroHeader"
|
||||||
|
import HtmlContent from "../HtmlContent"
|
||||||
|
import MeetingsSidebar from "../Sidebar/MeetingsSidebar"
|
||||||
|
import MeetingsAdditionalContent from "./MeetingRooms"
|
||||||
|
|
||||||
|
import styles from "./meetingsSubpage.module.css"
|
||||||
|
|
||||||
|
import type { AdditionalData, Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
|
interface MeetingsSubpageProps {
|
||||||
|
hotelId: string
|
||||||
|
hotel: Hotel
|
||||||
|
additionalData: AdditionalData
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function MeetingsSubpage({
|
||||||
|
hotelId,
|
||||||
|
hotel,
|
||||||
|
additionalData,
|
||||||
|
}: MeetingsSubpageProps) {
|
||||||
|
const intl = await getIntl()
|
||||||
|
const lang = getLang()
|
||||||
|
const heading = intl.formatMessage({
|
||||||
|
defaultMessage: "Meetings, Conferences & Events",
|
||||||
|
})
|
||||||
|
const mainBody = additionalData.meetingRooms.mainBody
|
||||||
|
const elevatorPitch = hotel.hotelContent.texts.meetingDescription?.medium
|
||||||
|
const heroImage = additionalData.conferencesAndMeetings?.heroImages[0]
|
||||||
|
const [meetingRooms] = await safeTry(
|
||||||
|
getMeetingRooms({ hotelId, language: lang })
|
||||||
|
)
|
||||||
|
const meetingPackageDestination = meetingPackageDestinationByHotelId[hotelId]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StickyMeetingPackageWidget destination={meetingPackageDestination} />
|
||||||
|
<section className={styles.meetingsSubpage}>
|
||||||
|
<HeroHeader breadcrumbsTitle={heading} heroImage={heroImage} />
|
||||||
|
|
||||||
|
<div className={styles.contentContainer}>
|
||||||
|
<Typography variant="Title/md">
|
||||||
|
<h1 className={styles.heading}>{heading}</h1>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<aside className={styles.sidebar}>
|
||||||
|
{meetingRooms ? (
|
||||||
|
<MeetingsSidebar
|
||||||
|
country={hotel.address.country}
|
||||||
|
email={meetingRooms[0].attributes.email}
|
||||||
|
phoneNumber={meetingRooms[0].attributes.phoneNumber}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</aside>
|
||||||
|
<Divider color="baseSurfaceSubtleHover" className={styles.divider} />
|
||||||
|
<main className={styles.mainContent}>
|
||||||
|
{elevatorPitch ? (
|
||||||
|
<div className={styles.intro}>
|
||||||
|
<Typography variant="Body/Lead text">
|
||||||
|
<p>{elevatorPitch}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{mainBody ? <HtmlContent html={mainBody} /> : null}
|
||||||
|
</main>
|
||||||
|
{meetingRooms ? (
|
||||||
|
<div className={styles.meetingsInformation}>
|
||||||
|
<MeetingsAdditionalContent rooms={meetingRooms} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.meetingsSubpage {
|
||||||
|
padding-bottom: var(--Space-x8);
|
||||||
|
color: var(--Text-Default);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
width: var(--max-width-content);
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: var(--Text-Heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parkingInformation {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.contentContainer {
|
||||||
|
grid-template-columns: var(--max-width-text-block) 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
row-gap: var(--Space-x2);
|
||||||
|
column-gap: var(--Space-x8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meetingsInformation {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import ParkingInformation from "@/components/ParkingInformation"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
|
import HeroHeader from "../HeroHeader"
|
||||||
|
import HtmlContent from "../HtmlContent"
|
||||||
|
import ParkingSidebar from "../Sidebar/ParkingSidebar"
|
||||||
|
|
||||||
|
import styles from "./parkingSubpage.module.css"
|
||||||
|
|
||||||
|
import type { AdditionalData, Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
|
interface ParkingSubpageProps {
|
||||||
|
hotel: Hotel
|
||||||
|
additionalData: AdditionalData
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ParkingSubpage({
|
||||||
|
hotel,
|
||||||
|
additionalData,
|
||||||
|
}: ParkingSubpageProps) {
|
||||||
|
const intl = await getIntl()
|
||||||
|
const heading = intl.formatMessage({
|
||||||
|
defaultMessage: "Parking",
|
||||||
|
})
|
||||||
|
const { mainBody, elevatorPitch } = additionalData.hotelParking
|
||||||
|
const heroImage = additionalData.parkingImages?.heroImages[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className={styles.parkingSubpage}>
|
||||||
|
<HeroHeader breadcrumbsTitle={heading} heroImage={heroImage} />
|
||||||
|
|
||||||
|
<div className={styles.contentContainer}>
|
||||||
|
<Typography variant="Title/md">
|
||||||
|
<h1 className={styles.heading}>{heading}</h1>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<aside className={styles.sidebar}>
|
||||||
|
<ParkingSidebar
|
||||||
|
address={hotel.address}
|
||||||
|
contactInformation={hotel.contactInformation}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
<Divider color="baseSurfaceSubtleHover" className={styles.divider} />
|
||||||
|
<main className={styles.mainContent}>
|
||||||
|
{elevatorPitch ? (
|
||||||
|
<div className={styles.intro}>
|
||||||
|
<Typography variant="Body/Lead text">
|
||||||
|
<p>{elevatorPitch}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{mainBody ? <HtmlContent html={mainBody} /> : null}
|
||||||
|
|
||||||
|
<div className={styles.parkingInformation}>
|
||||||
|
{hotel.parking.map((data) => (
|
||||||
|
<ParkingInformation
|
||||||
|
key={data.type}
|
||||||
|
parking={data}
|
||||||
|
showExternalParkingButton={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
.parkingSubpage {
|
||||||
|
padding-bottom: var(--Space-x8);
|
||||||
|
color: var(--Text-Default);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
width: var(--max-width-content);
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: var(--Text-Heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parkingInformation {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.contentContainer {
|
||||||
|
grid-template-columns: var(--max-width-text-block) 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
row-gap: var(--Space-x2);
|
||||||
|
column-gap: var(--Space-x8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import ButtonLink from "@/components/ButtonLink"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
|
import HeroHeader from "../HeroHeader"
|
||||||
|
import HtmlContent from "../HtmlContent"
|
||||||
|
import RestaurantSidebar from "../Sidebar/RestaurantSidebar"
|
||||||
|
|
||||||
|
import styles from "./restaurantSubpage.module.css"
|
||||||
|
|
||||||
|
import type { Hotel, Restaurant } from "@/types/hotel"
|
||||||
|
|
||||||
|
interface RestaurantSubpageProps {
|
||||||
|
restaurant: Restaurant
|
||||||
|
hotelAddress: Hotel["address"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function RestaurantSubpage({
|
||||||
|
restaurant,
|
||||||
|
hotelAddress,
|
||||||
|
}: RestaurantSubpageProps) {
|
||||||
|
const intl = await getIntl()
|
||||||
|
const { mainBody, elevatorPitch, name, bookTableUrl } = restaurant
|
||||||
|
const heroImage = restaurant.content.images[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className={styles.restaurantSubpage}>
|
||||||
|
<HeroHeader breadcrumbsTitle={name} heroImage={heroImage} />
|
||||||
|
|
||||||
|
<div className={styles.contentContainer}>
|
||||||
|
<Typography variant="Title/md">
|
||||||
|
<h1 className={styles.heading}>{name}</h1>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<aside className={styles.sidebar}>
|
||||||
|
<RestaurantSidebar
|
||||||
|
restaurant={restaurant}
|
||||||
|
hotelAddress={hotelAddress}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
<Divider color="baseSurfaceSubtleHover" className={styles.divider} />
|
||||||
|
<main className={styles.mainContent}>
|
||||||
|
{elevatorPitch ? (
|
||||||
|
<div className={styles.intro}>
|
||||||
|
<Typography variant="Body/Lead text">
|
||||||
|
<p>{elevatorPitch}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{mainBody ? <HtmlContent html={mainBody} /> : null}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{bookTableUrl ? (
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<ButtonLink
|
||||||
|
href={bookTableUrl}
|
||||||
|
variant="Primary"
|
||||||
|
typography="Body/Paragraph/mdBold"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Book a table",
|
||||||
|
})}
|
||||||
|
</ButtonLink>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
.restaurantSubpage {
|
||||||
|
padding-bottom: var(--Space-x8);
|
||||||
|
color: var(--Text-Default);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
width: var(--max-width-content);
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: var(--Text-Heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
position: sticky;
|
||||||
|
padding: var(--Space-x3) var(--Space-x2);
|
||||||
|
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||||
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.contentContainer {
|
||||||
|
grid-template-columns: var(--max-width-text-block) 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
row-gap: var(--Space-x2);
|
||||||
|
column-gap: var(--Space-x8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import NextImage from "next/image"
|
||||||
|
import { notFound } from "next/navigation"
|
||||||
|
import { Suspense } from "react"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import Breadcrumbs from "@/components/Breadcrumbs"
|
||||||
|
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
|
import styles from "./reviewsSubpage.module.css"
|
||||||
|
|
||||||
|
import type { Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
|
interface ReviewsSubpageProps {
|
||||||
|
hotel: Hotel
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ReviewsSubpage({ hotel }: ReviewsSubpageProps) {
|
||||||
|
const intl = await getIntl()
|
||||||
|
const lang = getLang()
|
||||||
|
const tripAdvisorData = hotel.ratings?.tripAdvisor
|
||||||
|
|
||||||
|
if (!tripAdvisorData?.reviews.widgetScriptEmbedUrlIframe) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const awardsLogos = tripAdvisorData.awards
|
||||||
|
.map((award) => ({
|
||||||
|
imageUrl: award.images.small,
|
||||||
|
altText: award.displayName,
|
||||||
|
}))
|
||||||
|
.filter((award) => !!award.imageUrl)
|
||||||
|
const showNordicEcoLabel = !!hotel.hotelFacts?.ecoLabels.nordicEcoLabel
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className={styles.reviewsSubpage}>
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<BreadcrumbsSkeleton
|
||||||
|
size="contentWidth"
|
||||||
|
color="Surface/Secondary/Default"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Breadcrumbs
|
||||||
|
subpageTitle={intl.formatMessage({
|
||||||
|
defaultMessage: "Reviews",
|
||||||
|
})}
|
||||||
|
size="contentWidth"
|
||||||
|
color="Surface/Secondary/Default"
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<Typography variant="Title/md">
|
||||||
|
<h1 className={styles.heading}>{hotel.name}</h1>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<p className={styles.subheading}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Ratings and reviews",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className={styles.contentContainer}>
|
||||||
|
<aside className={styles.sidebar}>
|
||||||
|
<Typography variant="Title/xs">
|
||||||
|
<h2 className={styles.sidebarHeading}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Awards and certifications",
|
||||||
|
})}
|
||||||
|
</h2>
|
||||||
|
</Typography>
|
||||||
|
<div className={styles.sidebarContent}>
|
||||||
|
{showNordicEcoLabel ? (
|
||||||
|
<NextImage
|
||||||
|
src={`/_static/img/icons/swan-eco/swan_eco_dark_${lang}.png`}
|
||||||
|
alt={intl.formatMessage({
|
||||||
|
defaultMessage: "Nordic Swan Ecolabel",
|
||||||
|
})}
|
||||||
|
className={styles.logo}
|
||||||
|
height={100}
|
||||||
|
width={100}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{awardsLogos?.map((award) => (
|
||||||
|
<NextImage
|
||||||
|
src={award.imageUrl}
|
||||||
|
alt={award.altText}
|
||||||
|
className={styles.logo}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
key={award.imageUrl}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main className={styles.mainContent}>
|
||||||
|
<iframe
|
||||||
|
className={styles.iframe}
|
||||||
|
src={tripAdvisorData.reviews.widgetScriptEmbedUrlIframe}
|
||||||
|
loading="lazy"
|
||||||
|
title={intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Ratings and reviews for {hotelName}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hotelName: hotel.name,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
.reviewsSubpage {
|
||||||
|
padding-bottom: var(--Space-x8);
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: var(--max-width-content);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
padding: var(--Space-x4) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading,
|
||||||
|
.sidebarHeading {
|
||||||
|
color: var(--Text-Heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
width: var(--max-width-content);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarContent {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--Space-x7);
|
||||||
|
padding: var(--Space-x5) var(--Space-x15);
|
||||||
|
background-color: var(--Background-Secondary);
|
||||||
|
border-radius: var(--Corner-radius-lg);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 1100px; /* Maximum(ish) content height on desktop without the need of scrolling */
|
||||||
|
border-width: 0;
|
||||||
|
border-radius: var(--Corner-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 120px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1366px) {
|
||||||
|
.subheading,
|
||||||
|
.sidebarHeading {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.contentContainer {
|
||||||
|
grid-template-columns: var(--max-width-text-block) 1fr;
|
||||||
|
grid-template-areas: "main sidebar";
|
||||||
|
gap: var(--Space-x8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
grid-area: main;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
grid-area: sidebar;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ export default async function MeetingsSidebar({
|
|||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={styles.sidebar}>
|
<>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Typography variant="Title/xs" className={styles.heading}>
|
<Typography variant="Title/xs" className={styles.heading}>
|
||||||
<h3>
|
<h3>
|
||||||
@@ -51,6 +51,6 @@ export default async function MeetingsSidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,18 @@ import { Country } from "@/types/enums/country"
|
|||||||
import type { Hotel } from "@/types/hotel"
|
import type { Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
interface HotelSidebarProps {
|
interface HotelSidebarProps {
|
||||||
hotel: Hotel
|
address: Hotel["address"]
|
||||||
|
contactInformation: Hotel["contactInformation"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ParkingSidebar({ hotel }: HotelSidebarProps) {
|
export default async function ParkingSidebar({
|
||||||
|
address,
|
||||||
|
contactInformation,
|
||||||
|
}: HotelSidebarProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={styles.sidebar}>
|
<>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Typography variant="Title/xs" className={styles.heading}>
|
<Typography variant="Title/xs" className={styles.heading}>
|
||||||
<h3>
|
<h3>
|
||||||
@@ -28,11 +32,11 @@ export default async function ParkingSidebar({ hotel }: HotelSidebarProps) {
|
|||||||
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular" className={styles.text}>
|
<Typography variant="Body/Paragraph/mdRegular" className={styles.text}>
|
||||||
<div>
|
<div>
|
||||||
<p>{hotel.address.streetAddress}</p>
|
<p>{address.streetAddress}</p>
|
||||||
<p>
|
<p>
|
||||||
{hotel.address.zipCode} {hotel.address.city}
|
{address.zipCode} {address.city}
|
||||||
</p>
|
</p>
|
||||||
<p> {hotel.address.country}</p>
|
<p> {address.country}</p>
|
||||||
</div>
|
</div>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,10 +50,10 @@ export default async function ParkingSidebar({ hotel }: HotelSidebarProps) {
|
|||||||
</h3>
|
</h3>
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className={styles.contactDetails}>
|
<div className={styles.contactDetails}>
|
||||||
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
|
<Link href={`tel:${contactInformation.phoneNumber}`}>
|
||||||
{hotel.contactInformation.phoneNumber}
|
{contactInformation.phoneNumber}
|
||||||
</Link>
|
</Link>
|
||||||
{hotel.address.country === Country.Finland ? (
|
{address.country === Country.Finland ? (
|
||||||
<Typography
|
<Typography
|
||||||
variant="Body/Paragraph/mdRegular"
|
variant="Body/Paragraph/mdRegular"
|
||||||
className={styles.text}
|
className={styles.text}
|
||||||
@@ -63,12 +67,12 @@ export default async function ParkingSidebar({ hotel }: HotelSidebarProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
<Link
|
<Link
|
||||||
textDecoration="underline"
|
textDecoration="underline"
|
||||||
href={`mailto:${hotel.contactInformation.email}`}
|
href={`mailto:${contactInformation.email}`}
|
||||||
>
|
>
|
||||||
{hotel.contactInformation.email}
|
{contactInformation.email}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,21 +12,20 @@ import { Country } from "@/types/enums/country"
|
|||||||
import type { Hotel, Restaurant } from "@/types/hotel"
|
import type { Hotel, Restaurant } from "@/types/hotel"
|
||||||
|
|
||||||
interface RestaurantSidebarProps {
|
interface RestaurantSidebarProps {
|
||||||
hotel: Hotel
|
hotelAddress: Hotel["address"]
|
||||||
restaurant: Restaurant
|
restaurant: Restaurant
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function RestaurantSidebar({
|
export default async function RestaurantSidebar({
|
||||||
hotel,
|
hotelAddress,
|
||||||
restaurant,
|
restaurant,
|
||||||
}: RestaurantSidebarProps) {
|
}: RestaurantSidebarProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
const { address } = hotel
|
|
||||||
const { openingDetails, phoneNumber, email, bookTableUrl } = restaurant
|
const { openingDetails, phoneNumber, email, bookTableUrl } = restaurant
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={styles.sidebar}>
|
<>
|
||||||
{openingDetails.length ? (
|
{openingDetails.length ? (
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Typography variant="Title/xs" className={styles.heading}>
|
<Typography variant="Title/xs" className={styles.heading}>
|
||||||
@@ -97,9 +96,9 @@ export default async function RestaurantSidebar({
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="Body/Paragraph/mdRegular" className={styles.text}>
|
<Typography variant="Body/Paragraph/mdRegular" className={styles.text}>
|
||||||
<div>
|
<div>
|
||||||
<p>{address.streetAddress}</p>
|
<p>{hotelAddress.streetAddress}</p>
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
<p>{`${address.zipCode} ${address.city}`}</p>
|
<p>{`${hotelAddress.zipCode} ${hotelAddress.city}`}</p>
|
||||||
</div>
|
</div>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +115,7 @@ export default async function RestaurantSidebar({
|
|||||||
{phoneNumber && (
|
{phoneNumber && (
|
||||||
<>
|
<>
|
||||||
<Link href={`tel:${phoneNumber}`}>{phoneNumber}</Link>
|
<Link href={`tel:${phoneNumber}`}>{phoneNumber}</Link>
|
||||||
{address.country === Country.Finland ? (
|
{hotelAddress.country === Country.Finland ? (
|
||||||
<Typography
|
<Typography
|
||||||
variant="Body/Paragraph/mdRegular"
|
variant="Body/Paragraph/mdRegular"
|
||||||
className={styles.text}
|
className={styles.text}
|
||||||
@@ -138,6 +137,6 @@ export default async function RestaurantSidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</aside>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,23 @@ import { translateWellnessType } from "../../HotelPage/utils"
|
|||||||
import styles from "./sidebar.module.css"
|
import styles from "./sidebar.module.css"
|
||||||
|
|
||||||
import { Country } from "@/types/enums/country"
|
import { Country } from "@/types/enums/country"
|
||||||
import type { Hotel } from "@/types/hotel"
|
import type { HealthFacility, Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
interface WellnessSidebarProps {
|
interface WellnessSidebarProps {
|
||||||
hotel: Hotel
|
healthFacilities: HealthFacility[]
|
||||||
|
address: Hotel["address"]
|
||||||
|
contactInformation: Hotel["contactInformation"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function WellnessSidebar({ hotel }: WellnessSidebarProps) {
|
export default async function WellnessSidebar({
|
||||||
|
healthFacilities,
|
||||||
|
address,
|
||||||
|
contactInformation,
|
||||||
|
}: WellnessSidebarProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={styles.sidebar}>
|
<>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Typography variant="Title/xs" className={styles.heading}>
|
<Typography variant="Title/xs" className={styles.heading}>
|
||||||
<h3>
|
<h3>
|
||||||
@@ -27,7 +33,7 @@ export default async function WellnessSidebar({ hotel }: WellnessSidebarProps) {
|
|||||||
})}
|
})}
|
||||||
</h3>
|
</h3>
|
||||||
</Typography>
|
</Typography>
|
||||||
{hotel.healthFacilities.map((facility) => (
|
{healthFacilities.map((facility) => (
|
||||||
<div key={facility.type}>
|
<div key={facility.type}>
|
||||||
<Typography variant="Title/Subtitle/md" className={styles.text}>
|
<Typography variant="Title/Subtitle/md" className={styles.text}>
|
||||||
<h4>{translateWellnessType(facility.type, intl)}</h4>
|
<h4>{translateWellnessType(facility.type, intl)}</h4>
|
||||||
@@ -97,11 +103,11 @@ export default async function WellnessSidebar({ hotel }: WellnessSidebarProps) {
|
|||||||
className={styles.text}
|
className={styles.text}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p>{hotel.address.streetAddress}</p>
|
<p>{address.streetAddress}</p>
|
||||||
<p>
|
<p>
|
||||||
{hotel.address.zipCode} {hotel.address.city}
|
{address.zipCode} {address.city}
|
||||||
</p>
|
</p>
|
||||||
<p> {hotel.address.country}</p>
|
<p> {address.country}</p>
|
||||||
</div>
|
</div>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,10 +121,11 @@ export default async function WellnessSidebar({ hotel }: WellnessSidebarProps) {
|
|||||||
})}
|
})}
|
||||||
</h3>
|
</h3>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
|
<div className={styles.contactDetails}>
|
||||||
{hotel.contactInformation.phoneNumber}
|
<Link href={`tel:${contactInformation.phoneNumber}`}>
|
||||||
|
{contactInformation.phoneNumber}
|
||||||
</Link>
|
</Link>
|
||||||
{hotel.address.country === Country.Finland ? (
|
{address.country === Country.Finland ? (
|
||||||
<Typography
|
<Typography
|
||||||
variant="Body/Paragraph/mdRegular"
|
variant="Body/Paragraph/mdRegular"
|
||||||
className={styles.text}
|
className={styles.text}
|
||||||
@@ -131,6 +138,7 @@ export default async function WellnessSidebar({ hotel }: WellnessSidebarProps) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import MeetingsSidebar from "./MeetingsSidebar"
|
|
||||||
import ParkingSidebar from "./ParkingSidebar"
|
|
||||||
import RestaurantSidebar from "./RestaurantSidebar"
|
|
||||||
import WellnessSidebar from "./WellnessSidebar"
|
|
||||||
|
|
||||||
import type { MeetingRooms } from "@/types/components/hotelPage/meetingRooms"
|
|
||||||
import type { AdditionalData, Hotel, Restaurant } from "@/types/hotel"
|
|
||||||
|
|
||||||
interface HotelSubpageSidebarProps {
|
|
||||||
subpage: string
|
|
||||||
hotel: Hotel
|
|
||||||
additionalData: AdditionalData
|
|
||||||
restaurants: Restaurant[]
|
|
||||||
meetingRooms: MeetingRooms | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HotelSubpageSidebar({
|
|
||||||
subpage,
|
|
||||||
hotel,
|
|
||||||
additionalData,
|
|
||||||
restaurants,
|
|
||||||
meetingRooms,
|
|
||||||
}: HotelSubpageSidebarProps) {
|
|
||||||
const restaurantSubPage = restaurants.find(
|
|
||||||
(restaurant) => restaurant.nameInUrl === subpage
|
|
||||||
)
|
|
||||||
|
|
||||||
if (restaurantSubPage) {
|
|
||||||
return <RestaurantSidebar hotel={hotel} restaurant={restaurantSubPage} />
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (subpage) {
|
|
||||||
case additionalData.hotelParking.nameInUrl:
|
|
||||||
return <ParkingSidebar hotel={hotel} />
|
|
||||||
case additionalData.healthAndFitness.nameInUrl:
|
|
||||||
return <WellnessSidebar hotel={hotel} />
|
|
||||||
case additionalData.hotelSpecialNeeds.nameInUrl:
|
|
||||||
return null
|
|
||||||
case additionalData.meetingRooms.nameInUrl:
|
|
||||||
if (!meetingRooms) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<MeetingsSidebar
|
|
||||||
phoneNumber={meetingRooms[0].attributes.phoneNumber}
|
|
||||||
email={meetingRooms[0].attributes.email}
|
|
||||||
country={hotel.address.country}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,17 @@
|
|||||||
.sidebar {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Spacing-x3);
|
|
||||||
grid-column: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x-one-and-half);
|
gap: var(--Space-x15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuList {
|
.menuList {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x-half);
|
gap: var(--Space-x05);
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonContainer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contactDetails {
|
.contactDetails {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
justify-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
@@ -31,14 +22,8 @@
|
|||||||
color: var(--Text-Default);
|
color: var(--Text-Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1367px) {
|
@media screen and (max-width: 1366px) {
|
||||||
.sidebar {
|
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 1 / span 2;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonContainer {
|
.buttonContainer {
|
||||||
display: block;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
|
import HeroHeader from "../HeroHeader"
|
||||||
|
import HtmlContent from "../HtmlContent"
|
||||||
|
import WellnessSidebar from "../Sidebar/WellnessSidebar"
|
||||||
|
|
||||||
|
import styles from "./wellnessSubpage.module.css"
|
||||||
|
|
||||||
|
import type { AdditionalData, Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
|
interface WellnessSubpageProps {
|
||||||
|
hotel: Hotel
|
||||||
|
healthAndFitness: AdditionalData["healthAndFitness"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function WellnessSubpage({
|
||||||
|
hotel,
|
||||||
|
healthAndFitness,
|
||||||
|
}: WellnessSubpageProps) {
|
||||||
|
const intl = await getIntl()
|
||||||
|
const heading = intl.formatMessage({
|
||||||
|
defaultMessage: "Gym & Wellness",
|
||||||
|
})
|
||||||
|
const { mainBody, elevatorPitch } = healthAndFitness
|
||||||
|
const heroImage = hotel.healthFacilities.find(
|
||||||
|
(fac) => fac.content.images.length
|
||||||
|
)?.content.images[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className={styles.wellnessSubpage}>
|
||||||
|
<HeroHeader breadcrumbsTitle={heading} heroImage={heroImage} />
|
||||||
|
|
||||||
|
<div className={styles.contentContainer}>
|
||||||
|
<Typography variant="Title/md">
|
||||||
|
<h1 className={styles.heading}>{heading}</h1>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<aside className={styles.sidebar}>
|
||||||
|
<WellnessSidebar
|
||||||
|
address={hotel.address}
|
||||||
|
contactInformation={hotel.contactInformation}
|
||||||
|
healthFacilities={hotel.healthFacilities}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
<Divider color="baseSurfaceSubtleHover" className={styles.divider} />
|
||||||
|
<main className={styles.mainContent}>
|
||||||
|
{elevatorPitch ? (
|
||||||
|
<div className={styles.intro}>
|
||||||
|
<Typography variant="Body/Lead text">
|
||||||
|
<p>{elevatorPitch}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{mainBody ? <HtmlContent html={mainBody} /> : null}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
.wellnessSubpage {
|
||||||
|
padding-bottom: var(--Space-x8);
|
||||||
|
color: var(--Text-Default);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
width: var(--max-width-content);
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: var(--Text-Heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.contentContainer {
|
||||||
|
grid-template-columns: var(--max-width-text-block) 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
row-gap: var(--Space-x2);
|
||||||
|
column-gap: var(--Space-x8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
.hotelSubpage:not(.hasStickyButton) {
|
|
||||||
padding-bottom: var(--Spacing-x9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: grid;
|
|
||||||
background-color: var(--Base-Surface-Subtle-Normal);
|
|
||||||
padding-bottom: var(--Spacing-x4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.heroWrapper {
|
|
||||||
width: 100%;
|
|
||||||
max-width: var(--max-width-content);
|
|
||||||
margin: 0 auto;
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Spacing-x4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentContainer {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Spacing-x3);
|
|
||||||
align-items: start;
|
|
||||||
width: 100%;
|
|
||||||
max-width: var(--max-width-content);
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: var(--Spacing-x4) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainContent {
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
gap: var(--Spacing-x4);
|
|
||||||
grid-column: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meetingsContent {
|
|
||||||
grid-column: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonContainer {
|
|
||||||
position: sticky;
|
|
||||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
|
||||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
|
||||||
border-top: 1px solid var(--Base-Border-Subtle);
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading {
|
|
||||||
color: var(--Text-Heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
color: var(--Text-Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1367px) {
|
|
||||||
.hotelSubpage {
|
|
||||||
padding-bottom: var(--Spacing-x9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentContainer {
|
|
||||||
grid-template-columns: var(--max-width-text-block) 1fr;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
row-gap: var(--Spacing-x2);
|
|
||||||
column-gap: var(--Spacing-x9);
|
|
||||||
padding: var(--Spacing-x4) 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainContent {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
gap: var(--Spacing-x3);
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meetingsContent {
|
|
||||||
grid-column: 1 / span 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonContainer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,16 @@
|
|||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { Suspense } from "react"
|
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import {
|
|
||||||
getHotel,
|
|
||||||
getHotelPage,
|
|
||||||
getMeetingRooms,
|
|
||||||
} from "@/lib/trpc/memoizedRequests"
|
|
||||||
|
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs"
|
|
||||||
import Hero from "@/components/Hero"
|
|
||||||
import { meetingPackageDestinationByHotelId } from "@/components/MeetingPackageWidget/utils"
|
|
||||||
import StickyMeetingPackageWidget from "@/components/StickyMeetingPackageWidget"
|
|
||||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
|
||||||
import { getIntl } from "@/i18n"
|
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
import { safeTry } from "@/utils/safeTry"
|
|
||||||
|
|
||||||
import MeetingsAdditionalContent from "./AdditionalContent/Meetings"
|
import AccessibilitySubpage from "./AccessibilitySubpage"
|
||||||
import HotelSubpageAdditionalContent from "./AdditionalContent"
|
import MeetingsSubpage from "./MeetingsSubpage"
|
||||||
import HtmlContent from "./HtmlContent"
|
import ParkingSubpage from "./ParkingSubpage"
|
||||||
import HotelSubpageSidebar from "./Sidebar"
|
import RestaurantSubpage from "./RestaurantSubpage"
|
||||||
import { getSubpageData, verifySubpageShouldExist } from "./utils"
|
import ReviewsSubpage from "./ReviewsSubpage"
|
||||||
|
import { verifySubpageShouldExist } from "./utils"
|
||||||
import styles from "./hotelSubpage.module.css"
|
import WellnessSubpage from "./WellnessSubpage"
|
||||||
|
|
||||||
import type { HotelSubpageProps } from "@/types/components/hotelPage/subpage"
|
import type { HotelSubpageProps } from "@/types/components/hotelPage/subpage"
|
||||||
|
|
||||||
@@ -35,8 +19,7 @@ export default async function HotelSubpage({
|
|||||||
subpage,
|
subpage,
|
||||||
}: HotelSubpageProps) {
|
}: HotelSubpageProps) {
|
||||||
const lang = getLang()
|
const lang = getLang()
|
||||||
const [intl, hotelPageData, hotelData] = await Promise.all([
|
const [hotelPageData, hotelData] = await Promise.all([
|
||||||
getIntl(),
|
|
||||||
getHotelPage(),
|
getHotelPage(),
|
||||||
getHotel({ hotelId, language: lang, isCardOnlyPayment: false }),
|
getHotel({ hotelId, language: lang, isCardOnlyPayment: false }),
|
||||||
])
|
])
|
||||||
@@ -49,99 +32,48 @@ export default async function HotelSubpage({
|
|||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageData = getSubpageData(intl, subpage, hotelData)
|
const { hotel, additionalData, restaurants } = hotelData
|
||||||
if (!pageData) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hotel, restaurants, additionalData } = hotelData
|
const currentRestaurant = restaurants.find(
|
||||||
|
|
||||||
const [meetingRooms] =
|
|
||||||
hotelData.additionalData.meetingRooms.nameInUrl === subpage
|
|
||||||
? await safeTry(getMeetingRooms({ hotelId, language: lang }))
|
|
||||||
: []
|
|
||||||
|
|
||||||
const restaurantButton = restaurants.find(
|
|
||||||
(restaurant) => restaurant.nameInUrl === subpage
|
(restaurant) => restaurant.nameInUrl === subpage
|
||||||
)
|
)
|
||||||
const meetingPackageDestination = hotelId
|
if (currentRestaurant) {
|
||||||
? meetingPackageDestinationByHotelId[hotelId]
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<RestaurantSubpage
|
||||||
<section
|
restaurant={currentRestaurant}
|
||||||
className={`${styles.hotelSubpage} ${restaurantButton?.bookTableUrl ? styles.hasStickyButton : ""} `}
|
hotelAddress={hotel.address}
|
||||||
>
|
|
||||||
{meetingRooms && (
|
|
||||||
<StickyMeetingPackageWidget destination={meetingPackageDestination} />
|
|
||||||
)}
|
|
||||||
<div className={styles.header}>
|
|
||||||
<Suspense fallback={<BreadcrumbsSkeleton size="contentWidth" />}>
|
|
||||||
<Breadcrumbs subpageTitle={pageData.heading} size="contentWidth" />
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
{pageData.heroImage ? (
|
|
||||||
<div className={styles.heroWrapper}>
|
|
||||||
{pageData.heroImage && (
|
|
||||||
<Hero
|
|
||||||
src={pageData.heroImage.src}
|
|
||||||
alt={pageData.heroImage.alt}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.contentContainer}>
|
|
||||||
<Typography variant="Title/md" className={styles.heading}>
|
|
||||||
<h1>{pageData.heading}</h1>
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<HotelSubpageSidebar
|
|
||||||
subpage={subpage}
|
|
||||||
hotel={hotel}
|
|
||||||
additionalData={additionalData}
|
|
||||||
restaurants={restaurants}
|
|
||||||
meetingRooms={meetingRooms}
|
|
||||||
/>
|
|
||||||
<Divider color="baseSurfaceSubtleHover" className={styles.divider} />
|
|
||||||
<main className={styles.mainContent}>
|
|
||||||
{pageData.elevatorPitch && (
|
|
||||||
<div className={styles.intro}>
|
|
||||||
<Typography variant="Body/Lead text" className={styles.text}>
|
|
||||||
<p>{pageData.elevatorPitch}</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{pageData.mainBody && <HtmlContent html={pageData.mainBody} />}
|
|
||||||
|
|
||||||
<HotelSubpageAdditionalContent
|
|
||||||
subpage={subpage}
|
|
||||||
hotel={hotel}
|
|
||||||
additionalData={additionalData}
|
|
||||||
/>
|
|
||||||
</main>
|
|
||||||
{meetingRooms && (
|
|
||||||
<div className={styles.meetingsContent}>
|
|
||||||
<MeetingsAdditionalContent rooms={meetingRooms} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{restaurantButton?.bookTableUrl && (
|
|
||||||
<div className={styles.buttonContainer}>
|
|
||||||
<Button intent="primary" theme="base" asChild>
|
|
||||||
<a href={restaurantButton.bookTableUrl}>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Book a table",
|
|
||||||
})}
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
{/* Tracking */}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (subpage) {
|
||||||
|
case additionalData.meetingRooms.nameInUrl:
|
||||||
|
return (
|
||||||
|
<MeetingsSubpage
|
||||||
|
hotelId={hotelId}
|
||||||
|
hotel={hotel}
|
||||||
|
additionalData={additionalData}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case additionalData.healthAndFitness.nameInUrl:
|
||||||
|
return (
|
||||||
|
<WellnessSubpage
|
||||||
|
hotel={hotel}
|
||||||
|
healthAndFitness={additionalData.healthAndFitness}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case additionalData.hotelParking.nameInUrl:
|
||||||
|
return <ParkingSubpage hotel={hotel} additionalData={additionalData} />
|
||||||
|
case additionalData.hotelSpecialNeeds.nameInUrl:
|
||||||
|
return (
|
||||||
|
<AccessibilitySubpage
|
||||||
|
hotelName={hotel.name}
|
||||||
|
additionalData={additionalData}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case "reviews":
|
||||||
|
return <ReviewsSubpage hotel={hotel} />
|
||||||
|
default:
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +1,5 @@
|
|||||||
import type { IntlShape } from "react-intl"
|
|
||||||
|
|
||||||
import type { HotelData } from "@/types/hotel"
|
import type { HotelData } from "@/types/hotel"
|
||||||
|
|
||||||
export function getSubpageData(
|
|
||||||
intl: IntlShape,
|
|
||||||
subpage: string,
|
|
||||||
hotelData: HotelData
|
|
||||||
) {
|
|
||||||
const additionalData = hotelData.additionalData
|
|
||||||
const hotel = hotelData.hotel
|
|
||||||
const restaurants = hotelData.restaurants
|
|
||||||
const restaurantSubPage = restaurants.find(
|
|
||||||
(restaurant) => restaurant.nameInUrl === subpage
|
|
||||||
)
|
|
||||||
|
|
||||||
if (restaurantSubPage) {
|
|
||||||
const restaurantImage = restaurantSubPage.content.images[0]
|
|
||||||
|
|
||||||
return {
|
|
||||||
mainBody: restaurantSubPage.mainBody,
|
|
||||||
elevatorPitch: restaurantSubPage.elevatorPitch,
|
|
||||||
heading: restaurantSubPage.name,
|
|
||||||
heroImage: restaurantImage
|
|
||||||
? {
|
|
||||||
src: restaurantSubPage.content.images[0].imageSizes.medium,
|
|
||||||
alt: restaurantSubPage.content.images[0].metaData.altText || "",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (subpage) {
|
|
||||||
case additionalData.hotelParking.nameInUrl:
|
|
||||||
const parkingImage = additionalData.parkingImages?.heroImages[0]
|
|
||||||
return {
|
|
||||||
...additionalData.hotelParking,
|
|
||||||
heading: intl.formatMessage({
|
|
||||||
defaultMessage: "Parking",
|
|
||||||
}),
|
|
||||||
heroImage: parkingImage
|
|
||||||
? {
|
|
||||||
src: parkingImage.imageSizes.medium,
|
|
||||||
alt: parkingImage.metaData.altText || "",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}
|
|
||||||
case additionalData.healthAndFitness.nameInUrl:
|
|
||||||
const wellnessImage = hotel.healthFacilities.find(
|
|
||||||
(fac) => fac.content.images.length
|
|
||||||
)?.content.images[0]
|
|
||||||
return {
|
|
||||||
...additionalData.healthAndFitness,
|
|
||||||
heading: intl.formatMessage({
|
|
||||||
defaultMessage: "Gym & Wellness",
|
|
||||||
}),
|
|
||||||
heroImage: wellnessImage
|
|
||||||
? {
|
|
||||||
src: wellnessImage.imageSizes.medium,
|
|
||||||
alt: wellnessImage.metaData.altText || "",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}
|
|
||||||
case additionalData.hotelSpecialNeeds.nameInUrl:
|
|
||||||
const accessibilityImage = additionalData.accessibility?.heroImages[0]
|
|
||||||
return {
|
|
||||||
...additionalData.hotelSpecialNeeds,
|
|
||||||
heading: intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "Accessibility at {hotel}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hotel: hotel.name,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
heroImage: accessibilityImage
|
|
||||||
? {
|
|
||||||
src: accessibilityImage.imageSizes.medium,
|
|
||||||
alt: accessibilityImage.metaData.altText || "",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}
|
|
||||||
case additionalData.meetingRooms.nameInUrl:
|
|
||||||
const meetingImage = additionalData.conferencesAndMeetings?.heroImages[0]
|
|
||||||
return {
|
|
||||||
elevatorPitch: hotel.hotelContent.texts.meetingDescription?.medium,
|
|
||||||
mainBody: additionalData.meetingRooms.mainBody,
|
|
||||||
heading: intl.formatMessage({
|
|
||||||
defaultMessage: "Meetings, Conferences & Events",
|
|
||||||
}),
|
|
||||||
heroImage: meetingImage
|
|
||||||
? {
|
|
||||||
src: meetingImage.imageSizes.medium,
|
|
||||||
alt: meetingImage.metaData.altText || "",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifySubpageShouldExist(
|
export function verifySubpageShouldExist(
|
||||||
hotelData: HotelData,
|
hotelData: HotelData,
|
||||||
subpage: string
|
subpage: string
|
||||||
@@ -129,6 +29,12 @@ export function verifySubpageShouldExist(
|
|||||||
if (additionalData.displayWebPage.meetingRoom) {
|
if (additionalData.displayWebPage.meetingRoom) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
case "reviews":
|
||||||
|
if (
|
||||||
|
hotelData.hotel.ratings?.tripAdvisor.reviews.widgetScriptEmbedUrlIframe
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ const nextConfig = {
|
|||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "*.scandichotels.com",
|
hostname: "*.scandichotels.com",
|
||||||
},
|
},
|
||||||
|
// Tripadvisor CDN for award images
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "static.tacdn.com",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user