diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/hotel_page/[uid]/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/hotel_page/[uid]/page.tsx
index 6406b1c5d..f4e13b6a7 100644
--- a/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/hotel_page/[uid]/page.tsx
+++ b/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/hotel_page/[uid]/page.tsx
@@ -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"
diff --git a/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx b/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx
deleted file mode 100644
index 9b7452da4..000000000
--- a/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx
+++ /dev/null
@@ -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 (
-
- )
-}
diff --git a/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/CardImage/cardImage.module.css b/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/CardImage/cardImage.module.css
deleted file mode 100644
index e923e6a66..000000000
--- a/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/CardImage/cardImage.module.css
+++ /dev/null
@@ -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);
-}
diff --git a/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/CardImage/index.tsx b/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/CardImage/index.tsx
deleted file mode 100644
index ff5c83ad4..000000000
--- a/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/CardImage/index.tsx
+++ /dev/null
@@ -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 (
-
-
- {imageCards?.map(
- ({ backgroundImage }) =>
- backgroundImage && (
-
-
-
- )
- )}
-
-
-
- )
-}
diff --git a/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css b/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css
deleted file mode 100644
index ae275e36e..000000000
--- a/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx b/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx
deleted file mode 100644
index 55be6edec..000000000
--- a/apps/scandic-web/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx
+++ /dev/null
@@ -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 (
-
-
- {facilitiesCardGrid.map((card: FacilityCardType) =>
- isFacilityImage(card) && card.backgroundImage ? (
-
-
-
- ) : (
-
- )
- )}
-
-
-
-
-
- )
-}
diff --git a/apps/scandic-web/components/ContentType/HotelPage/Facilities/facilities.module.css b/apps/scandic-web/components/ContentType/HotelPage/Facilities/facilities.module.css
index ac08a952b..ff28142bc 100644
--- a/apps/scandic-web/components/ContentType/HotelPage/Facilities/facilities.module.css
+++ b/apps/scandic-web/components/ContentType/HotelPage/Facilities/facilities.module.css
@@ -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;
+ }
+ }
}
diff --git a/apps/scandic-web/components/ContentType/HotelPage/Facilities/index.tsx b/apps/scandic-web/components/ContentType/HotelPage/Facilities/index.tsx
index 87161ae7b..d1c499b5c 100644
--- a/apps/scandic-web/components/ContentType/HotelPage/Facilities/index.tsx
+++ b/apps/scandic-web/components/ContentType/HotelPage/Facilities/index.tsx
@@ -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 (
- {translatedFacilityGrids.map((cardGrid: FacilityGrid) => (
-
+ {facilityRows.map((facility, index) => (
+
+
+ {facility.images.length ? (
+
+ {facility.images.map((image) => (
+
+
+
+ ))}
+
+ ) : null}
+
))}
- {activitiesCards.length ? (
-
- {activitiesCards.map((card) => (
-
+ {activityCards.length ? (
+
+ {activityCards.map((card) => (
+
))}
) : null}
diff --git a/apps/scandic-web/components/ContentType/HotelPage/Facilities/utils.ts b/apps/scandic-web/components/ContentType/HotelPage/Facilities/utils.ts
new file mode 100644
index 000000000..ca170dd10
--- /dev/null
+++ b/apps/scandic-web/components/ContentType/HotelPage/Facilities/utils.ts
@@ -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
+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
+ })
+}
diff --git a/apps/scandic-web/components/ContentType/HotelPage/IntroSection/introSection.module.css b/apps/scandic-web/components/ContentType/HotelPage/IntroSection/introSection.module.css
index a5275a571..b85b987ac 100644
--- a/apps/scandic-web/components/ContentType/HotelPage/IntroSection/introSection.module.css
+++ b/apps/scandic-web/components/ContentType/HotelPage/IntroSection/introSection.module.css
@@ -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;
diff --git a/apps/scandic-web/components/ContentType/HotelPage/index.tsx b/apps/scandic-web/components/ContentType/HotelPage/index.tsx
index dfccac83a..7dca96823 100644
--- a/apps/scandic-web/components/ContentType/HotelPage/index.tsx
+++ b/apps/scandic-web/components/ContentType/HotelPage/index.tsx
@@ -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 (
@@ -225,14 +220,14 @@ export default async function HotelPage({
preamble={hotelRoomElevatorPitchText}
/>
) : null}
- {facilities && (
-
- )}
+
{campaignsBlock ? (
{
- card: FacilityCard | CardProps
- imageCards?: FacilityImage[]
-}
diff --git a/apps/scandic-web/types/components/hotelPage/facilities.ts b/apps/scandic-web/types/components/hotelPage/facilities.ts
index c1d476e90..f22e3fb62 100644
--- a/apps/scandic-web/types/components/hotelPage/facilities.ts
+++ b/apps/scandic-web/types/components/hotelPage/facilities.ts
@@ -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",
diff --git a/apps/scandic-web/utils/facilityCards.ts b/apps/scandic-web/utils/facilityCards.ts
deleted file mode 100644
index 59a7fee3f..000000000
--- a/apps/scandic-web/utils/facilityCards.ts
+++ /dev/null
@@ -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[],
- }
-}
diff --git a/apps/scandic-web/utils/theme/types.ts b/apps/scandic-web/utils/theme/types.ts
deleted file mode 100644
index 21d2221ea..000000000
--- a/apps/scandic-web/utils/theme/types.ts
+++ /dev/null
@@ -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)
diff --git a/packages/common/package.json b/packages/common/package.json
index 0eb2e5731..044ac9804 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -64,6 +64,7 @@
"./utils/promiseWithTimeout": "./utils/promiseWithTimeout.ts",
"./utils/rangeArray": "./utils/rangeArray.ts",
"./utils/safeTry": "./utils/safeTry.ts",
+ "./utils/theme": "./utils/theme.ts",
"./utils/toCapitalCase": "./utils/toCapitalCase.ts",
"./utils/url": "./utils/url.ts",
"./utils/zod/*": "./utils/zod/*.ts"
diff --git a/apps/scandic-web/utils/theme/index.ts b/packages/common/utils/theme.ts
similarity index 67%
rename from apps/scandic-web/utils/theme/index.ts
rename to packages/common/utils/theme.ts
index 3d3491abc..482a9d767 100644
--- a/apps/scandic-web/utils/theme/index.ts
+++ b/packages/common/utils/theme.ts
@@ -1,7 +1,19 @@
import { SignatureHotelEnum } from "@scandic-hotels/common/constants/signatureHotels"
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) {
switch (hotelId) {
@@ -15,8 +27,10 @@ function getSignatureHotelTheme(hotelId: string) {
return Theme.grandHotel
case SignatureHotelEnum.Marski:
return Theme.marski
+ case SignatureHotelEnum.TheDock:
+ return Theme.theDock
default:
- return Theme.scandic
+ return DEFAULT_THEME
}
}
diff --git a/packages/design-system/lib/components/ImageFallback/index.tsx b/packages/design-system/lib/components/ImageFallback/index.tsx
index f5f727cbd..c8a672860 100644
--- a/packages/design-system/lib/components/ImageFallback/index.tsx
+++ b/packages/design-system/lib/components/ImageFallback/index.tsx
@@ -1,8 +1,9 @@
+import { cx } from 'class-variance-authority'
import { MaterialIcon } from '../Icons/MaterialIcon'
import styles from './imageFallback.module.css'
-interface ImageFallbackProps {
+interface ImageFallbackProps extends React.HTMLAttributes {
width?: string
height?: string
}
@@ -10,9 +11,15 @@ interface ImageFallbackProps {
export default function ImageFallback({
width = '100%',
height = '100%',
+ className,
+ ...props
}: ImageFallbackProps) {
return (
-
+
= {
+ 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 (
+
+ {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 (
+
+
{theme}
+
+
+ )
+ })}
+
+ )
+ }
+
+ return (
+
+
+
+ )
+ },
+ ],
+}
+
+export default meta
+
+type Story = StoryObj
+
+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,
+ },
+}
diff --git a/packages/design-system/lib/components/InfoCard/InfoCard.tsx b/packages/design-system/lib/components/InfoCard/InfoCard.tsx
new file mode 100644
index 000000000..b5474cea1
--- /dev/null
+++ b/packages/design-system/lib/components/InfoCard/InfoCard.tsx
@@ -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 (
+
+ {theme === 'Image' ? (
+
+ {backgroundImage ? (
+
+ ) : (
+
+ )}
+
+ ) : null}
+
+
+ {topTitle ? (
+
+ {topTitle}
+
+ ) : null}
+
+ {heading}
+
+
+ {bodyText ? (
+
+ {bodyText}
+
+ ) : null}
+
+ {primaryButton ? (
+
+ {primaryButton.text}
+
+ ) : null}
+ {secondaryButton ? (
+
+ {secondaryButton.text}
+
+ ) : null}
+
+
+
+ )
+}
diff --git a/packages/design-system/lib/components/InfoCard/index.tsx b/packages/design-system/lib/components/InfoCard/index.tsx
new file mode 100644
index 000000000..a2542ec97
--- /dev/null
+++ b/packages/design-system/lib/components/InfoCard/index.tsx
@@ -0,0 +1,2 @@
+export { InfoCard } from './InfoCard'
+export type { InfoCardProps } from './types'
diff --git a/packages/design-system/lib/components/InfoCard/infoCard.module.css b/packages/design-system/lib/components/InfoCard/infoCard.module.css
new file mode 100644
index 000000000..52161efdc
--- /dev/null
+++ b/packages/design-system/lib/components/InfoCard/infoCard.module.css
@@ -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;
+ }
+}
diff --git a/packages/design-system/lib/components/InfoCard/types.ts b/packages/design-system/lib/components/InfoCard/types.ts
new file mode 100644
index 000000000..3bf24e0cf
--- /dev/null
+++ b/packages/design-system/lib/components/InfoCard/types.ts
@@ -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
+}
+
+export interface InfoCardProps
+ extends React.HTMLAttributes,
+ VariantProps {
+ topTitle?: string | null
+ heading?: string | null
+ bodyText?: string | null
+ backgroundImage?: InfoCardBackgroundImage
+ primaryButton?: InfoCardButton | null
+ secondaryButton?: InfoCardButton | null
+}
diff --git a/packages/design-system/lib/components/InfoCard/utils.ts b/packages/design-system/lib/components/InfoCard/utils.ts
new file mode 100644
index 000000000..62f1b5f54
--- /dev/null
+++ b/packages/design-system/lib/components/InfoCard/utils.ts
@@ -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
+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['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
+}
diff --git a/packages/design-system/lib/components/InfoCard/variants.ts b/packages/design-system/lib/components/InfoCard/variants.ts
new file mode 100644
index 000000000..07aec643a
--- /dev/null
+++ b/packages/design-system/lib/components/InfoCard/variants.ts
@@ -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)
diff --git a/packages/design-system/package.json b/packages/design-system/package.json
index 6a38236b4..47ab4d5eb 100644
--- a/packages/design-system/package.json
+++ b/packages/design-system/package.json
@@ -134,6 +134,7 @@
"./ImageContainer": "./lib/components/ImageContainer/index.tsx",
"./ImageFallback": "./lib/components/ImageFallback/index.tsx",
"./ImageGallery": "./lib/components/ImageGallery/index.tsx",
+ "./InfoCard": "./lib/components/InfoCard/index.tsx",
"./Input": "./lib/components/Input/index.tsx",
"./JsonToHtml": "./lib/components/JsonToHtml/JsonToHtml.tsx",
"./Label": "./lib/components/Label/index.tsx",
diff --git a/packages/trpc/lib/types/hotel.ts b/packages/trpc/lib/types/hotel.ts
index fe5d4b47b..e42b40621 100644
--- a/packages/trpc/lib/types/hotel.ts
+++ b/packages/trpc/lib/types/hotel.ts
@@ -40,6 +40,7 @@ import type { imageSchema } from "../routers/hotels/schemas/image"
export type HotelData = z.output
export type Amenities = z.output
+export type Amenity = Amenities[number]
export type CheckInData = z.output
type CitySchema = z.output
export type City = Pick & CitySchema["attributes"]