Merge branch 'develop' into feat/sw-386-header-fixes
This commit is contained in:
27
app/[lang]/(live)/(public)/hotelreservation/README.md
Normal file
27
app/[lang]/(live)/(public)/hotelreservation/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Booking flow
|
||||||
|
|
||||||
|
The booking flow is the user journey of booking one or more rooms at our
|
||||||
|
hotels. Everything from choosing the date to payment and confirmation is
|
||||||
|
part of the booking flow.
|
||||||
|
|
||||||
|
## Booking widget
|
||||||
|
|
||||||
|
On most of the pages on the website we have a booking widget. This is where
|
||||||
|
the user starts the booking flow, by filling the form and submit. If they
|
||||||
|
entered a city as the destination they will land on the select hotel page
|
||||||
|
and if they entered a specific hotel they will land on the select rate page.
|
||||||
|
|
||||||
|
## Select hotel
|
||||||
|
|
||||||
|
Lists available hotels based on the search criteria. When the user selects
|
||||||
|
a hotel they land on the select rate page.
|
||||||
|
|
||||||
|
## Select rate, room, breakfast etc
|
||||||
|
|
||||||
|
This is a page with an accordion like design, but every accordion is handled
|
||||||
|
as its own page with its own URL.
|
||||||
|
|
||||||
|
## State management
|
||||||
|
|
||||||
|
The state, like search parameters and selected alternatives, is kept
|
||||||
|
throughout the booking flow in the URL.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import SectionContainer from "@/components/Section/Container"
|
import SectionContainer from "@/components/Section/Container"
|
||||||
import SectionHeader from "@/components/Section/Header"
|
import SectionHeader from "@/components/Section/Header"
|
||||||
import Card from "@/components/TempDesignSystem/Card"
|
import Card from "@/components/TempDesignSystem/Card"
|
||||||
|
import ContentCard from "@/components/TempDesignSystem/ContentCard"
|
||||||
import Grids from "@/components/TempDesignSystem/Grids"
|
import Grids from "@/components/TempDesignSystem/Grids"
|
||||||
import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard"
|
import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard"
|
||||||
|
|
||||||
@@ -21,8 +22,19 @@ export default function CardsGrid({
|
|||||||
<Grids.Stackable>
|
<Grids.Stackable>
|
||||||
{cards_grid.cards.map((card) => {
|
{cards_grid.cards.map((card) => {
|
||||||
switch (card.__typename) {
|
switch (card.__typename) {
|
||||||
case CardsGridEnum.Card: {
|
case CardsGridEnum.Card:
|
||||||
return (
|
return card.isContentCard ? (
|
||||||
|
<ContentCard
|
||||||
|
key={card.system.uid}
|
||||||
|
title={card.heading || ""}
|
||||||
|
description={card.body_text || ""}
|
||||||
|
primaryButton={card.primaryButton}
|
||||||
|
secondaryButton={card.secondaryButton}
|
||||||
|
sidePeekButton={card.sidePeekButton}
|
||||||
|
backgroundImage={card.background_image}
|
||||||
|
style="default"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Card
|
<Card
|
||||||
theme={cards_grid.theme || "one"}
|
theme={cards_grid.theme || "one"}
|
||||||
key={card.system.uid}
|
key={card.system.uid}
|
||||||
@@ -33,7 +45,6 @@ export default function CardsGrid({
|
|||||||
primaryButton={card.primaryButton}
|
primaryButton={card.primaryButton}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
case CardsGridEnum.LoyaltyCard:
|
case CardsGridEnum.LoyaltyCard:
|
||||||
return (
|
return (
|
||||||
<LoyaltyCard
|
<LoyaltyCard
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function RoomCard({
|
|||||||
onClick={handleRoomCtaClick}
|
onClick={handleRoomCtaClick}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: "See room details" })}
|
{intl.formatMessage({ id: "See room details" })}
|
||||||
<ChevronRightIcon className={styles.chevron} />
|
<ChevronRightIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import useDropdownStore from "@/stores/main-menu"
|
||||||
|
|
||||||
|
import { ChevronLeftIcon } from "@/components/Icons"
|
||||||
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
|
import styles from "./languageSwitcherContainer.module.css"
|
||||||
|
|
||||||
|
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
||||||
|
import {
|
||||||
|
type LanguageSwitcherContainerProps,
|
||||||
|
LanguageSwitcherTypesEnum,
|
||||||
|
} from "@/types/components/languageSwitcher/languageSwitcher"
|
||||||
|
|
||||||
|
export default function LanguageSwitcherContainer({
|
||||||
|
children,
|
||||||
|
type,
|
||||||
|
}: LanguageSwitcherContainerProps) {
|
||||||
|
const { toggleDropdown } = useDropdownStore()
|
||||||
|
const intl = useIntl()
|
||||||
|
const isFooter = type === LanguageSwitcherTypesEnum.Footer
|
||||||
|
const isMobileHeader = type === LanguageSwitcherTypesEnum.MobileHeader
|
||||||
|
const position = isFooter
|
||||||
|
? DropdownTypeEnum.FooterLanguageSwitcher
|
||||||
|
: DropdownTypeEnum.HamburgerMenu
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isMobileHeader ? (
|
||||||
|
<div className={styles.backWrapper}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.backButton}
|
||||||
|
onClick={() => toggleDropdown(position)}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon color="red" />
|
||||||
|
<Subtitle type="one">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Main menu",
|
||||||
|
})}
|
||||||
|
</Subtitle>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{isFooter ? (
|
||||||
|
<div className={styles.closeWrapper}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.closeButton}
|
||||||
|
aria-label={intl.formatMessage({
|
||||||
|
id: "Close menu",
|
||||||
|
})}
|
||||||
|
onClick={() => toggleDropdown(position)}
|
||||||
|
>
|
||||||
|
<span className={styles.bar}></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
.backWrapper {
|
||||||
|
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||||
|
padding: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeWrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: var(--Spacing-x2);
|
||||||
|
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
justify-self: flex-start;
|
||||||
|
padding: 11px var(--Spacing-x1) var(--Spacing-x2);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar,
|
||||||
|
.bar::after,
|
||||||
|
.bar::before {
|
||||||
|
background: var(--Base-Text-High-contrast);
|
||||||
|
border-radius: 2.3px;
|
||||||
|
display: inline-block;
|
||||||
|
height: 3px;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s;
|
||||||
|
width: var(--Spacing-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar::after,
|
||||||
|
.bar::before {
|
||||||
|
content: "";
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
width: var(--Spacing-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar::after {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar::before {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.closeWrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,8 @@
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Lang, languages } from "@/constants/languages"
|
import { Lang, languages } from "@/constants/languages"
|
||||||
import useDropdownStore from "@/stores/main-menu"
|
|
||||||
|
|
||||||
import { CheckIcon, ChevronLeftIcon } from "@/components/Icons"
|
import { CheckIcon } from "@/components/Icons"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
@@ -13,38 +12,19 @@ import { useTrapFocus } from "@/hooks/useTrapFocus"
|
|||||||
|
|
||||||
import styles from "./languageSwitcherContent.module.css"
|
import styles from "./languageSwitcherContent.module.css"
|
||||||
|
|
||||||
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
import type { LanguageSwitcherContentProps } from "@/types/components/languageSwitcher/languageSwitcher"
|
||||||
import type { LanguageSwitcherProps } from "@/types/components/languageSwitcher/languageSwitcher"
|
|
||||||
|
|
||||||
export default function LanguageSwitcherContent({
|
export default function LanguageSwitcherContent({
|
||||||
urls,
|
urls,
|
||||||
type,
|
}: LanguageSwitcherContentProps) {
|
||||||
}: LanguageSwitcherProps) {
|
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const currentLanguage = useLang()
|
const currentLanguage = useLang()
|
||||||
const { toggleDropdown } = useDropdownStore()
|
|
||||||
const languageSwitcherRef = useTrapFocus()
|
const languageSwitcherRef = useTrapFocus()
|
||||||
const urlKeys = Object.keys(urls) as Lang[]
|
const urlKeys = Object.keys(urls) as Lang[]
|
||||||
const position =
|
|
||||||
type === "footer"
|
|
||||||
? DropdownTypeEnum.FooterLanguageSwitcher
|
|
||||||
: DropdownTypeEnum.HamburgerMenu
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.languageSwitcherContent} ref={languageSwitcherRef}>
|
<div className={styles.languageSwitcherContent} ref={languageSwitcherRef}>
|
||||||
{type === "mobileHeader" ? (
|
|
||||||
<div className={styles.backWrapper}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.backButton}
|
|
||||||
onClick={() => toggleDropdown(position)}
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon color="red" />
|
|
||||||
<Subtitle type="one">Main Menu</Subtitle>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className={styles.languageWrapper}>
|
<div className={styles.languageWrapper}>
|
||||||
<Subtitle className={styles.subtitle} type="two">
|
<Subtitle className={styles.subtitle} type="two">
|
||||||
{intl.formatMessage({ id: "Select your language" })}
|
{intl.formatMessage({ id: "Select your language" })}
|
||||||
|
|||||||
@@ -1,22 +1,3 @@
|
|||||||
.backWrapper {
|
|
||||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
|
||||||
padding: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.backButton {
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--Base-Text-High-contrast);
|
|
||||||
font-family: var(--typography-Subtitle-1-fontFamily);
|
|
||||||
font-weight: var(--typography-Subtitle-1-fontWeight);
|
|
||||||
font-size: var(--typography-Subtitle-1-Mobile-fontSize);
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.languageWrapper {
|
.languageWrapper {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x3);
|
gap: var(--Spacing-x3);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { languages } from "@/constants/languages"
|
import { languages } from "@/constants/languages"
|
||||||
@@ -9,13 +10,17 @@ import { ChevronDownIcon, GlobeIcon } from "@/components/Icons"
|
|||||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import LanguageSwitcherContainer from "./LanguageSwitcherContainer"
|
||||||
import LanguageSwitcherContent from "./LanguageSwitcherContent"
|
import LanguageSwitcherContent from "./LanguageSwitcherContent"
|
||||||
import { languageSwitcherVariants } from "./variants"
|
import { languageSwitcherVariants } from "./variants"
|
||||||
|
|
||||||
import styles from "./languageSwitcher.module.css"
|
import styles from "./languageSwitcher.module.css"
|
||||||
|
|
||||||
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
||||||
import type { LanguageSwitcherProps } from "@/types/components/languageSwitcher/languageSwitcher"
|
import {
|
||||||
|
type LanguageSwitcherProps,
|
||||||
|
LanguageSwitcherTypesEnum,
|
||||||
|
} from "@/types/components/languageSwitcher/languageSwitcher"
|
||||||
|
|
||||||
export default function LanguageSwitcher({
|
export default function LanguageSwitcher({
|
||||||
urls,
|
urls,
|
||||||
@@ -30,8 +35,11 @@ export default function LanguageSwitcher({
|
|||||||
isHeaderLanguageSwitcherMobileOpen,
|
isHeaderLanguageSwitcherMobileOpen,
|
||||||
} = useDropdownStore()
|
} = useDropdownStore()
|
||||||
|
|
||||||
const position = type === "footer" ? "footer" : "header"
|
const isFooter = type === LanguageSwitcherTypesEnum.Footer
|
||||||
const color = type === "footer" ? "pale" : "burgundy"
|
const isHeader = !isFooter
|
||||||
|
|
||||||
|
const position = isFooter ? "footer" : "header"
|
||||||
|
const color = isFooter ? "pale" : "burgundy"
|
||||||
|
|
||||||
const dropdownType = {
|
const dropdownType = {
|
||||||
footer: DropdownTypeEnum.FooterLanguageSwitcher,
|
footer: DropdownTypeEnum.FooterLanguageSwitcher,
|
||||||
@@ -40,8 +48,8 @@ export default function LanguageSwitcher({
|
|||||||
}[type]
|
}[type]
|
||||||
|
|
||||||
const isLanguageSwitcherOpen =
|
const isLanguageSwitcherOpen =
|
||||||
(type === "footer" && isFooterLanguageSwitcherOpen) ||
|
(isFooter && isFooterLanguageSwitcherOpen) ||
|
||||||
(type !== "footer" &&
|
(isHeader &&
|
||||||
(isHeaderLanguageSwitcherOpen || isHeaderLanguageSwitcherMobileOpen))
|
(isHeaderLanguageSwitcherOpen || isHeaderLanguageSwitcherMobileOpen))
|
||||||
|
|
||||||
useHandleKeyUp((event: KeyboardEvent) => {
|
useHandleKeyUp((event: KeyboardEvent) => {
|
||||||
@@ -50,6 +58,18 @@ export default function LanguageSwitcher({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFooter && isFooterLanguageSwitcherOpen) {
|
||||||
|
document.body.style.overflow = "hidden"
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = ""
|
||||||
|
}
|
||||||
|
}, [isFooter, isFooterLanguageSwitcherOpen])
|
||||||
|
|
||||||
const classNames = languageSwitcherVariants({ color, position })
|
const classNames = languageSwitcherVariants({ color, position })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,7 +98,9 @@ export default function LanguageSwitcher({
|
|||||||
className={`${styles.dropdown} ${isLanguageSwitcherOpen ? styles.isExpanded : ""}`}
|
className={`${styles.dropdown} ${isLanguageSwitcherOpen ? styles.isExpanded : ""}`}
|
||||||
>
|
>
|
||||||
{isLanguageSwitcherOpen ? (
|
{isLanguageSwitcherOpen ? (
|
||||||
<LanguageSwitcherContent urls={urls} type={type} />
|
<LanguageSwitcherContainer type={type}>
|
||||||
|
<LanguageSwitcherContent urls={urls} />
|
||||||
|
</LanguageSwitcherContainer>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,20 +31,36 @@
|
|||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: var(--main-menu-mobile-height);
|
|
||||||
right: -100vw;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
transition: right 0.3s;
|
|
||||||
z-index: var(--menu-overlay-z-index);
|
z-index: var(--menu-overlay-z-index);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown.isExpanded {
|
.top .dropdown {
|
||||||
|
right: -100vw;
|
||||||
|
top: var(--main-menu-mobile-height);
|
||||||
|
bottom: 0;
|
||||||
|
transition: right 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top .dropdown.isExpanded {
|
||||||
display: block;
|
display: block;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bottom .dropdown {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom .dropdown.isExpanded {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.languageSwitcher {
|
.languageSwitcher {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -81,10 +97,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bottom .dropdown {
|
.bottom .dropdown {
|
||||||
top: auto;
|
transition: none;
|
||||||
|
height: auto;
|
||||||
|
left: -100%;
|
||||||
bottom: 2.25rem;
|
bottom: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bottom .dropdown.isExpanded {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom .dropdown::before {
|
.bottom .dropdown::before {
|
||||||
top: 100%;
|
top: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
.card {
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 399px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default {
|
||||||
|
background-color: var(--Base-Surface-Subtle-Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured {
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
}
|
||||||
|
|
||||||
|
.default,
|
||||||
|
.featured {
|
||||||
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 12.58625rem; /* 201.38px / 16 = 12.58625rem */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backgroundImage {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x-one-and-half);
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: var(--Base-Text-Medium-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaButton {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1367px) {
|
||||||
|
.card:not(.alwaysStack) .ctaContainer {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:not(.alwaysStack) .ctaContainer:has(:only-child) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidePeekCTA {
|
||||||
|
/* TODO: Create ticket to remove padding on "link" buttons,
|
||||||
|
align w. design on this. */
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
98
components/TempDesignSystem/ContentCard/index.tsx
Normal file
98
components/TempDesignSystem/ContentCard/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
import { ChevronRightIcon } from "@/components/Icons"
|
||||||
|
import Image from "@/components/Image"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|
||||||
|
import Subtitle from "../Text/Subtitle"
|
||||||
|
import { contentCardVariants } from "./variants"
|
||||||
|
|
||||||
|
import styles from "./contentCard.module.css"
|
||||||
|
|
||||||
|
import type { ContentCardProps } from "@/types/components/contentCard"
|
||||||
|
|
||||||
|
export default function ContentCard({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
primaryButton,
|
||||||
|
secondaryButton,
|
||||||
|
sidePeekButton,
|
||||||
|
backgroundImage,
|
||||||
|
style = "default",
|
||||||
|
alwaysStack = false,
|
||||||
|
className,
|
||||||
|
}: ContentCardProps) {
|
||||||
|
const cardClasses = contentCardVariants({ style, alwaysStack, className })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cardClasses}>
|
||||||
|
{backgroundImage && (
|
||||||
|
<div className={styles.imageContainer}>
|
||||||
|
<Image
|
||||||
|
src={backgroundImage.url}
|
||||||
|
alt={backgroundImage.meta?.alt || ""}
|
||||||
|
className={styles.backgroundImage}
|
||||||
|
width={399}
|
||||||
|
height={201}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.content}>
|
||||||
|
<Subtitle textAlign="left" type="two" color="black">
|
||||||
|
{title}
|
||||||
|
</Subtitle>
|
||||||
|
<Body color="black">{description}</Body>
|
||||||
|
{!!sidePeekButton ? (
|
||||||
|
<Button
|
||||||
|
// onClick={() => {
|
||||||
|
// // TODO: Implement sidePeek functionality once SW-341 is merged.
|
||||||
|
// }}
|
||||||
|
theme="base"
|
||||||
|
variant="icon"
|
||||||
|
intent="text"
|
||||||
|
size="small"
|
||||||
|
className={styles.sidePeekCTA}
|
||||||
|
>
|
||||||
|
{sidePeekButton.title}
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className={styles.ctaContainer}>
|
||||||
|
{primaryButton && (
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
intent="primary"
|
||||||
|
size="small"
|
||||||
|
className={styles.ctaButton}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={primaryButton.href}
|
||||||
|
target={primaryButton.openInNewTab ? "_blank" : undefined}
|
||||||
|
>
|
||||||
|
{primaryButton.title}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{secondaryButton && (
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
intent="secondary"
|
||||||
|
size="small"
|
||||||
|
className={styles.ctaButton}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={secondaryButton.href}
|
||||||
|
target={secondaryButton.openInNewTab ? "_blank" : undefined}
|
||||||
|
>
|
||||||
|
{secondaryButton.title}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
components/TempDesignSystem/ContentCard/variants.ts
Normal file
20
components/TempDesignSystem/ContentCard/variants.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
import styles from "./contentCard.module.css"
|
||||||
|
|
||||||
|
export const contentCardVariants = cva(styles.card, {
|
||||||
|
variants: {
|
||||||
|
style: {
|
||||||
|
default: styles.default,
|
||||||
|
featured: styles.featured,
|
||||||
|
},
|
||||||
|
alwaysStack: {
|
||||||
|
true: styles.alwaysStack,
|
||||||
|
false: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
style: "default",
|
||||||
|
alwaysStack: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -100,6 +100,7 @@
|
|||||||
"Log in here": "Log ind her",
|
"Log in here": "Log ind her",
|
||||||
"Log in/Join": "Log på/Tilmeld dig",
|
"Log in/Join": "Log på/Tilmeld dig",
|
||||||
"Log out": "Log ud",
|
"Log out": "Log ud",
|
||||||
|
"Main menu": "Hovedmenu",
|
||||||
"Manage preferences": "Administrer præferencer",
|
"Manage preferences": "Administrer præferencer",
|
||||||
"Map": "Kort",
|
"Map": "Kort",
|
||||||
"Map of HOTEL_NAME": "Map of {hotelName}",
|
"Map of HOTEL_NAME": "Map of {hotelName}",
|
||||||
|
|||||||
@@ -98,6 +98,7 @@
|
|||||||
"Log in here": "Hier einloggen",
|
"Log in here": "Hier einloggen",
|
||||||
"Log in/Join": "Log in/Anmelden",
|
"Log in/Join": "Log in/Anmelden",
|
||||||
"Log out": "Ausloggen",
|
"Log out": "Ausloggen",
|
||||||
|
"Main menu": "Hauptmenü",
|
||||||
"Manage preferences": "Verwalten von Voreinstellungen",
|
"Manage preferences": "Verwalten von Voreinstellungen",
|
||||||
"Map": "Karte",
|
"Map": "Karte",
|
||||||
"Map of HOTEL_NAME": "Map of {hotelName}",
|
"Map of HOTEL_NAME": "Map of {hotelName}",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"Log in here": "Log in here",
|
"Log in here": "Log in here",
|
||||||
"Log in/Join": "Log in/Join",
|
"Log in/Join": "Log in/Join",
|
||||||
"Log out": "Log out",
|
"Log out": "Log out",
|
||||||
|
"Main menu": "Main menu",
|
||||||
"Manage preferences": "Manage preferences",
|
"Manage preferences": "Manage preferences",
|
||||||
"Map": "Map",
|
"Map": "Map",
|
||||||
"Map of HOTEL_NAME": "Map of {hotelName}",
|
"Map of HOTEL_NAME": "Map of {hotelName}",
|
||||||
|
|||||||
@@ -99,6 +99,7 @@
|
|||||||
"Log in here": "Kirjaudu sisään",
|
"Log in here": "Kirjaudu sisään",
|
||||||
"Log in/Join": "Kirjaudu sisään/Liittyä",
|
"Log in/Join": "Kirjaudu sisään/Liittyä",
|
||||||
"Log out": "Kirjaudu ulos",
|
"Log out": "Kirjaudu ulos",
|
||||||
|
"Main menu": "Päävalikko",
|
||||||
"Manage preferences": "Asetusten hallinta",
|
"Manage preferences": "Asetusten hallinta",
|
||||||
"Map": "Kartta",
|
"Map": "Kartta",
|
||||||
"Map of HOTEL_NAME": "Map of {hotelName}",
|
"Map of HOTEL_NAME": "Map of {hotelName}",
|
||||||
|
|||||||
@@ -99,6 +99,7 @@
|
|||||||
"Log in here": "Logg inn her",
|
"Log in here": "Logg inn her",
|
||||||
"Log in/Join": "Logg på/Bli med",
|
"Log in/Join": "Logg på/Bli med",
|
||||||
"Log out": "Logg ut",
|
"Log out": "Logg ut",
|
||||||
|
"Main menu": "Hovedmeny",
|
||||||
"Manage preferences": "Administrer preferanser",
|
"Manage preferences": "Administrer preferanser",
|
||||||
"Map": "Kart",
|
"Map": "Kart",
|
||||||
"Map of HOTEL_NAME": "Map of {hotelName}",
|
"Map of HOTEL_NAME": "Map of {hotelName}",
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
"Log in here": "Logga in här",
|
"Log in here": "Logga in här",
|
||||||
"Log in/Join": "Logga in/Gå med",
|
"Log in/Join": "Logga in/Gå med",
|
||||||
"Log out": "Logga ut",
|
"Log out": "Logga ut",
|
||||||
|
"Main menu": "Huvudmeny",
|
||||||
"Manage preferences": "Hantera inställningar",
|
"Manage preferences": "Hantera inställningar",
|
||||||
"Map": "Karta",
|
"Map": "Karta",
|
||||||
"Map of HOTEL_NAME": "Map of {hotelName}",
|
"Map of HOTEL_NAME": "Map of {hotelName}",
|
||||||
|
|||||||
@@ -1,30 +1,13 @@
|
|||||||
fragment CardBlock on Card {
|
fragment CardBlock on Card {
|
||||||
|
is_content_card
|
||||||
heading
|
heading
|
||||||
body_text
|
body_text
|
||||||
background_image
|
background_image
|
||||||
scripted_top_title
|
scripted_top_title
|
||||||
title
|
title
|
||||||
has_secondary_button
|
|
||||||
secondary_button {
|
|
||||||
is_contentstack_link
|
|
||||||
cta_text
|
|
||||||
open_in_new_tab
|
|
||||||
external_link {
|
|
||||||
title
|
|
||||||
href
|
|
||||||
}
|
|
||||||
linkConnection {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
__typename
|
|
||||||
...LoyaltyPageLink
|
|
||||||
...ContentPageLink
|
|
||||||
...AccountPageLink
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
has_primary_button
|
has_primary_button
|
||||||
|
has_secondary_button
|
||||||
|
has_sidepeek_button
|
||||||
primary_button {
|
primary_button {
|
||||||
is_contentstack_link
|
is_contentstack_link
|
||||||
cta_text
|
cta_text
|
||||||
@@ -44,6 +27,28 @@ fragment CardBlock on Card {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
secondary_button {
|
||||||
|
is_contentstack_link
|
||||||
|
cta_text
|
||||||
|
open_in_new_tab
|
||||||
|
external_link {
|
||||||
|
title
|
||||||
|
href
|
||||||
|
}
|
||||||
|
linkConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
__typename
|
||||||
|
...LoyaltyPageLink
|
||||||
|
...ContentPageLink
|
||||||
|
...AccountPageLink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sidepeek_button {
|
||||||
|
call_to_action_text
|
||||||
|
}
|
||||||
system {
|
system {
|
||||||
locale
|
locale
|
||||||
uid
|
uid
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
GetLoyaltyPageSettings,
|
GetLoyaltyPageSettings,
|
||||||
} from "@/lib/graphql/Query/BookingWidgetToggle.graphql"
|
} from "@/lib/graphql/Query/BookingWidgetToggle.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
import { contentstackBaseProcedure, router } from "@/server/trpc"
|
||||||
|
|
||||||
import { generateTag } from "@/utils/generateTag"
|
import { generateTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ import { affix as bookingwidgetAffix } from "./utils"
|
|||||||
import { ContentTypeEnum } from "@/types/requests/contentType"
|
import { ContentTypeEnum } from "@/types/requests/contentType"
|
||||||
|
|
||||||
export const bookingwidgetQueryRouter = router({
|
export const bookingwidgetQueryRouter = router({
|
||||||
getToggle: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
getToggle: contentstackBaseProcedure.query(async ({ ctx }) => {
|
||||||
const failedResponse = { hideBookingWidget: false }
|
const failedResponse = { hideBookingWidget: false }
|
||||||
const { contentType, uid, lang } = ctx
|
const { contentType, uid, lang } = ctx
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export const contentPageDynamicContent = z.object({
|
|||||||
|
|
||||||
export const cardBlock = z.object({
|
export const cardBlock = z.object({
|
||||||
__typename: z.literal(CardsGridEnum.Card),
|
__typename: z.literal(CardsGridEnum.Card),
|
||||||
|
isContentCard: z.boolean(),
|
||||||
heading: z.string().nullable(),
|
heading: z.string().nullable(),
|
||||||
body_text: z.string().nullable(),
|
body_text: z.string().nullable(),
|
||||||
background_image: z.any(),
|
background_image: z.any(),
|
||||||
@@ -88,6 +89,11 @@ export const cardBlock = z.object({
|
|||||||
isExternal: z.boolean(),
|
isExternal: z.boolean(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
sidePeekButton: z
|
||||||
|
.object({
|
||||||
|
title: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
system: z.object({
|
system: z.object({
|
||||||
locale: z.nativeEnum(Lang),
|
locale: z.nativeEnum(Lang),
|
||||||
uid: z.string(),
|
uid: z.string(),
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export const contentPageQueryRouter = router({
|
|||||||
case CardsGridEnum.Card:
|
case CardsGridEnum.Card:
|
||||||
return {
|
return {
|
||||||
...card,
|
...card,
|
||||||
|
isContentCard: !!card.is_content_card,
|
||||||
backgroundImage: makeImageVaultImage(
|
backgroundImage: makeImageVaultImage(
|
||||||
card.background_image
|
card.background_image
|
||||||
),
|
),
|
||||||
@@ -111,6 +112,14 @@ export const contentPageQueryRouter = router({
|
|||||||
secondaryButton: card.has_secondary_button
|
secondaryButton: card.has_secondary_button
|
||||||
? makeButtonObject(card.secondary_button)
|
? makeButtonObject(card.secondary_button)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
sidePeekButton:
|
||||||
|
card.has_sidepeek_button ||
|
||||||
|
!!card.sidepeek_button?.call_to_action_text
|
||||||
|
? {
|
||||||
|
title:
|
||||||
|
card.sidepeek_button.call_to_action_text,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
case CardsGridEnum.LoyaltyCard:
|
case CardsGridEnum.LoyaltyCard:
|
||||||
return {
|
return {
|
||||||
|
|||||||
21
types/components/contentCard.ts
Normal file
21
types/components/contentCard.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { contentCardVariants } from "@/components/TempDesignSystem/ContentCard/variants"
|
||||||
|
|
||||||
|
import { ImageVaultAsset } from "@/types/components/imageVault"
|
||||||
|
import type { CardProps } from "@/components/TempDesignSystem/Card/card"
|
||||||
|
|
||||||
|
interface SidePeekButton {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentCardProps
|
||||||
|
extends VariantProps<typeof contentCardVariants> {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
primaryButton?: CardProps["primaryButton"]
|
||||||
|
secondaryButton?: CardProps["secondaryButton"]
|
||||||
|
sidePeekButton?: SidePeekButton
|
||||||
|
backgroundImage?: ImageVaultAsset
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
@@ -1,6 +1,25 @@
|
|||||||
|
import { ReactElement } from "react"
|
||||||
|
|
||||||
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
|
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
|
||||||
|
|
||||||
|
export enum LanguageSwitcherTypesEnum {
|
||||||
|
MobileHeader = "mobileHeader",
|
||||||
|
DesktopHeader = "desktopHeader",
|
||||||
|
Footer = "footer",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LanguageSwitcherTypes = `${LanguageSwitcherTypesEnum}`
|
||||||
|
|
||||||
export interface LanguageSwitcherProps {
|
export interface LanguageSwitcherProps {
|
||||||
type: "mobileHeader" | "desktopHeader" | "footer"
|
type: LanguageSwitcherTypes
|
||||||
urls: LanguageSwitcherData
|
urls: LanguageSwitcherData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LanguageSwitcherContentProps {
|
||||||
|
urls: LanguageSwitcherData
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanguageSwitcherContainerProps {
|
||||||
|
type: LanguageSwitcherTypes
|
||||||
|
children: ReactElement<any, any>
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user