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 TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getSingleDecimal } from "@/utils/numberFormatting"
|
||||
|
||||
import TripAdvisorLink from "./TripAdvisorLink"
|
||||
|
||||
import styles from "./introSection.module.css"
|
||||
|
||||
import { SidepeekSlugs } from "@/types/components/hotelPage/hotelPage"
|
||||
@@ -29,17 +30,6 @@ export default async function IntroSection({
|
||||
)
|
||||
|
||||
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 (
|
||||
<section className={styles.introSection}>
|
||||
@@ -59,12 +49,8 @@ export default async function IntroSection({
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.bodyText}>{formattedLocationText}</p>
|
||||
</Typography>
|
||||
{formattedTripAdvisorText && (
|
||||
<span className={styles.tripAdvisorText}>
|
||||
<TripadvisorIcon color="Icon/Interactive/Secondary" />
|
||||
{formattedTripAdvisorText}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{tripAdvisor ? <TripAdvisorLink tripAdvisor={tripAdvisor} /> : null}
|
||||
</div>
|
||||
<div className={styles.subtitleContent}>
|
||||
<Typography variant="Body/Lead text">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
.mainContent {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.subtitleContent {
|
||||
@@ -15,18 +16,6 @@
|
||||
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 {
|
||||
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,
|
||||
.ol {
|
||||
padding: var(--Spacing-x2) var(--Spacing-x0);
|
||||
padding: var(--Space-x2) var(--Space-x0);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
margin-left: var(--Spacing-x2);
|
||||
gap: var(--Space-x1);
|
||||
margin-left: var(--Space-x2);
|
||||
}
|
||||
|
||||
.ol > li::marker {
|
||||
@@ -11,7 +11,7 @@
|
||||
}
|
||||
|
||||
.li {
|
||||
margin-left: var(--Spacing-x3);
|
||||
margin-left: var(--Space-x3);
|
||||
}
|
||||
|
||||
.li > p {
|
||||
|
||||
@@ -5,17 +5,15 @@ import Grids from "@/components/TempDesignSystem/Grids"
|
||||
import MeetingRoomCard from "@/components/TempDesignSystem/MeetingRoomCard"
|
||||
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"
|
||||
|
||||
interface MeetingsAdditionalContentProps {
|
||||
interface MeetingRoomsProps {
|
||||
rooms: MeetingRooms
|
||||
}
|
||||
|
||||
export default function MeetingsAdditionalContent({
|
||||
rooms,
|
||||
}: MeetingsAdditionalContentProps) {
|
||||
export default function MeetingRooms({ rooms }: MeetingRoomsProps) {
|
||||
const showToggleButton = rooms.length > 3
|
||||
const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton)
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
|
||||
.section {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x4);
|
||||
gap: var(--Space-x4);
|
||||
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()
|
||||
|
||||
return (
|
||||
<aside className={styles.sidebar}>
|
||||
<>
|
||||
<div className={styles.content}>
|
||||
<Typography variant="Title/xs" className={styles.heading}>
|
||||
<h3>
|
||||
@@ -51,6 +51,6 @@ export default async function MeetingsSidebar({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,14 +9,18 @@ import { Country } from "@/types/enums/country"
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
|
||||
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()
|
||||
|
||||
return (
|
||||
<aside className={styles.sidebar}>
|
||||
<>
|
||||
<div className={styles.content}>
|
||||
<Typography variant="Title/xs" className={styles.heading}>
|
||||
<h3>
|
||||
@@ -28,11 +32,11 @@ export default async function ParkingSidebar({ hotel }: HotelSidebarProps) {
|
||||
|
||||
<Typography variant="Body/Paragraph/mdRegular" className={styles.text}>
|
||||
<div>
|
||||
<p>{hotel.address.streetAddress}</p>
|
||||
<p>{address.streetAddress}</p>
|
||||
<p>
|
||||
{hotel.address.zipCode} {hotel.address.city}
|
||||
{address.zipCode} {address.city}
|
||||
</p>
|
||||
<p> {hotel.address.country}</p>
|
||||
<p> {address.country}</p>
|
||||
</div>
|
||||
</Typography>
|
||||
</div>
|
||||
@@ -46,10 +50,10 @@ export default async function ParkingSidebar({ hotel }: HotelSidebarProps) {
|
||||
</h3>
|
||||
</Typography>
|
||||
<div className={styles.contactDetails}>
|
||||
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
|
||||
{hotel.contactInformation.phoneNumber}
|
||||
<Link href={`tel:${contactInformation.phoneNumber}`}>
|
||||
{contactInformation.phoneNumber}
|
||||
</Link>
|
||||
{hotel.address.country === Country.Finland ? (
|
||||
{address.country === Country.Finland ? (
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdRegular"
|
||||
className={styles.text}
|
||||
@@ -63,12 +67,12 @@ export default async function ParkingSidebar({ hotel }: HotelSidebarProps) {
|
||||
) : null}
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
href={`mailto:${hotel.contactInformation.email}`}
|
||||
href={`mailto:${contactInformation.email}`}
|
||||
>
|
||||
{hotel.contactInformation.email}
|
||||
{contactInformation.email}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,21 +12,20 @@ import { Country } from "@/types/enums/country"
|
||||
import type { Hotel, Restaurant } from "@/types/hotel"
|
||||
|
||||
interface RestaurantSidebarProps {
|
||||
hotel: Hotel
|
||||
hotelAddress: Hotel["address"]
|
||||
restaurant: Restaurant
|
||||
}
|
||||
|
||||
export default async function RestaurantSidebar({
|
||||
hotel,
|
||||
hotelAddress,
|
||||
restaurant,
|
||||
}: RestaurantSidebarProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
const { address } = hotel
|
||||
const { openingDetails, phoneNumber, email, bookTableUrl } = restaurant
|
||||
|
||||
return (
|
||||
<aside className={styles.sidebar}>
|
||||
<>
|
||||
{openingDetails.length ? (
|
||||
<div className={styles.content}>
|
||||
<Typography variant="Title/xs" className={styles.heading}>
|
||||
@@ -97,9 +96,9 @@ export default async function RestaurantSidebar({
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular" className={styles.text}>
|
||||
<div>
|
||||
<p>{address.streetAddress}</p>
|
||||
<p>{hotelAddress.streetAddress}</p>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<p>{`${address.zipCode} ${address.city}`}</p>
|
||||
<p>{`${hotelAddress.zipCode} ${hotelAddress.city}`}</p>
|
||||
</div>
|
||||
</Typography>
|
||||
</div>
|
||||
@@ -116,7 +115,7 @@ export default async function RestaurantSidebar({
|
||||
{phoneNumber && (
|
||||
<>
|
||||
<Link href={`tel:${phoneNumber}`}>{phoneNumber}</Link>
|
||||
{address.country === Country.Finland ? (
|
||||
{hotelAddress.country === Country.Finland ? (
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdRegular"
|
||||
className={styles.text}
|
||||
@@ -138,6 +137,6 @@ export default async function RestaurantSidebar({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,17 +8,23 @@ import { translateWellnessType } from "../../HotelPage/utils"
|
||||
import styles from "./sidebar.module.css"
|
||||
|
||||
import { Country } from "@/types/enums/country"
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { HealthFacility, Hotel } from "@/types/hotel"
|
||||
|
||||
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()
|
||||
|
||||
return (
|
||||
<aside className={styles.sidebar}>
|
||||
<>
|
||||
<div className={styles.content}>
|
||||
<Typography variant="Title/xs" className={styles.heading}>
|
||||
<h3>
|
||||
@@ -27,7 +33,7 @@ export default async function WellnessSidebar({ hotel }: WellnessSidebarProps) {
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
{hotel.healthFacilities.map((facility) => (
|
||||
{healthFacilities.map((facility) => (
|
||||
<div key={facility.type}>
|
||||
<Typography variant="Title/Subtitle/md" className={styles.text}>
|
||||
<h4>{translateWellnessType(facility.type, intl)}</h4>
|
||||
@@ -97,11 +103,11 @@ export default async function WellnessSidebar({ hotel }: WellnessSidebarProps) {
|
||||
className={styles.text}
|
||||
>
|
||||
<div>
|
||||
<p>{hotel.address.streetAddress}</p>
|
||||
<p>{address.streetAddress}</p>
|
||||
<p>
|
||||
{hotel.address.zipCode} {hotel.address.city}
|
||||
{address.zipCode} {address.city}
|
||||
</p>
|
||||
<p> {hotel.address.country}</p>
|
||||
<p> {address.country}</p>
|
||||
</div>
|
||||
</Typography>
|
||||
</div>
|
||||
@@ -115,22 +121,24 @@ export default async function WellnessSidebar({ hotel }: WellnessSidebarProps) {
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
|
||||
{hotel.contactInformation.phoneNumber}
|
||||
</Link>
|
||||
{hotel.address.country === Country.Finland ? (
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdRegular"
|
||||
className={styles.text}
|
||||
>
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Price 0,16 €/min + local call charges",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
<div className={styles.contactDetails}>
|
||||
<Link href={`tel:${contactInformation.phoneNumber}`}>
|
||||
{contactInformation.phoneNumber}
|
||||
</Link>
|
||||
{address.country === Country.Finland ? (
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdRegular"
|
||||
className={styles.text}
|
||||
>
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Price 0,16 €/min + local call charges",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
gap: var(--Space-x15);
|
||||
}
|
||||
|
||||
.menuList {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-half);
|
||||
gap: var(--Space-x05);
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.contactDetails {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.heading {
|
||||
@@ -31,14 +22,8 @@
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
@media (min-width: 1367px) {
|
||||
.sidebar {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / span 2;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1366px) {
|
||||
.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 { 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 { safeTry } from "@/utils/safeTry"
|
||||
|
||||
import MeetingsAdditionalContent from "./AdditionalContent/Meetings"
|
||||
import HotelSubpageAdditionalContent from "./AdditionalContent"
|
||||
import HtmlContent from "./HtmlContent"
|
||||
import HotelSubpageSidebar from "./Sidebar"
|
||||
import { getSubpageData, verifySubpageShouldExist } from "./utils"
|
||||
|
||||
import styles from "./hotelSubpage.module.css"
|
||||
import AccessibilitySubpage from "./AccessibilitySubpage"
|
||||
import MeetingsSubpage from "./MeetingsSubpage"
|
||||
import ParkingSubpage from "./ParkingSubpage"
|
||||
import RestaurantSubpage from "./RestaurantSubpage"
|
||||
import ReviewsSubpage from "./ReviewsSubpage"
|
||||
import { verifySubpageShouldExist } from "./utils"
|
||||
import WellnessSubpage from "./WellnessSubpage"
|
||||
|
||||
import type { HotelSubpageProps } from "@/types/components/hotelPage/subpage"
|
||||
|
||||
@@ -35,8 +19,7 @@ export default async function HotelSubpage({
|
||||
subpage,
|
||||
}: HotelSubpageProps) {
|
||||
const lang = getLang()
|
||||
const [intl, hotelPageData, hotelData] = await Promise.all([
|
||||
getIntl(),
|
||||
const [hotelPageData, hotelData] = await Promise.all([
|
||||
getHotelPage(),
|
||||
getHotel({ hotelId, language: lang, isCardOnlyPayment: false }),
|
||||
])
|
||||
@@ -49,99 +32,48 @@ export default async function HotelSubpage({
|
||||
notFound()
|
||||
}
|
||||
|
||||
const pageData = getSubpageData(intl, subpage, hotelData)
|
||||
if (!pageData) {
|
||||
notFound()
|
||||
}
|
||||
const { hotel, additionalData, restaurants } = hotelData
|
||||
|
||||
const { hotel, restaurants, additionalData } = hotelData
|
||||
|
||||
const [meetingRooms] =
|
||||
hotelData.additionalData.meetingRooms.nameInUrl === subpage
|
||||
? await safeTry(getMeetingRooms({ hotelId, language: lang }))
|
||||
: []
|
||||
|
||||
const restaurantButton = restaurants.find(
|
||||
const currentRestaurant = restaurants.find(
|
||||
(restaurant) => restaurant.nameInUrl === subpage
|
||||
)
|
||||
const meetingPackageDestination = hotelId
|
||||
? meetingPackageDestinationByHotelId[hotelId]
|
||||
: undefined
|
||||
if (currentRestaurant) {
|
||||
return (
|
||||
<RestaurantSubpage
|
||||
restaurant={currentRestaurant}
|
||||
hotelAddress={hotel.address}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
className={`${styles.hotelSubpage} ${restaurantButton?.bookTableUrl ? styles.hasStickyButton : ""} `}
|
||||
>
|
||||
{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"
|
||||
|
||||
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(
|
||||
hotelData: HotelData,
|
||||
subpage: string
|
||||
@@ -129,6 +29,12 @@ export function verifySubpageShouldExist(
|
||||
if (additionalData.displayWebPage.meetingRoom) {
|
||||
return true
|
||||
}
|
||||
case "reviews":
|
||||
if (
|
||||
hotelData.hotel.ratings?.tripAdvisor.reviews.widgetScriptEmbedUrlIframe
|
||||
) {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -60,6 +60,11 @@ const nextConfig = {
|
||||
protocol: "https",
|
||||
hostname: "*.scandichotels.com",
|
||||
},
|
||||
// Tripadvisor CDN for award images
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "static.tacdn.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user