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 { cx } from "class-variance-authority"
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_THEME,
|
||||||
|
getThemeByHotel,
|
||||||
|
} from "@scandic-hotels/common/utils/theme"
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests"
|
import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import HotelMapPage from "@/components/ContentType/HotelMapPage"
|
import HotelMapPage from "@/components/ContentType/HotelMapPage"
|
||||||
import HotelPage from "@/components/ContentType/HotelPage"
|
import HotelPage from "@/components/ContentType/HotelPage"
|
||||||
import HotelSubpage from "@/components/ContentType/HotelSubpage"
|
import HotelSubpage from "@/components/ContentType/HotelSubpage"
|
||||||
import { getThemeByHotel } from "@/utils/theme"
|
|
||||||
import { DEFAULT_THEME } from "@/utils/theme/types"
|
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
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,
|
.facilitiesSection,
|
||||||
.activitiesCards {
|
.activityCards {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Space-x2);
|
gap: var(--Space-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.activitiesCards {
|
.facilityRow {
|
||||||
scroll-margin-top: var(--hotel-page-scroll-margin-top);
|
&: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) {
|
@media screen and (min-width: 768px) {
|
||||||
.facilitiesSection,
|
.facilitiesSection,
|
||||||
.activitiesCards {
|
.activityCards {
|
||||||
gap: var(--Space-x7);
|
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 { getIntl } from "@/i18n"
|
||||||
import { isFacilityCard, setFacilityCardGrids } from "@/utils/facilityCards"
|
|
||||||
|
|
||||||
import ActivitiesCardGrid from "./CardGrid/ActivitiesCardGrid"
|
import {
|
||||||
import FacilitiesCardGrid from "./CardGrid"
|
mapActivityCardsToInfoCards,
|
||||||
|
mapFacilitiesToFacilityRows,
|
||||||
|
mapFacilityDataToFacilities,
|
||||||
|
} from "./utils"
|
||||||
|
|
||||||
import styles from "./facilities.module.css"
|
import styles from "./facilities.module.css"
|
||||||
|
|
||||||
import {
|
import type { Theme } from "@scandic-hotels/common/utils/theme"
|
||||||
type Facilities,
|
import type { ActivityCard } from "@scandic-hotels/trpc/types/hotelPage"
|
||||||
type FacilitiesProps,
|
|
||||||
FacilityCardButtonText,
|
import type { HotelPageSections } from "@/types/components/hotelPage/sections"
|
||||||
type FacilityCardType,
|
|
||||||
type FacilityGrid,
|
|
||||||
} from "@/types/components/hotelPage/facilities"
|
|
||||||
import { HotelHashValues } from "@/types/enums/hotelPage"
|
import { HotelHashValues } from "@/types/enums/hotelPage"
|
||||||
|
|
||||||
export default async function Facilities({
|
interface FacilitiesProps {
|
||||||
facilities,
|
restaurantImages: FacilityData | null
|
||||||
amenities,
|
conferencesAndMeetings: FacilityData | null
|
||||||
healthFacilities,
|
healthAndWellness: FacilityData | null
|
||||||
activitiesCards,
|
pageSections: HotelPageSections
|
||||||
|
activities: ActivityCard[]
|
||||||
|
hotelTheme: Theme
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function Facilities({
|
||||||
|
restaurantImages,
|
||||||
|
conferencesAndMeetings,
|
||||||
|
healthAndWellness,
|
||||||
|
pageSections,
|
||||||
|
activities,
|
||||||
|
hotelTheme,
|
||||||
}: FacilitiesProps) {
|
}: FacilitiesProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
const facilities = mapFacilityDataToFacilities(
|
||||||
const facilityCardGrids = setFacilityCardGrids(
|
restaurantImages,
|
||||||
facilities,
|
conferencesAndMeetings,
|
||||||
amenities,
|
healthAndWellness,
|
||||||
healthFacilities
|
pageSections
|
||||||
)
|
)
|
||||||
|
const facilityRows = mapFacilitiesToFacilityRows(intl, facilities)
|
||||||
|
const activityCards = mapActivityCardsToInfoCards(activities)
|
||||||
|
|
||||||
const translatedFacilityGrids: Facilities = facilityCardGrids.map(
|
if (!facilityRows.length && !activityCards.length) {
|
||||||
(cardGrid: FacilityGrid) => {
|
return null
|
||||||
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",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.facilitiesSection}>
|
<section className={styles.facilitiesSection}>
|
||||||
{translatedFacilityGrids.map((cardGrid: FacilityGrid) => (
|
{facilityRows.map((facility, index) => (
|
||||||
<FacilitiesCardGrid
|
<div
|
||||||
key={cardGrid[0].id}
|
id={facility.id}
|
||||||
facilitiesCardGrid={cardGrid}
|
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 ? (
|
{activityCards.length ? (
|
||||||
<div id={HotelHashValues.activities} className={styles.activitiesCards}>
|
<div id={HotelHashValues.activities} className={styles.activityCards}>
|
||||||
{activitiesCards.map((card) => (
|
{activityCards.map((card) => (
|
||||||
<ActivitiesCardGrid
|
<InfoCard key={card.heading} {...card} hotelTheme={hotelTheme} />
|
||||||
key={card.upcoming_activities_card.contentPage.href}
|
|
||||||
{...card.upcoming_activities_card}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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);
|
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) {
|
@media screen and (max-width: 767px) {
|
||||||
.contactInformationDivider {
|
.contactInformationDivider {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Suspense } from "react"
|
|||||||
|
|
||||||
import { dt } from "@scandic-hotels/common/dt"
|
import { dt } from "@scandic-hotels/common/dt"
|
||||||
import { safeTry } from "@scandic-hotels/common/utils/safeTry"
|
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 { Alert } from "@scandic-hotels/design-system/Alert"
|
||||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||||
import { type HotelPageData } from "@scandic-hotels/trpc/types/hotelPage"
|
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 BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
import { setFacilityCards } from "@/utils/facilityCards"
|
|
||||||
import { generateHotelSchema } from "@/utils/jsonSchemas"
|
import { generateHotelSchema } from "@/utils/jsonSchemas"
|
||||||
import { Theme } from "@/utils/theme/types"
|
|
||||||
|
|
||||||
import MapCard from "./Map/MapCard"
|
import MapCard from "./Map/MapCard"
|
||||||
import MapWithCardWrapper from "./Map/MapWithCard"
|
import MapWithCardWrapper from "./Map/MapWithCard"
|
||||||
@@ -35,7 +34,7 @@ import TripAdvisorSidePeek from "./SidePeeks/Tripadvisor"
|
|||||||
import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise"
|
import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise"
|
||||||
import AmenitiesList from "./AmenitiesList"
|
import AmenitiesList from "./AmenitiesList"
|
||||||
import DialogshiftWidget from "./DialogshiftWidget"
|
import DialogshiftWidget from "./DialogshiftWidget"
|
||||||
import Facilities from "./Facilities"
|
import { Facilities } from "./Facilities"
|
||||||
import IntroSection from "./IntroSection"
|
import IntroSection from "./IntroSection"
|
||||||
import PreviewImages from "./PreviewImages"
|
import PreviewImages from "./PreviewImages"
|
||||||
import { Rooms } from "./Rooms"
|
import { Rooms } from "./Rooms"
|
||||||
@@ -131,12 +130,8 @@ export default async function HotelPage({
|
|||||||
},
|
},
|
||||||
sectionHeadings
|
sectionHeadings
|
||||||
)
|
)
|
||||||
|
const activities = activitiesCards.map(
|
||||||
const facilities = setFacilityCards(
|
(card) => card.upcoming_activities_card
|
||||||
restaurantImages ?? undefined,
|
|
||||||
conferencesAndMeetings ?? undefined,
|
|
||||||
healthAndWellness ?? undefined,
|
|
||||||
pageSections
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const coordinates = {
|
const coordinates = {
|
||||||
@@ -151,7 +146,7 @@ export default async function HotelPage({
|
|||||||
)
|
)
|
||||||
const trackingHotelData = getTrackingHotelData(hotelData.hotel)
|
const trackingHotelData = getTrackingHotelData(hotelData.hotel)
|
||||||
|
|
||||||
const isThemed = theme !== Theme.scandic
|
const isThemed = theme !== DEFAULT_THEME
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.pageContainer}>
|
<div className={styles.pageContainer}>
|
||||||
@@ -225,14 +220,14 @@ export default async function HotelPage({
|
|||||||
preamble={hotelRoomElevatorPitchText}
|
preamble={hotelRoomElevatorPitchText}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{facilities && (
|
<Facilities
|
||||||
<Facilities
|
restaurantImages={restaurantImages ?? null}
|
||||||
facilities={facilities}
|
conferencesAndMeetings={conferencesAndMeetings ?? null}
|
||||||
activitiesCards={activitiesCards}
|
healthAndWellness={healthAndWellness ?? null}
|
||||||
amenities={detailedFacilities}
|
pageSections={pageSections}
|
||||||
healthFacilities={healthFacilities}
|
activities={activities}
|
||||||
/>
|
hotelTheme={theme}
|
||||||
)}
|
/>
|
||||||
{campaignsBlock ? (
|
{campaignsBlock ? (
|
||||||
<HotelCampaigns
|
<HotelCampaigns
|
||||||
heading={campaignsBlock.heading}
|
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 {
|
export enum HealthFacilitiesEnum {
|
||||||
Jacuzzi = "Jacuzzi",
|
Jacuzzi = "Jacuzzi",
|
||||||
Gym = "Gym",
|
Gym = "Gym",
|
||||||
@@ -77,12 +7,6 @@ export enum HealthFacilitiesEnum {
|
|||||||
OutdoorPool = "OutdoorPool",
|
OutdoorPool = "OutdoorPool",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FacilityCardButtonText = {
|
|
||||||
RESTAURANT: "Read more",
|
|
||||||
MEETINGS: "Read more",
|
|
||||||
WELLNESS: "Read more",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export enum ExternalGymDetails {
|
export enum ExternalGymDetails {
|
||||||
NameOfExternalGym = "NameOfExternalGym",
|
NameOfExternalGym = "NameOfExternalGym",
|
||||||
DistanceToExternalGym = "DistanceToExternalGym",
|
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,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)
|
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
"./utils/promiseWithTimeout": "./utils/promiseWithTimeout.ts",
|
"./utils/promiseWithTimeout": "./utils/promiseWithTimeout.ts",
|
||||||
"./utils/rangeArray": "./utils/rangeArray.ts",
|
"./utils/rangeArray": "./utils/rangeArray.ts",
|
||||||
"./utils/safeTry": "./utils/safeTry.ts",
|
"./utils/safeTry": "./utils/safeTry.ts",
|
||||||
|
"./utils/theme": "./utils/theme.ts",
|
||||||
"./utils/toCapitalCase": "./utils/toCapitalCase.ts",
|
"./utils/toCapitalCase": "./utils/toCapitalCase.ts",
|
||||||
"./utils/url": "./utils/url.ts",
|
"./utils/url": "./utils/url.ts",
|
||||||
"./utils/zod/*": "./utils/zod/*.ts"
|
"./utils/zod/*": "./utils/zod/*.ts"
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
import { SignatureHotelEnum } from "@scandic-hotels/common/constants/signatureHotels"
|
import { SignatureHotelEnum } from "@scandic-hotels/common/constants/signatureHotels"
|
||||||
import { HotelTypeEnum } from "@scandic-hotels/trpc/enums/hotelType"
|
import { HotelTypeEnum } from "@scandic-hotels/trpc/enums/hotelType"
|
||||||
|
|
||||||
import { DEFAULT_THEME, Theme } from "./types"
|
export enum Theme {
|
||||||
|
scandic = "scandic",
|
||||||
|
downtownCamper = "downtown-camper",
|
||||||
|
haymarket = "haymarket",
|
||||||
|
scandicGo = "scandic-go",
|
||||||
|
grandHotel = "grand-hotel",
|
||||||
|
hotelNorge = "hotel-norge",
|
||||||
|
marski = "marski",
|
||||||
|
theDock = "the-dock",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_THEME = Theme.scandic
|
||||||
|
export const THEMES = Object.values(Theme)
|
||||||
|
|
||||||
function getSignatureHotelTheme(hotelId: string) {
|
function getSignatureHotelTheme(hotelId: string) {
|
||||||
switch (hotelId) {
|
switch (hotelId) {
|
||||||
@@ -15,8 +27,10 @@ function getSignatureHotelTheme(hotelId: string) {
|
|||||||
return Theme.grandHotel
|
return Theme.grandHotel
|
||||||
case SignatureHotelEnum.Marski:
|
case SignatureHotelEnum.Marski:
|
||||||
return Theme.marski
|
return Theme.marski
|
||||||
|
case SignatureHotelEnum.TheDock:
|
||||||
|
return Theme.theDock
|
||||||
default:
|
default:
|
||||||
return Theme.scandic
|
return DEFAULT_THEME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { cx } from 'class-variance-authority'
|
||||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||||
|
|
||||||
import styles from './imageFallback.module.css'
|
import styles from './imageFallback.module.css'
|
||||||
|
|
||||||
interface ImageFallbackProps {
|
interface ImageFallbackProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
width?: string
|
width?: string
|
||||||
height?: string
|
height?: string
|
||||||
}
|
}
|
||||||
@@ -10,9 +11,15 @@ interface ImageFallbackProps {
|
|||||||
export default function ImageFallback({
|
export default function ImageFallback({
|
||||||
width = '100%',
|
width = '100%',
|
||||||
height = '100%',
|
height = '100%',
|
||||||
|
className,
|
||||||
|
...props
|
||||||
}: ImageFallbackProps) {
|
}: ImageFallbackProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.imageFallback} style={{ width, height }}>
|
<div
|
||||||
|
{...props}
|
||||||
|
className={cx(styles.imageFallback, className)}
|
||||||
|
style={{ width, height }}
|
||||||
|
>
|
||||||
<MaterialIcon
|
<MaterialIcon
|
||||||
icon="imagesmode"
|
icon="imagesmode"
|
||||||
size={32}
|
size={32}
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||||
|
|
||||||
|
import { Theme } from '@scandic-hotels/common/utils/theme'
|
||||||
|
import { InfoCard } from './InfoCard.tsx'
|
||||||
|
import { infoCardConfig } from './variants.ts'
|
||||||
|
|
||||||
|
const DEFAULT_ARGS = {
|
||||||
|
topTitle: "Here's to your health!",
|
||||||
|
heading: 'Gym & Wellness',
|
||||||
|
primaryButton: {
|
||||||
|
href: '#',
|
||||||
|
text: 'Primary button',
|
||||||
|
},
|
||||||
|
secondaryButton: {
|
||||||
|
href: '#',
|
||||||
|
text: 'Secondary button',
|
||||||
|
},
|
||||||
|
bodyText:
|
||||||
|
'Our gym is open 24/7 and offers state-of-the-art equipment to help you stay fit during your stay.',
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta: Meta<typeof InfoCard> = {
|
||||||
|
title: 'Components/InfoCard',
|
||||||
|
component: InfoCard,
|
||||||
|
argTypes: {
|
||||||
|
topTitle: {
|
||||||
|
control: 'text',
|
||||||
|
table: {
|
||||||
|
type: { summary: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
topTitleAngled: {
|
||||||
|
control: 'boolean',
|
||||||
|
description:
|
||||||
|
'Whether the top title should be angled. Only applies when `hotelTheme` is set to `Theme.scandic`.',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
control: 'text',
|
||||||
|
table: {
|
||||||
|
type: { summary: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bodyText: {
|
||||||
|
control: 'text',
|
||||||
|
table: {
|
||||||
|
type: { summary: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
control: 'select',
|
||||||
|
options: Object.keys(infoCardConfig.variants.theme),
|
||||||
|
table: {
|
||||||
|
type: {
|
||||||
|
summary: Object.keys(infoCardConfig.variants.theme).join(' | '),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
control: 'select',
|
||||||
|
options: Object.keys(infoCardConfig.variants.height),
|
||||||
|
table: {
|
||||||
|
type: {
|
||||||
|
summary: Object.keys(infoCardConfig.variants.height).join(' | '),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hotelTheme: {
|
||||||
|
control: 'select',
|
||||||
|
options: Object.keys(infoCardConfig.variants.hotelTheme),
|
||||||
|
description:
|
||||||
|
'The hotel theme to adjust button colors for better contrast.',
|
||||||
|
table: {
|
||||||
|
type: { summary: 'Theme', detail: Object.values(Theme).join(' | ') },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
control: 'object',
|
||||||
|
table: {
|
||||||
|
type: {
|
||||||
|
summary: 'InfoCardBackgroundImage',
|
||||||
|
detail:
|
||||||
|
'{ src: string, alt?: string, focalPoint?: { x: number, y: number }, dimensions?: { width: number, height: number, aspectRatio?: number } }',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
primaryButton: {
|
||||||
|
control: 'object',
|
||||||
|
table: {
|
||||||
|
type: {
|
||||||
|
summary: 'InfoCardButton',
|
||||||
|
detail:
|
||||||
|
'{ href: string, text: string, openInNewTab?: boolean, scrollOnClick?: boolean, onClick?: MouseEventHandler }',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secondaryButton: {
|
||||||
|
control: 'object',
|
||||||
|
table: {
|
||||||
|
type: {
|
||||||
|
summary: 'InfoCardButton',
|
||||||
|
detail:
|
||||||
|
'{ href: string, text: string, openInNewTab?: boolean, scrollOnClick?: boolean, onClick?: MouseEventHandler }',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: { ...DEFAULT_ARGS },
|
||||||
|
decorators: [
|
||||||
|
(Story, context) => {
|
||||||
|
if (context.name.toLowerCase().indexOf('all themes') >= 0) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={context.args.hotelTheme!}
|
||||||
|
style={{ display: 'grid', gap: '1rem' }}
|
||||||
|
>
|
||||||
|
{Object.keys(infoCardConfig.variants.theme).map((theme) => {
|
||||||
|
console.log(theme)
|
||||||
|
const args = {
|
||||||
|
...context.args,
|
||||||
|
backgroundImage:
|
||||||
|
theme === 'Image'
|
||||||
|
? {
|
||||||
|
src: './img/img1.jpg',
|
||||||
|
alt: 'Image alt text',
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'grid', gap: '0.5rem' }}>
|
||||||
|
<h3>{theme}</h3>
|
||||||
|
<InfoCard
|
||||||
|
{...args}
|
||||||
|
theme={theme as keyof typeof infoCardConfig.variants.theme}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex' }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof InfoCard>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary_1: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
theme: 'Primary 1',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary_2: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
theme: 'Primary 2',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary_3: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
theme: 'Primary 3',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Accent: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
theme: 'Accent',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const White: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
theme: 'White',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Image: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
backgroundImage: {
|
||||||
|
src: './img/img1.jpg',
|
||||||
|
alt: 'Image alt text',
|
||||||
|
},
|
||||||
|
theme: 'Image',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllThemesScandic: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
hotelTheme: Theme.scandic,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllThemesDowntownCamper: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
hotelTheme: Theme.downtownCamper,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllThemesHaymarket: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
hotelTheme: Theme.haymarket,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllThemesScandicGo: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
hotelTheme: Theme.scandicGo,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllThemesGrandHotel: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
hotelTheme: Theme.grandHotel,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllThemesHotelNorge: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
hotelTheme: Theme.hotelNorge,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllThemesMarski: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
hotelTheme: Theme.marski,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllThemesTheDock: Story = {
|
||||||
|
args: {
|
||||||
|
...meta.args,
|
||||||
|
hotelTheme: Theme.theDock,
|
||||||
|
},
|
||||||
|
}
|
||||||
100
packages/design-system/lib/components/InfoCard/InfoCard.tsx
Normal file
100
packages/design-system/lib/components/InfoCard/InfoCard.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import ButtonLink from '../ButtonLink'
|
||||||
|
import Image from '../Image'
|
||||||
|
import { Typography } from '../Typography'
|
||||||
|
|
||||||
|
import { getButtonProps } from './utils'
|
||||||
|
|
||||||
|
import { infoCardVariants } from './variants'
|
||||||
|
|
||||||
|
import styles from './infoCard.module.css'
|
||||||
|
|
||||||
|
import ImageFallback from '../ImageFallback'
|
||||||
|
import type { InfoCardProps } from './types'
|
||||||
|
|
||||||
|
export function InfoCard({
|
||||||
|
primaryButton,
|
||||||
|
secondaryButton,
|
||||||
|
topTitle,
|
||||||
|
heading,
|
||||||
|
bodyText,
|
||||||
|
className,
|
||||||
|
theme,
|
||||||
|
height,
|
||||||
|
backgroundImage,
|
||||||
|
topTitleAngled,
|
||||||
|
hotelTheme,
|
||||||
|
}: InfoCardProps) {
|
||||||
|
const classNames = infoCardVariants({
|
||||||
|
theme,
|
||||||
|
hotelTheme,
|
||||||
|
topTitleAngled,
|
||||||
|
height,
|
||||||
|
className,
|
||||||
|
})
|
||||||
|
const buttonProps = getButtonProps(theme, hotelTheme)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames}>
|
||||||
|
{theme === 'Image' ? (
|
||||||
|
<div className={styles.backgroundImageWrapper}>
|
||||||
|
{backgroundImage ? (
|
||||||
|
<Image
|
||||||
|
src={backgroundImage.src}
|
||||||
|
className={styles.backgroundImage}
|
||||||
|
alt={backgroundImage.alt ?? ''}
|
||||||
|
fill
|
||||||
|
sizes="(min-width: 1367px) 700px, 900px"
|
||||||
|
focalPoint={backgroundImage.focalPoint}
|
||||||
|
dimensions={backgroundImage.dimensions}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ImageFallback className={styles.backgroundImage} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.titleWrapper}>
|
||||||
|
{topTitle ? (
|
||||||
|
<Typography variant="Title/Decorative/md">
|
||||||
|
<span className={styles.topTitle}>{topTitle}</span>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<Typography variant="Title/smLowCase">
|
||||||
|
<h3 className={styles.heading}>{heading}</h3>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
{bodyText ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.bodyText}>{bodyText}</p>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
{primaryButton ? (
|
||||||
|
<ButtonLink
|
||||||
|
size="Small"
|
||||||
|
href={primaryButton.href}
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
onClick={primaryButton.onClick}
|
||||||
|
scroll={primaryButton.scrollOnClick ?? false}
|
||||||
|
{...buttonProps.primaryButton}
|
||||||
|
>
|
||||||
|
{primaryButton.text}
|
||||||
|
</ButtonLink>
|
||||||
|
) : null}
|
||||||
|
{secondaryButton ? (
|
||||||
|
<ButtonLink
|
||||||
|
size="Small"
|
||||||
|
href={secondaryButton.href}
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
onClick={secondaryButton.onClick}
|
||||||
|
scroll={secondaryButton.scrollOnClick ?? false}
|
||||||
|
{...buttonProps.secondaryButton}
|
||||||
|
>
|
||||||
|
{secondaryButton.text}
|
||||||
|
</ButtonLink>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
2
packages/design-system/lib/components/InfoCard/index.tsx
Normal file
2
packages/design-system/lib/components/InfoCard/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { InfoCard } from './InfoCard'
|
||||||
|
export type { InfoCardProps } from './types'
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
.infoCard {
|
||||||
|
--background-color: var(--Surface-Brand-Primary-1-Default);
|
||||||
|
--topTitle-color: var(--Text-Brand-OnPrimary-1-Accent);
|
||||||
|
--heading-color: var(--Text-Brand-OnPrimary-1-Heading);
|
||||||
|
--text-color: var(--Text-Brand-OnPrimary-1-Default);
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
text-wrap: balance;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.height-fixed {
|
||||||
|
height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.height-dynamic {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-primary-1 {
|
||||||
|
--background-color: var(--Surface-Brand-Primary-1-Default);
|
||||||
|
--topTitle-color: var(--Text-Brand-OnPrimary-1-Accent);
|
||||||
|
--heading-color: var(--Text-Brand-OnPrimary-1-Heading);
|
||||||
|
--text-color: var(--Text-Brand-OnPrimary-1-Default);
|
||||||
|
}
|
||||||
|
.theme-primary-2 {
|
||||||
|
--background-color: var(--Surface-Brand-Primary-2-Default);
|
||||||
|
--topTitle-color: var(--Text-Brand-OnPrimary-2-Accent);
|
||||||
|
--heading-color: var(--Text-Brand-OnPrimary-2-Heading);
|
||||||
|
--text-color: var(--Text-Brand-OnPrimary-2-Default);
|
||||||
|
}
|
||||||
|
.theme-primary-3 {
|
||||||
|
--background-color: var(--Surface-Brand-Primary-3-Default);
|
||||||
|
--topTitle-color: var(--Text-Brand-OnPrimary-3-Accent);
|
||||||
|
--heading-color: var(--Text-Brand-OnPrimary-3-Heading);
|
||||||
|
--text-color: var(--Text-Brand-OnPrimary-3-Default);
|
||||||
|
}
|
||||||
|
.theme-accent {
|
||||||
|
--background-color: var(--Surface-Brand-Accent-Default);
|
||||||
|
--topTitle-color: var(--Text-Brand-OnAccent-Accent);
|
||||||
|
--heading-color: var(--Text-Brand-OnAccent-Heading);
|
||||||
|
--text-color: var(--Text-Brand-OnAccent-Default);
|
||||||
|
}
|
||||||
|
.theme-white {
|
||||||
|
--background-color: var(--Surface-Primary-Default);
|
||||||
|
--topTitle-color: var(--Text-Accent-Primary);
|
||||||
|
--heading-color: var(--Text-Heading);
|
||||||
|
--text-color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
.theme-image {
|
||||||
|
--background-color: transparent;
|
||||||
|
--topTitle-color: var(--Text-Inverted);
|
||||||
|
--heading-color: var(--Text-Inverted);
|
||||||
|
--text-color: var(--Text-Inverted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleWrapper {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topTitle {
|
||||||
|
color: var(--topTitle-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-title-angled .topTitle {
|
||||||
|
transform: rotate(-3deg);
|
||||||
|
transform-origin: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: var(--heading-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyText {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backgroundImageWrapper {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(0, 0, 0, 0) 0%,
|
||||||
|
rgba(0, 0, 0, 0.36) 50%,
|
||||||
|
rgba(0, 0, 0, 0.75) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.backgroundImage {
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
height: min(100%, 320px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: var(--Space-x4) var(--Space-x3);
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.primaryButton,
|
||||||
|
.secondaryButton {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
packages/design-system/lib/components/InfoCard/types.ts
Normal file
30
packages/design-system/lib/components/InfoCard/types.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { MouseEventHandler } from 'react'
|
||||||
|
import type { infoCardVariants } from './variants'
|
||||||
|
|
||||||
|
export type InfoCardBackgroundImage = {
|
||||||
|
src: string
|
||||||
|
alt?: string
|
||||||
|
focalPoint?: { x: number; y: number }
|
||||||
|
dimensions?: { width: number; height: number; aspectRatio?: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InfoCardButton = {
|
||||||
|
href: string
|
||||||
|
text: string
|
||||||
|
openInNewTab?: boolean
|
||||||
|
scrollOnClick?: boolean
|
||||||
|
onClick?: MouseEventHandler<HTMLAnchorElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InfoCardProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof infoCardVariants> {
|
||||||
|
topTitle?: string | null
|
||||||
|
heading?: string | null
|
||||||
|
bodyText?: string | null
|
||||||
|
backgroundImage?: InfoCardBackgroundImage
|
||||||
|
primaryButton?: InfoCardButton | null
|
||||||
|
secondaryButton?: InfoCardButton | null
|
||||||
|
}
|
||||||
166
packages/design-system/lib/components/InfoCard/utils.ts
Normal file
166
packages/design-system/lib/components/InfoCard/utils.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { Theme } from '@scandic-hotels/common/utils/theme'
|
||||||
|
import type { variants as buttonVariants } from '../Button/variants'
|
||||||
|
import type { infoCardVariants } from './variants'
|
||||||
|
|
||||||
|
type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||||
|
type InfoCardButtonProps = {
|
||||||
|
primaryButton: {
|
||||||
|
variant: ButtonVariants['variant']
|
||||||
|
color: ButtonVariants['color']
|
||||||
|
}
|
||||||
|
secondaryButton: {
|
||||||
|
variant: ButtonVariants['variant']
|
||||||
|
color: ButtonVariants['color']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRIMARY = { variant: 'Primary', color: 'Primary' } as const
|
||||||
|
const PRIMARY_INVERTED = { variant: 'Primary', color: 'Inverted' } as const
|
||||||
|
const SECONDARY = { variant: 'Secondary', color: 'Primary' } as const
|
||||||
|
const SECONDARY_INVERTED = { variant: 'Secondary', color: 'Inverted' } as const
|
||||||
|
const TERTIARY = { variant: 'Tertiary', color: 'Primary' } as const
|
||||||
|
|
||||||
|
// Determine button variant and color based on card theme and hotel theme.
|
||||||
|
// This is done to avoid low contrast issues and conflicting colors in
|
||||||
|
// certain combinations and according to design guidelines.
|
||||||
|
export function getButtonProps(
|
||||||
|
cardTheme: VariantProps<typeof infoCardVariants>['theme'],
|
||||||
|
hotelTheme: Theme | null = Theme.scandic
|
||||||
|
): InfoCardButtonProps {
|
||||||
|
let buttonProps: InfoCardButtonProps = {
|
||||||
|
primaryButton: TERTIARY,
|
||||||
|
secondaryButton: SECONDARY,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image theme always uses inverted buttons, regardless of hotel theme
|
||||||
|
if (cardTheme === 'Image') {
|
||||||
|
return {
|
||||||
|
primaryButton: PRIMARY_INVERTED,
|
||||||
|
secondaryButton: SECONDARY_INVERTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (hotelTheme) {
|
||||||
|
case Theme.scandic:
|
||||||
|
if (cardTheme === 'Primary 2' || cardTheme === 'Primary 3') {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY_INVERTED,
|
||||||
|
secondaryButton: SECONDARY_INVERTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Theme.downtownCamper:
|
||||||
|
if (
|
||||||
|
cardTheme === 'Primary 1' ||
|
||||||
|
cardTheme === 'Primary 2' ||
|
||||||
|
cardTheme === 'Primary 3' ||
|
||||||
|
cardTheme === 'Accent'
|
||||||
|
) {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY_INVERTED,
|
||||||
|
secondaryButton: SECONDARY_INVERTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Theme.haymarket:
|
||||||
|
if (cardTheme === 'Primary 1' || cardTheme === 'White') {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY,
|
||||||
|
secondaryButton: SECONDARY,
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
cardTheme === 'Primary 2' ||
|
||||||
|
cardTheme === 'Primary 3' ||
|
||||||
|
cardTheme === 'Accent'
|
||||||
|
) {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY_INVERTED,
|
||||||
|
secondaryButton: SECONDARY_INVERTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Theme.scandicGo:
|
||||||
|
if (cardTheme === 'Primary 1' || cardTheme === 'Primary 2') {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY_INVERTED,
|
||||||
|
secondaryButton: SECONDARY_INVERTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Theme.grandHotel:
|
||||||
|
if (
|
||||||
|
cardTheme === 'Primary 2' ||
|
||||||
|
cardTheme === 'Primary 3' ||
|
||||||
|
cardTheme === 'Accent' ||
|
||||||
|
cardTheme === 'White'
|
||||||
|
) {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY,
|
||||||
|
secondaryButton: SECONDARY,
|
||||||
|
}
|
||||||
|
} else if (cardTheme === 'Primary 1') {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY_INVERTED,
|
||||||
|
secondaryButton: SECONDARY_INVERTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Theme.hotelNorge:
|
||||||
|
if (
|
||||||
|
cardTheme === 'Primary 1' ||
|
||||||
|
cardTheme === 'Primary 2' ||
|
||||||
|
cardTheme === 'Primary 3'
|
||||||
|
) {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY_INVERTED,
|
||||||
|
secondaryButton: SECONDARY_INVERTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Theme.marski:
|
||||||
|
if (cardTheme === 'Primary 1') {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: TERTIARY,
|
||||||
|
secondaryButton: SECONDARY_INVERTED,
|
||||||
|
}
|
||||||
|
} else if (cardTheme === 'White') {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY,
|
||||||
|
secondaryButton: SECONDARY,
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
cardTheme === 'Primary 2' ||
|
||||||
|
cardTheme === 'Primary 3' ||
|
||||||
|
cardTheme === 'Accent'
|
||||||
|
) {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY_INVERTED,
|
||||||
|
secondaryButton: SECONDARY_INVERTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case Theme.theDock:
|
||||||
|
if (
|
||||||
|
cardTheme === 'Primary 1' ||
|
||||||
|
cardTheme === 'Accent' ||
|
||||||
|
cardTheme === 'White'
|
||||||
|
) {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY,
|
||||||
|
secondaryButton: SECONDARY,
|
||||||
|
}
|
||||||
|
} else if (cardTheme === 'Primary 2' || cardTheme === 'Primary 3') {
|
||||||
|
buttonProps = {
|
||||||
|
primaryButton: PRIMARY_INVERTED,
|
||||||
|
secondaryButton: SECONDARY_INVERTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttonProps
|
||||||
|
}
|
||||||
70
packages/design-system/lib/components/InfoCard/variants.ts
Normal file
70
packages/design-system/lib/components/InfoCard/variants.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { DEFAULT_THEME, Theme } from '@scandic-hotels/common/utils/theme'
|
||||||
|
import styles from './infoCard.module.css'
|
||||||
|
|
||||||
|
const variantKeys = {
|
||||||
|
theme: {
|
||||||
|
'Primary 1': 'Primary 1',
|
||||||
|
'Primary 2': 'Primary 2',
|
||||||
|
'Primary 3': 'Primary 3',
|
||||||
|
Accent: 'Accent',
|
||||||
|
Image: 'Image',
|
||||||
|
White: 'White',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
fixed: 'fixed',
|
||||||
|
dynamic: 'dynamic',
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const infoCardConfig = {
|
||||||
|
variants: {
|
||||||
|
theme: {
|
||||||
|
[variantKeys.theme['Primary 1']]: styles['theme-primary-1'],
|
||||||
|
[variantKeys.theme['Primary 2']]: styles['theme-primary-2'],
|
||||||
|
[variantKeys.theme['Primary 3']]: styles['theme-primary-3'],
|
||||||
|
[variantKeys.theme['Accent']]: styles['theme-accent'],
|
||||||
|
[variantKeys.theme['Image']]: styles['theme-image'],
|
||||||
|
[variantKeys.theme['White']]: styles['theme-white'],
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
[variantKeys.height.fixed]: styles['height-fixed'],
|
||||||
|
[variantKeys.height.dynamic]: styles['height-dynamic'],
|
||||||
|
},
|
||||||
|
// Only Theme.scandic can be used with the Angled variant.
|
||||||
|
// The topTitleAngled variant will be applied using the compoundVariants.
|
||||||
|
topTitleAngled: {
|
||||||
|
true: undefined,
|
||||||
|
false: undefined,
|
||||||
|
},
|
||||||
|
// The hotelTheme is not used to apply styles directly,
|
||||||
|
// but is needed for compound variants and to get the correct button color.
|
||||||
|
// The class name for the hotelTheme is applied on page level.
|
||||||
|
hotelTheme: {
|
||||||
|
[Theme.scandic]: undefined,
|
||||||
|
[Theme.downtownCamper]: undefined,
|
||||||
|
[Theme.haymarket]: undefined,
|
||||||
|
[Theme.scandicGo]: undefined,
|
||||||
|
[Theme.grandHotel]: undefined,
|
||||||
|
[Theme.hotelNorge]: undefined,
|
||||||
|
[Theme.marski]: undefined,
|
||||||
|
[Theme.theDock]: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
hotelTheme: Theme.scandic,
|
||||||
|
topTitleAngled: true,
|
||||||
|
class: styles['top-title-angled'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultVariants: {
|
||||||
|
theme: variantKeys.theme['Primary 1'],
|
||||||
|
height: variantKeys.height.fixed,
|
||||||
|
topTitleAngled: false,
|
||||||
|
hotelTheme: DEFAULT_THEME,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const infoCardVariants = cva(styles.infoCard, infoCardConfig)
|
||||||
@@ -134,6 +134,7 @@
|
|||||||
"./ImageContainer": "./lib/components/ImageContainer/index.tsx",
|
"./ImageContainer": "./lib/components/ImageContainer/index.tsx",
|
||||||
"./ImageFallback": "./lib/components/ImageFallback/index.tsx",
|
"./ImageFallback": "./lib/components/ImageFallback/index.tsx",
|
||||||
"./ImageGallery": "./lib/components/ImageGallery/index.tsx",
|
"./ImageGallery": "./lib/components/ImageGallery/index.tsx",
|
||||||
|
"./InfoCard": "./lib/components/InfoCard/index.tsx",
|
||||||
"./Input": "./lib/components/Input/index.tsx",
|
"./Input": "./lib/components/Input/index.tsx",
|
||||||
"./JsonToHtml": "./lib/components/JsonToHtml/JsonToHtml.tsx",
|
"./JsonToHtml": "./lib/components/JsonToHtml/JsonToHtml.tsx",
|
||||||
"./Label": "./lib/components/Label/index.tsx",
|
"./Label": "./lib/components/Label/index.tsx",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import type { imageSchema } from "../routers/hotels/schemas/image"
|
|||||||
export type HotelData = z.output<typeof hotelSchema>
|
export type HotelData = z.output<typeof hotelSchema>
|
||||||
|
|
||||||
export type Amenities = z.output<typeof detailedFacilitiesSchema>
|
export type Amenities = z.output<typeof detailedFacilitiesSchema>
|
||||||
|
export type Amenity = Amenities[number]
|
||||||
export type CheckInData = z.output<typeof checkinSchema>
|
export type CheckInData = z.output<typeof checkinSchema>
|
||||||
type CitySchema = z.output<typeof citySchema>
|
type CitySchema = z.output<typeof citySchema>
|
||||||
export type City = Pick<CitySchema, "id" | "type"> & CitySchema["attributes"]
|
export type City = Pick<CitySchema, "id" | "type"> & CitySchema["attributes"]
|
||||||
|
|||||||
Reference in New Issue
Block a user