feat(BOOK-62): Added new InfoCard component and using that on hotel pages
Approved-by: Bianca Widstam
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
import { cx } from "class-variance-authority"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import {
|
||||
DEFAULT_THEME,
|
||||
getThemeByHotel,
|
||||
} from "@scandic-hotels/common/utils/theme"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import HotelMapPage from "@/components/ContentType/HotelMapPage"
|
||||
import HotelPage from "@/components/ContentType/HotelPage"
|
||||
import HotelSubpage from "@/components/ContentType/HotelSubpage"
|
||||
import { getThemeByHotel } from "@/utils/theme"
|
||||
import { DEFAULT_THEME } from "@/utils/theme/types"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import Card from "@/components/TempDesignSystem/Card"
|
||||
|
||||
import CardImage from "./CardImage"
|
||||
|
||||
import styles from "./cardGrid.module.css"
|
||||
|
||||
import type { ActivityCard } from "@scandic-hotels/trpc/types/hotelPage"
|
||||
|
||||
import type { CardProps } from "@/components/TempDesignSystem/Card/card"
|
||||
|
||||
export default function ActivitiesCardGrid(activitiesCard: ActivityCard) {
|
||||
const hasImage = activitiesCard.backgroundImage
|
||||
|
||||
const updatedCard: CardProps = {
|
||||
...activitiesCard,
|
||||
theme: hasImage ? "image" : "primaryDark",
|
||||
primaryButton: hasImage
|
||||
? {
|
||||
href: `?s=${activitiesCard.sidepeekSlug}`,
|
||||
title: activitiesCard.ctaText,
|
||||
isExternal: false,
|
||||
scrollOnClick: false,
|
||||
}
|
||||
: undefined,
|
||||
secondaryButton: hasImage
|
||||
? undefined
|
||||
: {
|
||||
href: `?s=${activitiesCard.sidepeekSlug}`,
|
||||
title: activitiesCard.ctaText,
|
||||
isExternal: false,
|
||||
scrollOnClick: false,
|
||||
},
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.desktopCards}>
|
||||
<Card {...updatedCard} className={styles.spanThree} />
|
||||
</div>
|
||||
<div className={styles.mobileCards}>
|
||||
<CardImage card={updatedCard} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
.cardImage {
|
||||
display: grid;
|
||||
gap: var(--Space-x025);
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
position: relative;
|
||||
height: 180px; /* Fixed height from Figma */
|
||||
overflow: hidden;
|
||||
border-radius: var(--Corner-radius-md);
|
||||
}
|
||||
|
||||
.image {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--Space-x025);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import Image from "@scandic-hotels/design-system/Image"
|
||||
|
||||
import Card from "@/components/TempDesignSystem/Card"
|
||||
|
||||
import styles from "./cardImage.module.css"
|
||||
|
||||
import type { CardImageProps } from "@/types/components/cardImage"
|
||||
|
||||
export default function CardImage({
|
||||
card,
|
||||
imageCards,
|
||||
className,
|
||||
}: CardImageProps) {
|
||||
return (
|
||||
<div className={`${styles.cardImage} ${className}`}>
|
||||
<div className={styles.imageContainer}>
|
||||
{imageCards?.map(
|
||||
({ backgroundImage }) =>
|
||||
backgroundImage && (
|
||||
<div key={backgroundImage.id} className={styles.imageWrapper}>
|
||||
<Image
|
||||
src={backgroundImage.url}
|
||||
className={styles.image}
|
||||
alt={backgroundImage.title}
|
||||
fill
|
||||
sizes="(min-width: 768px) 900px, 100vw"
|
||||
focalPoint={backgroundImage.focalPoint}
|
||||
dimensions={backgroundImage.dimensions}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<Card {...card} height="dynamic" className={styles.card} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
.cardContainer {
|
||||
scroll-margin-top: var(--hotel-page-scroll-margin-top);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.desktopCards {
|
||||
display: none;
|
||||
}
|
||||
.mobileCards {
|
||||
display: grid;
|
||||
gap: var(--Space-x025);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.mobileCards {
|
||||
display: none;
|
||||
}
|
||||
.desktopCards {
|
||||
display: grid;
|
||||
gap: var(--Space-x1);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
height: 320px;
|
||||
}
|
||||
.imageWrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: var(--Corner-radius-md);
|
||||
}
|
||||
.image {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.spanOne {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.spanTwo {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.spanThree {
|
||||
grid-column: span 3;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import Image from "@scandic-hotels/design-system/Image"
|
||||
|
||||
import Card from "@/components/TempDesignSystem/Card"
|
||||
import {
|
||||
filterFacilityCards,
|
||||
isFacilityCard,
|
||||
isFacilityImage,
|
||||
} from "@/utils/facilityCards"
|
||||
|
||||
import CardImage from "./CardImage"
|
||||
|
||||
import styles from "./cardGrid.module.css"
|
||||
|
||||
import type {
|
||||
CardGridProps,
|
||||
FacilityCardType,
|
||||
} from "@/types/components/hotelPage/facilities"
|
||||
|
||||
export default function FacilitiesCardGrid({
|
||||
facilitiesCardGrid,
|
||||
}: CardGridProps) {
|
||||
const imageCard = filterFacilityCards(facilitiesCardGrid)
|
||||
const nrCards = facilitiesCardGrid.length
|
||||
|
||||
function getCardClassName(card: FacilityCardType): string {
|
||||
if (nrCards === 1) {
|
||||
return styles.spanThree
|
||||
} else if (nrCards === 2 && !isFacilityCard(card)) {
|
||||
return styles.spanTwo
|
||||
}
|
||||
return styles.spanOne
|
||||
}
|
||||
|
||||
return (
|
||||
<div id={imageCard.card.id} className={styles.cardContainer}>
|
||||
<div className={styles.desktopCards}>
|
||||
{facilitiesCardGrid.map((card: FacilityCardType) =>
|
||||
isFacilityImage(card) && card.backgroundImage ? (
|
||||
<div
|
||||
key={card.id}
|
||||
className={`${styles.imageWrapper} ${getCardClassName(card)}`}
|
||||
>
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={card.backgroundImage.url}
|
||||
alt={card.backgroundImage.meta.alt || ""}
|
||||
fill
|
||||
sizes="(min-width: 1367px) 700px, 900px"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Card {...card} key={card.id} className={getCardClassName(card)} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.mobileCards}>
|
||||
<CardImage card={imageCard.card} imageCards={imageCard.images} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,78 @@
|
||||
.facilitiesSection,
|
||||
.activitiesCards {
|
||||
.activityCards {
|
||||
display: grid;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
.activitiesCards {
|
||||
scroll-margin-top: var(--hotel-page-scroll-margin-top);
|
||||
.facilityRow {
|
||||
&:has(.imageWrapper) {
|
||||
display: grid;
|
||||
gap: var(--Space-x025);
|
||||
}
|
||||
}
|
||||
|
||||
.imagesContainer {
|
||||
display: flex;
|
||||
gap: var(--Space-x025);
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: var(--Corner-radius-md);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.infoCard {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.facilitiesSection,
|
||||
.activitiesCards {
|
||||
.activityCards {
|
||||
gap: var(--Space-x7);
|
||||
}
|
||||
|
||||
.facilityRow {
|
||||
height: 320px;
|
||||
&:has(.imageWrapper) {
|
||||
display: grid;
|
||||
gap: var(--Space-x1);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
&.reverse {
|
||||
.infoCard {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.imagesContainer {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) and (max-width: 899px) {
|
||||
.facilityRow:has(.imageWrapper) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.imageWrapper:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 900px) {
|
||||
.facilityRow:has(.imageWrapper) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
.imageWrapper {
|
||||
&.spanTwo {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,109 @@
|
||||
import { logger } from "@scandic-hotels/common/logger"
|
||||
import { cx } from "class-variance-authority"
|
||||
|
||||
import Image from "@scandic-hotels/design-system/Image"
|
||||
import { InfoCard } from "@scandic-hotels/design-system/InfoCard"
|
||||
import { type FacilityData } from "@scandic-hotels/trpc/types/hotel"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
import { isFacilityCard, setFacilityCardGrids } from "@/utils/facilityCards"
|
||||
|
||||
import ActivitiesCardGrid from "./CardGrid/ActivitiesCardGrid"
|
||||
import FacilitiesCardGrid from "./CardGrid"
|
||||
import {
|
||||
mapActivityCardsToInfoCards,
|
||||
mapFacilitiesToFacilityRows,
|
||||
mapFacilityDataToFacilities,
|
||||
} from "./utils"
|
||||
|
||||
import styles from "./facilities.module.css"
|
||||
|
||||
import {
|
||||
type Facilities,
|
||||
type FacilitiesProps,
|
||||
FacilityCardButtonText,
|
||||
type FacilityCardType,
|
||||
type FacilityGrid,
|
||||
} from "@/types/components/hotelPage/facilities"
|
||||
import type { Theme } from "@scandic-hotels/common/utils/theme"
|
||||
import type { ActivityCard } from "@scandic-hotels/trpc/types/hotelPage"
|
||||
|
||||
import type { HotelPageSections } from "@/types/components/hotelPage/sections"
|
||||
import { HotelHashValues } from "@/types/enums/hotelPage"
|
||||
|
||||
export default async function Facilities({
|
||||
facilities,
|
||||
amenities,
|
||||
healthFacilities,
|
||||
activitiesCards,
|
||||
interface FacilitiesProps {
|
||||
restaurantImages: FacilityData | null
|
||||
conferencesAndMeetings: FacilityData | null
|
||||
healthAndWellness: FacilityData | null
|
||||
pageSections: HotelPageSections
|
||||
activities: ActivityCard[]
|
||||
hotelTheme: Theme
|
||||
}
|
||||
|
||||
export async function Facilities({
|
||||
restaurantImages,
|
||||
conferencesAndMeetings,
|
||||
healthAndWellness,
|
||||
pageSections,
|
||||
activities,
|
||||
hotelTheme,
|
||||
}: FacilitiesProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
const facilityCardGrids = setFacilityCardGrids(
|
||||
facilities,
|
||||
amenities,
|
||||
healthFacilities
|
||||
const facilities = mapFacilityDataToFacilities(
|
||||
restaurantImages,
|
||||
conferencesAndMeetings,
|
||||
healthAndWellness,
|
||||
pageSections
|
||||
)
|
||||
const facilityRows = mapFacilitiesToFacilityRows(intl, facilities)
|
||||
const activityCards = mapActivityCardsToInfoCards(activities)
|
||||
|
||||
const translatedFacilityGrids: Facilities = facilityCardGrids.map(
|
||||
(cardGrid: FacilityGrid) => {
|
||||
return cardGrid.map((card: FacilityCardType) => {
|
||||
if (isFacilityCard(card)) {
|
||||
return {
|
||||
...card,
|
||||
secondaryButton: {
|
||||
...card.secondaryButton,
|
||||
title: translateButtonText(card.secondaryButton.title),
|
||||
},
|
||||
}
|
||||
}
|
||||
return card
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
function translateButtonText(text: string) {
|
||||
switch (text) {
|
||||
case FacilityCardButtonText.MEETINGS:
|
||||
case FacilityCardButtonText.RESTAURANT:
|
||||
case FacilityCardButtonText.WELLNESS:
|
||||
return intl.formatMessage({
|
||||
id: "common.readMore",
|
||||
defaultMessage: "Read more",
|
||||
})
|
||||
default:
|
||||
logger.warn(`Unsupported option given: ${text}`)
|
||||
return intl.formatMessage({
|
||||
id: "common.readMore",
|
||||
defaultMessage: "Read more",
|
||||
})
|
||||
}
|
||||
if (!facilityRows.length && !activityCards.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.facilitiesSection}>
|
||||
{translatedFacilityGrids.map((cardGrid: FacilityGrid) => (
|
||||
<FacilitiesCardGrid
|
||||
key={cardGrid[0].id}
|
||||
facilitiesCardGrid={cardGrid}
|
||||
/>
|
||||
{facilityRows.map((facility, index) => (
|
||||
<div
|
||||
id={facility.id}
|
||||
key={facility.id}
|
||||
className={cx(styles.facilityRow, {
|
||||
[styles.reverse]: index % 2 !== 0,
|
||||
})}
|
||||
>
|
||||
<InfoCard
|
||||
className={styles.infoCard}
|
||||
topTitle={facility.topTitle}
|
||||
heading={facility.heading}
|
||||
secondaryButton={{
|
||||
href: facility.sidepeekHref,
|
||||
text: intl.formatMessage({
|
||||
id: "common.readMore",
|
||||
defaultMessage: "Read more",
|
||||
}),
|
||||
}}
|
||||
theme={facility.theme}
|
||||
hotelTheme={hotelTheme}
|
||||
height="dynamic"
|
||||
/>
|
||||
{facility.images.length ? (
|
||||
<div className={styles.imagesContainer}>
|
||||
{facility.images.map((image) => (
|
||||
<div
|
||||
key={image.src}
|
||||
className={cx(styles.imageWrapper, {
|
||||
[styles.spanTwo]: facility.images.length === 1,
|
||||
})}
|
||||
>
|
||||
<Image
|
||||
key={image.src}
|
||||
className={styles.image}
|
||||
src={image.src}
|
||||
alt={image.altText ?? image.altText_En ?? ""}
|
||||
fill
|
||||
sizes="(min-width: 1367px) 700px, 900px"
|
||||
focalPoint={{ x: 50, y: 50 }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
{activitiesCards.length ? (
|
||||
<div id={HotelHashValues.activities} className={styles.activitiesCards}>
|
||||
{activitiesCards.map((card) => (
|
||||
<ActivitiesCardGrid
|
||||
key={card.upcoming_activities_card.contentPage.href}
|
||||
{...card.upcoming_activities_card}
|
||||
/>
|
||||
{activityCards.length ? (
|
||||
<div id={HotelHashValues.activities} className={styles.activityCards}>
|
||||
{activityCards.map((card) => (
|
||||
<InfoCard key={card.heading} {...card} hotelTheme={hotelTheme} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { InfoCardProps } from "@scandic-hotels/design-system/InfoCard"
|
||||
import type { ApiImage, FacilityData } from "@scandic-hotels/trpc/types/hotel"
|
||||
import type { ActivityCard } from "@scandic-hotels/trpc/types/hotelPage"
|
||||
import type { IntlShape } from "react-intl"
|
||||
|
||||
import { SidepeekSlugs } from "@/types/components/hotelPage/hotelPage"
|
||||
import type { HotelPageSections } from "@/types/components/hotelPage/sections"
|
||||
import { HotelHashValues } from "@/types/enums/hotelPage"
|
||||
|
||||
type CardTheme = NonNullable<InfoCardProps["theme"]>
|
||||
type FacilityType = "wellness" | "meetings" | "restaurant"
|
||||
type Facility = FacilityData & { type: FacilityType }
|
||||
|
||||
interface FacilityRow {
|
||||
heading: string
|
||||
images: ApiImage[]
|
||||
id: string
|
||||
topTitle: string | null
|
||||
sidepeekHref: string
|
||||
theme: CardTheme
|
||||
}
|
||||
|
||||
export function mapFacilityDataToFacilities(
|
||||
restaurantImages: FacilityData | null,
|
||||
conferencesAndMeetings: FacilityData | null,
|
||||
healthAndWellness: FacilityData | null,
|
||||
pageSections: HotelPageSections
|
||||
): Facility[] {
|
||||
const facilities: Facility[] = []
|
||||
if (pageSections.restaurant && restaurantImages) {
|
||||
facilities.push({
|
||||
...restaurantImages,
|
||||
headingText: pageSections.restaurant.heading,
|
||||
type: "restaurant",
|
||||
})
|
||||
}
|
||||
if (pageSections.meetings && conferencesAndMeetings) {
|
||||
facilities.push({
|
||||
...conferencesAndMeetings,
|
||||
headingText: pageSections.meetings.heading,
|
||||
type: "meetings",
|
||||
})
|
||||
}
|
||||
if (pageSections.wellness && healthAndWellness) {
|
||||
facilities.push({
|
||||
...healthAndWellness,
|
||||
headingText: pageSections.wellness.heading,
|
||||
type: "wellness",
|
||||
})
|
||||
}
|
||||
|
||||
return facilities
|
||||
}
|
||||
|
||||
export function mapFacilitiesToFacilityRows(
|
||||
intl: IntlShape,
|
||||
facilities: Facility[]
|
||||
): FacilityRow[] {
|
||||
const mappedFacilities = facilities.map((f) => {
|
||||
const images = f.heroImages.slice(0, 2)
|
||||
const facilityType = f.type
|
||||
const id = HotelHashValues[facilityType] ?? facilityType
|
||||
const sidepeekSlug = SidepeekSlugs[facilityType] ?? null
|
||||
|
||||
return {
|
||||
heading: f.headingText,
|
||||
images,
|
||||
id,
|
||||
topTitle: getTopTitle(intl, facilityType),
|
||||
sidepeekHref: `?s=${sidepeekSlug}`,
|
||||
theme: getTheme(facilityType),
|
||||
}
|
||||
})
|
||||
|
||||
return mappedFacilities
|
||||
}
|
||||
|
||||
function getTopTitle(intl: IntlShape, facilityType: FacilityType) {
|
||||
switch (facilityType) {
|
||||
case "wellness":
|
||||
return intl.formatMessage({
|
||||
id: "hotelPage.facilities.wellnessTopTitle",
|
||||
defaultMessage: "Here's to your health!",
|
||||
})
|
||||
case "meetings":
|
||||
return intl.formatMessage({
|
||||
id: "hotelPage.facilities.meetingsTopTitle",
|
||||
defaultMessage: "Great minds meet here",
|
||||
})
|
||||
case "restaurant":
|
||||
return intl.formatMessage({
|
||||
id: "hotelPage.facilities.restaurantTopTitle",
|
||||
defaultMessage: "Good vibes, great bites!",
|
||||
})
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getTheme(facilityType: FacilityType): CardTheme {
|
||||
switch (facilityType) {
|
||||
case "wellness":
|
||||
return "Primary 1"
|
||||
case "meetings":
|
||||
return "Accent"
|
||||
case "restaurant":
|
||||
return "Primary 3"
|
||||
default:
|
||||
return "White"
|
||||
}
|
||||
}
|
||||
|
||||
export function mapActivityCardsToInfoCards(
|
||||
activityCards: ActivityCard[]
|
||||
): InfoCardProps[] {
|
||||
return activityCards.map((card) => {
|
||||
const image = card.backgroundImage
|
||||
const mappedCard: InfoCardProps = {
|
||||
theme: !!image ? "Image" : "Primary 3",
|
||||
heading: card.heading,
|
||||
bodyText: card.bodyText,
|
||||
topTitle: card.scriptedTopTitle,
|
||||
}
|
||||
|
||||
if (image) {
|
||||
mappedCard.backgroundImage = {
|
||||
src: image.url,
|
||||
alt: image.meta.alt ?? "",
|
||||
dimensions: image.dimensions,
|
||||
focalPoint: image.focalPoint,
|
||||
}
|
||||
mappedCard.primaryButton = {
|
||||
href: `?s=${card.sidepeekSlug}`,
|
||||
text: card.ctaText,
|
||||
}
|
||||
} else {
|
||||
mappedCard.secondaryButton = {
|
||||
href: `?s=${card.sidepeekSlug}`,
|
||||
text: card.ctaText,
|
||||
}
|
||||
}
|
||||
return mappedCard
|
||||
})
|
||||
}
|
||||
@@ -67,6 +67,20 @@
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
.localCharges::before {
|
||||
content: " (";
|
||||
}
|
||||
|
||||
.localCharges::after {
|
||||
content: ")";
|
||||
}
|
||||
|
||||
.subtitleContent {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.contactInformationDivider {
|
||||
display: none;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Suspense } from "react"
|
||||
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { safeTry } from "@scandic-hotels/common/utils/safeTry"
|
||||
import { DEFAULT_THEME, type Theme } from "@scandic-hotels/common/utils/theme"
|
||||
import { Alert } from "@scandic-hotels/design-system/Alert"
|
||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||
import { type HotelPageData } from "@scandic-hotels/trpc/types/hotelPage"
|
||||
@@ -17,9 +18,7 @@ import HotelCampaigns from "@/components/ContentType/HotelPage/Campaigns"
|
||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import { setFacilityCards } from "@/utils/facilityCards"
|
||||
import { generateHotelSchema } from "@/utils/jsonSchemas"
|
||||
import { Theme } from "@/utils/theme/types"
|
||||
|
||||
import MapCard from "./Map/MapCard"
|
||||
import MapWithCardWrapper from "./Map/MapWithCard"
|
||||
@@ -35,7 +34,7 @@ import TripAdvisorSidePeek from "./SidePeeks/Tripadvisor"
|
||||
import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise"
|
||||
import AmenitiesList from "./AmenitiesList"
|
||||
import DialogshiftWidget from "./DialogshiftWidget"
|
||||
import Facilities from "./Facilities"
|
||||
import { Facilities } from "./Facilities"
|
||||
import IntroSection from "./IntroSection"
|
||||
import PreviewImages from "./PreviewImages"
|
||||
import { Rooms } from "./Rooms"
|
||||
@@ -131,12 +130,8 @@ export default async function HotelPage({
|
||||
},
|
||||
sectionHeadings
|
||||
)
|
||||
|
||||
const facilities = setFacilityCards(
|
||||
restaurantImages ?? undefined,
|
||||
conferencesAndMeetings ?? undefined,
|
||||
healthAndWellness ?? undefined,
|
||||
pageSections
|
||||
const activities = activitiesCards.map(
|
||||
(card) => card.upcoming_activities_card
|
||||
)
|
||||
|
||||
const coordinates = {
|
||||
@@ -151,7 +146,7 @@ export default async function HotelPage({
|
||||
)
|
||||
const trackingHotelData = getTrackingHotelData(hotelData.hotel)
|
||||
|
||||
const isThemed = theme !== Theme.scandic
|
||||
const isThemed = theme !== DEFAULT_THEME
|
||||
|
||||
return (
|
||||
<div className={styles.pageContainer}>
|
||||
@@ -225,14 +220,14 @@ export default async function HotelPage({
|
||||
preamble={hotelRoomElevatorPitchText}
|
||||
/>
|
||||
) : null}
|
||||
{facilities && (
|
||||
<Facilities
|
||||
facilities={facilities}
|
||||
activitiesCards={activitiesCards}
|
||||
amenities={detailedFacilities}
|
||||
healthFacilities={healthFacilities}
|
||||
/>
|
||||
)}
|
||||
<Facilities
|
||||
restaurantImages={restaurantImages ?? null}
|
||||
conferencesAndMeetings={conferencesAndMeetings ?? null}
|
||||
healthAndWellness={healthAndWellness ?? null}
|
||||
pageSections={pageSections}
|
||||
activities={activities}
|
||||
hotelTheme={theme}
|
||||
/>
|
||||
{campaignsBlock ? (
|
||||
<HotelCampaigns
|
||||
heading={campaignsBlock.heading}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { CardProps } from "@/components/TempDesignSystem/Card/card"
|
||||
import type { FacilityCard, FacilityImage } from "./hotelPage/facilities"
|
||||
|
||||
export interface CardImageProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
card: FacilityCard | CardProps
|
||||
imageCards?: FacilityImage[]
|
||||
}
|
||||
@@ -1,73 +1,3 @@
|
||||
import type {
|
||||
Amenities,
|
||||
Facility,
|
||||
HealthFacilities,
|
||||
} from "@scandic-hotels/trpc/types/hotel"
|
||||
import type { ActivitiesCard } from "@scandic-hotels/trpc/types/hotelPage"
|
||||
|
||||
import type { CardProps } from "@/components/TempDesignSystem/Card/card"
|
||||
|
||||
export type FacilitiesProps = {
|
||||
facilities: Facility[]
|
||||
activitiesCards: ActivitiesCard[]
|
||||
amenities: Amenities
|
||||
healthFacilities: HealthFacilities
|
||||
}
|
||||
|
||||
export type FacilityImage = {
|
||||
backgroundImage: CardProps["backgroundImage"]
|
||||
theme: CardProps["theme"]
|
||||
id: string
|
||||
}
|
||||
|
||||
export type FacilityCard = {
|
||||
secondaryButton: {
|
||||
href: string
|
||||
title: string
|
||||
openInNewTab?: boolean
|
||||
isExternal: boolean
|
||||
scrollOnClick: boolean
|
||||
}
|
||||
heading: string
|
||||
scriptedTopTitle?: string
|
||||
theme: CardProps["theme"]
|
||||
id: string
|
||||
}
|
||||
|
||||
export type FacilityCardType = FacilityImage | FacilityCard
|
||||
export type FacilityGrid = FacilityCardType[]
|
||||
export type Facilities = FacilityGrid[]
|
||||
|
||||
export type CardGridProps = {
|
||||
facilitiesCardGrid: FacilityGrid
|
||||
}
|
||||
|
||||
export enum FacilityCardTypeEnum {
|
||||
wellness = "wellness",
|
||||
conference = "meetings",
|
||||
restaurant = "restaurants",
|
||||
}
|
||||
|
||||
export enum RestaurantHeadings {
|
||||
generic = "Good vibes, great bites!",
|
||||
restaurantAndBar = "Restaurant & Bar",
|
||||
bar = "Bar",
|
||||
restaurant = "Restaurant",
|
||||
breakfastRestaurant = "Breakfast Restaurant",
|
||||
}
|
||||
|
||||
export enum WellnessHeadings {
|
||||
generic = "Here's to your health!",
|
||||
GymPool = "Gym & Pool",
|
||||
GymSauna = "Gym & Sauna",
|
||||
GymPoolSaunaRelax = "Gym, Pool, Sauna & Relax",
|
||||
GymJacuzziSaunaRelax = "Gym, Jacuzzi, Sauna & Relax",
|
||||
}
|
||||
|
||||
export enum MeetingsHeading {
|
||||
Default = "Great minds meet here",
|
||||
}
|
||||
|
||||
export enum HealthFacilitiesEnum {
|
||||
Jacuzzi = "Jacuzzi",
|
||||
Gym = "Gym",
|
||||
@@ -77,12 +7,6 @@ export enum HealthFacilitiesEnum {
|
||||
OutdoorPool = "OutdoorPool",
|
||||
}
|
||||
|
||||
export const FacilityCardButtonText = {
|
||||
RESTAURANT: "Read more",
|
||||
MEETINGS: "Read more",
|
||||
WELLNESS: "Read more",
|
||||
} as const
|
||||
|
||||
export enum ExternalGymDetails {
|
||||
NameOfExternalGym = "NameOfExternalGym",
|
||||
DistanceToExternalGym = "DistanceToExternalGym",
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
import { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
|
||||
|
||||
import type {
|
||||
Amenities,
|
||||
Facility,
|
||||
FacilityData,
|
||||
HealthFacilities,
|
||||
} from "@scandic-hotels/trpc/types/hotel"
|
||||
|
||||
import {
|
||||
type Facilities,
|
||||
type FacilityCard,
|
||||
FacilityCardButtonText,
|
||||
type FacilityCardType,
|
||||
FacilityCardTypeEnum,
|
||||
type FacilityGrid,
|
||||
type FacilityImage,
|
||||
HealthFacilitiesEnum,
|
||||
MeetingsHeading,
|
||||
RestaurantHeadings,
|
||||
WellnessHeadings,
|
||||
} from "@/types/components/hotelPage/facilities"
|
||||
import {
|
||||
type HotelHashValue,
|
||||
SidepeekSlugs,
|
||||
} from "@/types/components/hotelPage/hotelPage"
|
||||
import type { HotelPageSections } from "@/types/components/hotelPage/sections"
|
||||
import { HotelHashValues } from "@/types/enums/hotelPage"
|
||||
import type { CardProps } from "@/components/TempDesignSystem/Card/card"
|
||||
|
||||
export function setFacilityCards(
|
||||
restaurantImages: FacilityData | undefined,
|
||||
conferencesAndMeetings: FacilityData | undefined,
|
||||
healthAndWellness: FacilityData | undefined,
|
||||
pageSections: HotelPageSections
|
||||
): Facility[] {
|
||||
const facilities = []
|
||||
if (pageSections.restaurant) {
|
||||
facilities.push(
|
||||
setFacilityCard(
|
||||
restaurantImages,
|
||||
FacilityCardTypeEnum.restaurant,
|
||||
pageSections.restaurant.heading
|
||||
)
|
||||
)
|
||||
}
|
||||
if (pageSections.meetings) {
|
||||
facilities.push(
|
||||
setFacilityCard(
|
||||
conferencesAndMeetings,
|
||||
FacilityCardTypeEnum.conference,
|
||||
pageSections.meetings.heading
|
||||
)
|
||||
)
|
||||
}
|
||||
if (pageSections.wellness) {
|
||||
facilities.push(
|
||||
setFacilityCard(
|
||||
healthAndWellness,
|
||||
FacilityCardTypeEnum.wellness,
|
||||
pageSections.wellness.heading
|
||||
)
|
||||
)
|
||||
}
|
||||
return facilities
|
||||
}
|
||||
|
||||
function setFacilityCard(
|
||||
facility: FacilityData | undefined,
|
||||
type: FacilityCardTypeEnum,
|
||||
heading: string
|
||||
): Facility {
|
||||
return {
|
||||
...facility,
|
||||
id: type,
|
||||
headingText: heading,
|
||||
heroImages: facility?.heroImages ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
export function isFacilityCard(card: FacilityCardType): card is FacilityCard {
|
||||
return "heading" in card
|
||||
}
|
||||
|
||||
export function isFacilityImage(card: FacilityCardType): card is FacilityImage {
|
||||
return "backgroundImage" in card
|
||||
}
|
||||
|
||||
function setCardProps(
|
||||
theme: CardProps["theme"],
|
||||
buttonText: (typeof FacilityCardButtonText)[keyof typeof FacilityCardButtonText],
|
||||
href: HotelHashValue,
|
||||
heading: string,
|
||||
slug: SidepeekSlugs,
|
||||
scriptedTopTitle?: string
|
||||
): FacilityCard {
|
||||
return {
|
||||
theme,
|
||||
id: href,
|
||||
heading,
|
||||
scriptedTopTitle,
|
||||
secondaryButton: {
|
||||
href: `?s=${slug}`,
|
||||
title: buttonText,
|
||||
isExternal: false,
|
||||
scrollOnClick: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function setFacilityCardGrids(
|
||||
facilities: Facility[],
|
||||
amenities: Amenities,
|
||||
healthFacilities: HealthFacilities
|
||||
): Facilities {
|
||||
const cards: Facilities = facilities
|
||||
.filter((fac) => !!fac.headingText)
|
||||
.map((facility) => {
|
||||
let card: FacilityCard
|
||||
|
||||
const grid: FacilityGrid = facility.heroImages
|
||||
.slice(0, 2)
|
||||
.map((image) => {
|
||||
// Can be a maximum 2 images per grid
|
||||
const img: FacilityImage = {
|
||||
backgroundImage: {
|
||||
url: image.src,
|
||||
title: image.title || image.title_En,
|
||||
meta: {
|
||||
alt: image.altText,
|
||||
caption: image.altText_En,
|
||||
},
|
||||
id: image.src,
|
||||
},
|
||||
theme: "image",
|
||||
id: image.src,
|
||||
}
|
||||
return img
|
||||
})
|
||||
|
||||
switch (facility.id) {
|
||||
case FacilityCardTypeEnum.wellness:
|
||||
const wellnessTitle = getWellnessHeading(healthFacilities)
|
||||
card = setCardProps(
|
||||
"one",
|
||||
FacilityCardButtonText.WELLNESS,
|
||||
HotelHashValues.wellness,
|
||||
facility.headingText,
|
||||
SidepeekSlugs.wellness,
|
||||
wellnessTitle
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
facilities.findIndex((f) => f === facility) % 2 === 0
|
||||
? grid.unshift(card)
|
||||
: grid.push(card)
|
||||
|
||||
break
|
||||
|
||||
case FacilityCardTypeEnum.conference:
|
||||
card = setCardProps(
|
||||
"primaryDim",
|
||||
FacilityCardButtonText.MEETINGS,
|
||||
HotelHashValues.meetings,
|
||||
facility.headingText,
|
||||
SidepeekSlugs.meetings,
|
||||
MeetingsHeading.Default
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
facilities.findIndex((f) => f === facility) % 2 === 0
|
||||
? grid.unshift(card)
|
||||
: grid.push(card)
|
||||
break
|
||||
|
||||
case FacilityCardTypeEnum.restaurant:
|
||||
const restaurantTitle = getRestaurantHeading(amenities)
|
||||
card = setCardProps(
|
||||
"primaryDark",
|
||||
FacilityCardButtonText.RESTAURANT,
|
||||
HotelHashValues.restaurant,
|
||||
facility.headingText,
|
||||
SidepeekSlugs.restaurant,
|
||||
restaurantTitle
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
facilities.findIndex((f) => f === facility) % 2 === 0
|
||||
? grid.unshift(card)
|
||||
: grid.push(card)
|
||||
break
|
||||
}
|
||||
return grid
|
||||
})
|
||||
return cards
|
||||
}
|
||||
|
||||
function getRestaurantHeading(amenities: Amenities): RestaurantHeadings {
|
||||
// For now return a generic message for all
|
||||
return RestaurantHeadings.generic
|
||||
|
||||
// TODO: Revisit logic below when Content team decides to do so.
|
||||
const hasBar = amenities.some(
|
||||
(facility) =>
|
||||
facility.id === FacilityEnum.Bar ||
|
||||
facility.id === FacilityEnum.RooftopBar ||
|
||||
facility.id === FacilityEnum.Skybar
|
||||
)
|
||||
const hasRestaurant = amenities.some(
|
||||
(facility) => facility.id === FacilityEnum.Restaurant
|
||||
)
|
||||
|
||||
if (hasBar && hasRestaurant) {
|
||||
return RestaurantHeadings.restaurantAndBar
|
||||
} else if (hasBar) {
|
||||
return RestaurantHeadings.bar
|
||||
} else if (hasRestaurant) {
|
||||
return RestaurantHeadings.restaurant
|
||||
}
|
||||
return RestaurantHeadings.breakfastRestaurant
|
||||
}
|
||||
|
||||
function getWellnessHeading(
|
||||
healthFacilities: HealthFacilities
|
||||
): WellnessHeadings | undefined {
|
||||
// For now return a generic message for all
|
||||
return WellnessHeadings.generic
|
||||
|
||||
// TODO: Revisit logic below when Content team decides to do so.
|
||||
const hasGym = healthFacilities.some(
|
||||
(facility) => facility.type === HealthFacilitiesEnum.Gym
|
||||
)
|
||||
const hasSauna = healthFacilities.some(
|
||||
(faility) => faility.type === HealthFacilitiesEnum.Sauna
|
||||
)
|
||||
const hasRelax = healthFacilities.some(
|
||||
(facility) => facility.type === HealthFacilitiesEnum.Relax
|
||||
)
|
||||
const hasJacuzzi = healthFacilities.some(
|
||||
(facility) => facility.type === HealthFacilitiesEnum.Jacuzzi
|
||||
)
|
||||
const hasPool = healthFacilities.some(
|
||||
(facility) =>
|
||||
facility.type === HealthFacilitiesEnum.IndoorPool ||
|
||||
facility.type === HealthFacilitiesEnum.OutdoorPool
|
||||
)
|
||||
|
||||
if (hasGym && hasJacuzzi && hasSauna && hasRelax) {
|
||||
return WellnessHeadings.GymJacuzziSaunaRelax
|
||||
} else if (hasGym && hasPool && hasSauna && hasRelax) {
|
||||
return WellnessHeadings.GymPoolSaunaRelax
|
||||
} else if (hasGym && hasSauna) {
|
||||
return WellnessHeadings.GymSauna
|
||||
} else if (hasGym && hasPool) {
|
||||
return WellnessHeadings.GymPool
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function filterFacilityCards(cards: FacilityGrid) {
|
||||
const card = cards.filter((card) => isFacilityCard(card))
|
||||
const images = cards.filter((card) => isFacilityImage(card))
|
||||
|
||||
return {
|
||||
card: card[0] as FacilityCard,
|
||||
images: images as FacilityImage[],
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { SignatureHotelEnum } from "@scandic-hotels/common/constants/signatureHotels"
|
||||
import { HotelTypeEnum } from "@scandic-hotels/trpc/enums/hotelType"
|
||||
|
||||
import { DEFAULT_THEME, Theme } from "./types"
|
||||
|
||||
function getSignatureHotelTheme(hotelId: string) {
|
||||
switch (hotelId) {
|
||||
case SignatureHotelEnum.Haymarket:
|
||||
return Theme.haymarket
|
||||
case SignatureHotelEnum.HotelNorge:
|
||||
return Theme.hotelNorge
|
||||
case SignatureHotelEnum.DowntownCamper:
|
||||
return Theme.downtownCamper
|
||||
case SignatureHotelEnum.GrandHotelOslo:
|
||||
return Theme.grandHotel
|
||||
case SignatureHotelEnum.Marski:
|
||||
return Theme.marski
|
||||
default:
|
||||
return Theme.scandic
|
||||
}
|
||||
}
|
||||
|
||||
export function getThemeByHotel(hotelId: string, hotelType: string) {
|
||||
if (hotelType === HotelTypeEnum.ScandicGo) {
|
||||
return Theme.scandicGo
|
||||
}
|
||||
if (hotelType === HotelTypeEnum.Signature) {
|
||||
return getSignatureHotelTheme(hotelId)
|
||||
}
|
||||
|
||||
return DEFAULT_THEME
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
export enum Theme {
|
||||
downtownCamper = "downtown-camper",
|
||||
grandHotel = "grand-hotel",
|
||||
haymarket = "haymarket",
|
||||
hotelNorge = "hotel-norge",
|
||||
marski = "marski",
|
||||
scandic = "scandic",
|
||||
scandicGo = "scandic-go",
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME = Theme.scandic
|
||||
export const THEMES = Object.values(Theme)
|
||||
Reference in New Issue
Block a user