From ca4521e4a13f4268fd9310cc898419512b3538c1 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 13 Sep 2024 09:37:00 +0200 Subject: [PATCH 01/10] feat(SW-378): Added close button to footer language switcher and made it slide up from below --- .../LanguageSwitcherContainer/index.tsx | 63 ++++++++++++++++ .../languageSwitcherContainer.module.css | 74 +++++++++++++++++++ .../LanguageSwitcherContent/index.tsx | 28 +------ .../languageSwitcherContent.module.css | 19 ----- components/LanguageSwitcher/index.tsx | 34 +++++++-- .../languageSwitcher.module.css | 34 +++++++-- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + .../languageSwitcher/languageSwitcher.ts | 21 +++++- 13 files changed, 223 insertions(+), 56 deletions(-) create mode 100644 components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx create mode 100644 components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css diff --git a/components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx b/components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx new file mode 100644 index 000000000..ada415172 --- /dev/null +++ b/components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx @@ -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 ( +
+ {isMobileHeader ? ( +
+ +
+ ) : null} + {isFooter ? ( +
+ +
+ ) : null} + {children} +
+ ) +} diff --git a/components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css b/components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css new file mode 100644 index 000000000..1f8983efa --- /dev/null +++ b/components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css @@ -0,0 +1,74 @@ +.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); +} + +.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 8px 16px; + 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: 32px; +} + +.bar::after, +.bar::before { + content: ""; + left: 0; + position: absolute; + top: 0; + transform-origin: 50% 50%; + width: 32px; +} + +.bar { + background: transparent; +} + +.bar::after { + transform: rotate(-45deg); +} + +.bar::before { + transform: rotate(45deg); +} + +@media screen and (min-width: 768px) { + .closeWrapper { + display: none; + } +} diff --git a/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx b/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx index 9aebfa711..3533ecb93 100644 --- a/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx +++ b/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx @@ -3,9 +3,8 @@ import { useIntl } from "react-intl" 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 Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" @@ -13,38 +12,19 @@ import { useTrapFocus } from "@/hooks/useTrapFocus" import styles from "./languageSwitcherContent.module.css" -import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown" -import type { LanguageSwitcherProps } from "@/types/components/languageSwitcher/languageSwitcher" +import type { LanguageSwitcherContentProps } from "@/types/components/languageSwitcher/languageSwitcher" export default function LanguageSwitcherContent({ urls, - type, -}: LanguageSwitcherProps) { +}: LanguageSwitcherContentProps) { const intl = useIntl() const currentLanguage = useLang() - const { toggleDropdown } = useDropdownStore() + const languageSwitcherRef = useTrapFocus() const urlKeys = Object.keys(urls) as Lang[] - const position = - type === "footer" - ? DropdownTypeEnum.FooterLanguageSwitcher - : DropdownTypeEnum.HamburgerMenu return (
- {type === "mobileHeader" ? ( -
- -
- ) : null} -
{intl.formatMessage({ id: "Select your language" })} diff --git a/components/LanguageSwitcher/LanguageSwitcherContent/languageSwitcherContent.module.css b/components/LanguageSwitcher/LanguageSwitcherContent/languageSwitcherContent.module.css index 465b39c23..24e3febb8 100644 --- a/components/LanguageSwitcher/LanguageSwitcherContent/languageSwitcherContent.module.css +++ b/components/LanguageSwitcher/LanguageSwitcherContent/languageSwitcherContent.module.css @@ -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 { display: grid; gap: var(--Spacing-x3); diff --git a/components/LanguageSwitcher/index.tsx b/components/LanguageSwitcher/index.tsx index 65888d578..7a5ecb6b2 100644 --- a/components/LanguageSwitcher/index.tsx +++ b/components/LanguageSwitcher/index.tsx @@ -1,5 +1,6 @@ "use client" +import { useEffect } from "react" import { useIntl } from "react-intl" import { languages } from "@/constants/languages" @@ -9,13 +10,17 @@ import { ChevronDownIcon, GlobeIcon } from "@/components/Icons" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import useLang from "@/hooks/useLang" +import LanguageSwitcherContainer from "./LanguageSwitcherContainer" import LanguageSwitcherContent from "./LanguageSwitcherContent" import { languageSwitcherVariants } from "./variants" import styles from "./languageSwitcher.module.css" 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({ urls, @@ -30,8 +35,11 @@ export default function LanguageSwitcher({ isHeaderLanguageSwitcherMobileOpen, } = useDropdownStore() - const position = type === "footer" ? "footer" : "header" - const color = type === "footer" ? "pale" : "burgundy" + const isFooter = type === LanguageSwitcherTypesEnum.Footer + const isHeader = !isFooter + + const position = isFooter ? "footer" : "header" + const color = isFooter ? "pale" : "burgundy" const dropdownType = { footer: DropdownTypeEnum.FooterLanguageSwitcher, @@ -40,8 +48,8 @@ export default function LanguageSwitcher({ }[type] const isLanguageSwitcherOpen = - (type === "footer" && isFooterLanguageSwitcherOpen) || - (type !== "footer" && + (isFooter && isFooterLanguageSwitcherOpen) || + (isHeader && (isHeaderLanguageSwitcherOpen || isHeaderLanguageSwitcherMobileOpen)) 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 }) return ( @@ -78,7 +98,9 @@ export default function LanguageSwitcher({ className={`${styles.dropdown} ${isLanguageSwitcherOpen ? styles.isExpanded : ""}`} > {isLanguageSwitcherOpen ? ( - + + + ) : null}
diff --git a/components/LanguageSwitcher/languageSwitcher.module.css b/components/LanguageSwitcher/languageSwitcher.module.css index 6c85a22aa..4cde01f31 100644 --- a/components/LanguageSwitcher/languageSwitcher.module.css +++ b/components/LanguageSwitcher/languageSwitcher.module.css @@ -31,20 +31,36 @@ .dropdown { position: fixed; - top: var(--main-menu-mobile-height); - right: -100vw; - bottom: 0; width: 100%; background-color: var(--Base-Surface-Primary-light-Normal); - transition: right 0.3s; 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; 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) { .languageSwitcher { position: relative; @@ -81,10 +97,16 @@ } .bottom .dropdown { - top: auto; + transition: none; + height: auto; + left: -100%; bottom: 2.25rem; } + .bottom .dropdown.isExpanded { + display: block; + } + .bottom .dropdown::before { top: 100%; } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index fc8186ab8..c6f01ae24 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -97,6 +97,7 @@ "Log in here": "Log ind her", "Log in/Join": "Log på/Tilmeld dig", "Log out": "Log ud", + "Main menu": "Hovedmenu", "Manage preferences": "Administrer præferencer", "Meetings & Conferences": "Møder & Konferencer", "Member price": "Medlemspris", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 9092608b6..9e989bb54 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -95,6 +95,7 @@ "Log in here": "Hier einloggen", "Log in/Join": "Log in/Anmelden", "Log out": "Ausloggen", + "Main menu": "Hauptmenü", "Manage preferences": "Verwalten von Voreinstellungen", "Member price": "Mitgliederpreis", "Member price from": "Mitgliederpreis ab", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index d08f7fd8b..838d32580 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -101,6 +101,7 @@ "Log in here": "Log in here", "Log in/Join": "Log in/Join", "Log out": "Log out", + "Main menu": "Main menu", "Manage preferences": "Manage preferences", "Meetings & Conferences": "Meetings & Conferences", "Member price": "Member price", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index b1d91abe3..0b9072373 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -96,6 +96,7 @@ "Log in here": "Kirjaudu sisään", "Log in/Join": "Kirjaudu sisään/Liittyä", "Log out": "Kirjaudu ulos", + "Main menu": "Päävalikko", "Manage preferences": "Asetusten hallinta", "Meetings & Conferences": "Kokoukset & Konferenssit", "Member price": "Jäsenhinta", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 9d66f5df8..bd0540e9d 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -96,6 +96,7 @@ "Log in here": "Logg inn her", "Log in/Join": "Logg på/Bli med", "Log out": "Logg ut", + "Main menu": "Hovedmeny", "Manage preferences": "Administrer preferanser", "Meetings & Conferences": "Møter & Konferanser", "Member price": "Medlemspris", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index be1e56601..60f1881cb 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -98,6 +98,7 @@ "Log in here": "Logga in här", "Log in/Join": "Logga in/Gå med", "Log out": "Logga ut", + "Main menu": "Huvudmeny", "Manage preferences": "Hantera inställningar", "Meetings & Conferences": "Möten & Konferenser", "Member price": "Medlemspris", diff --git a/types/components/languageSwitcher/languageSwitcher.ts b/types/components/languageSwitcher/languageSwitcher.ts index 6672fbe83..312d28712 100644 --- a/types/components/languageSwitcher/languageSwitcher.ts +++ b/types/components/languageSwitcher/languageSwitcher.ts @@ -1,6 +1,25 @@ +import { ReactElement } from "react" + import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" +export enum LanguageSwitcherTypesEnum { + MobileHeader = "mobileHeader", + DesktopHeader = "desktopHeader", + Footer = "footer", +} + +export type LanguageSwitcherTypes = `${LanguageSwitcherTypesEnum}` + export interface LanguageSwitcherProps { - type: "mobileHeader" | "desktopHeader" | "footer" + type: LanguageSwitcherTypes urls: LanguageSwitcherData } + +export interface LanguageSwitcherContentProps { + urls: LanguageSwitcherData +} + +export interface LanguageSwitcherContainerProps { + type: LanguageSwitcherTypes + children: ReactElement +} From a8cef1c6c588697566451b47f953ab0d2fcf719b Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 13 Sep 2024 09:39:45 +0200 Subject: [PATCH 02/10] feat(SW-378): Updated styling --- .../languageSwitcherContainer.module.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css b/components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css index 1f8983efa..1722db825 100644 --- a/components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css +++ b/components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css @@ -29,7 +29,7 @@ border: none; cursor: pointer; justify-self: flex-start; - padding: 11px 8px 16px; + padding: 11px var(--Spacing-x1) var(--Spacing-x2); user-select: none; } @@ -42,7 +42,7 @@ height: 3px; position: relative; transition: all 0.3s; - width: 32px; + width: var(--Spacing-x4); } .bar::after, @@ -52,7 +52,7 @@ position: absolute; top: 0; transform-origin: 50% 50%; - width: 32px; + width: var(--Spacing-x4); } .bar { From d555447467f83bad85f80c680c2a5e3ceb1b2164 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 13 Sep 2024 16:46:09 +0200 Subject: [PATCH 03/10] fix(SW-378): remove unused css --- .../languageSwitcherContainer.module.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css b/components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css index 1722db825..e683a5e26 100644 --- a/components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css +++ b/components/LanguageSwitcher/LanguageSwitcherContainer/languageSwitcherContainer.module.css @@ -6,10 +6,6 @@ .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; From 6cfc79f8b5570d321bab9bf7b250e1811e3848c0 Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Mon, 16 Sep 2024 07:09:47 +0000 Subject: [PATCH 04/10] Merged in feat/booking-flow-documentation (pull request #581) Add readme for booking flow * Add readme for booking flow Approved-by: Fredrik Thorsson --- .../(public)/hotelreservation/README.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/README.md diff --git a/app/[lang]/(live)/(public)/hotelreservation/README.md b/app/[lang]/(live)/(public)/hotelreservation/README.md new file mode 100644 index 000000000..fdc0f1ad4 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/README.md @@ -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. From 3e08b3357b95dd081af5f461131fd9b15daa83ce Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Wed, 11 Sep 2024 11:31:42 +0200 Subject: [PATCH 05/10] feat:(SW-219): content card component WIP --- .../HotelPage/Rooms/RoomCard/index.tsx | 2 +- .../HotelPage/hotelPage.module.css | 7 ++ components/ContentType/HotelPage/index.tsx | 30 +++++++ .../ContentCard/contentCard.module.css | 51 +++++++++++ .../TempDesignSystem/ContentCard/index.tsx | 85 +++++++++++++++++++ .../TempDesignSystem/ContentCard/variants.ts | 15 ++++ types/components/contentCard.ts | 26 ++++++ 7 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 components/TempDesignSystem/ContentCard/contentCard.module.css create mode 100644 components/TempDesignSystem/ContentCard/index.tsx create mode 100644 components/TempDesignSystem/ContentCard/variants.ts create mode 100644 types/components/contentCard.ts diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx index 158fb05ad..4e6c6624d 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx +++ b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx @@ -74,7 +74,7 @@ export function RoomCard({ onClick={handleRoomCtaClick} > {intl.formatMessage({ id: "See room details" })} - + diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index ceea1bfbe..f2f0377b6 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -63,3 +63,10 @@ align-items: end; } } + +/* Add this to your existing styles */ +.contentCardContainer { + margin: var(--Spacing-x4) 0; + display: grid; + gap: var(--Spacing-x4); +} diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index f37750db4..3737859fb 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -1,6 +1,8 @@ import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" +import ContentCard from "@/components/TempDesignSystem/ContentCard" + import { MOCK_FACILITIES } from "./Facilities/mockData" import { setActivityCard } from "./Facilities/utils" import DynamicMap from "./Map/DynamicMap" @@ -65,6 +67,34 @@ export default async function HotelPage() { + + {/* Add ContentCard here */} + + + {/* Example of ContentCard with SidePeek */} + {googleMapsApiKey ? ( <> diff --git a/components/TempDesignSystem/ContentCard/contentCard.module.css b/components/TempDesignSystem/ContentCard/contentCard.module.css new file mode 100644 index 000000000..e1fe789e0 --- /dev/null +++ b/components/TempDesignSystem/ContentCard/contentCard.module.css @@ -0,0 +1,51 @@ +.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); +} + +.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-x2); + align-items: flex-start; + padding: var(--Spacing-x4); +} + +.description { + color: var(--Base-Text-Medium-contrast); +} + +.ctaContainer { + display: flex; + gap: var(--Spacing-x2); + margin-top: var(--Spacing-x2); +} + +.sidePeekCTA { + /* TODO: Create ticket to remove padding on "link" buttons, + align w. design on this. */ + padding: 0 !important; +} diff --git a/components/TempDesignSystem/ContentCard/index.tsx b/components/TempDesignSystem/ContentCard/index.tsx new file mode 100644 index 000000000..d65542389 --- /dev/null +++ b/components/TempDesignSystem/ContentCard/index.tsx @@ -0,0 +1,85 @@ +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, + primaryCTA, + secondaryCTA, + sidePeekCTA, + backgroundImage, + style = "default", + className, +}: ContentCardProps) { + const cardClasses = contentCardVariants({ style, className }) + + return ( +
+ {backgroundImage && ( +
+ +
+ )} +
+ + {title} + + {description} + {sidePeekCTA ? ( + + ) : ( +
+ {primaryCTA && ( + + )} + {secondaryCTA && ( + + )} +
+ )} +
+
+ ) +} diff --git a/components/TempDesignSystem/ContentCard/variants.ts b/components/TempDesignSystem/ContentCard/variants.ts new file mode 100644 index 000000000..1fda9c69d --- /dev/null +++ b/components/TempDesignSystem/ContentCard/variants.ts @@ -0,0 +1,15 @@ +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, + }, + }, + defaultVariants: { + style: "default", + }, +}) diff --git a/types/components/contentCard.ts b/types/components/contentCard.ts new file mode 100644 index 000000000..a15e1edd5 --- /dev/null +++ b/types/components/contentCard.ts @@ -0,0 +1,26 @@ +import { VariantProps } from "class-variance-authority" + +import { contentCardVariants } from "@/components/TempDesignSystem/ContentCard/variants" + +export interface CTA { + label: string + href: string + openInNewTab?: boolean +} + +export interface SidePeekCTA { + label: string + // onClick: () => void + onClick: boolean +} + +export interface ContentCardProps + extends VariantProps { + title: string + description: string + primaryCTA?: CTA + secondaryCTA?: CTA + sidePeekCTA?: SidePeekCTA + backgroundImage?: string + className?: string +} From 3eb7e5f65309d07635750a11d8fb0b4cadb88c52 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Wed, 11 Sep 2024 15:40:41 +0200 Subject: [PATCH 06/10] feat(SW-219): add ability to always stack buttons --- components/ContentType/HotelPage/index.tsx | 11 +++---- .../ContentCard/contentCard.module.css | 30 +++++++++++++++---- .../TempDesignSystem/ContentCard/index.tsx | 17 +++++++++-- .../TempDesignSystem/ContentCard/variants.ts | 5 ++++ types/components/contentCard.ts | 1 + 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 3737859fb..9770cfe33 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -67,11 +67,12 @@ export default async function HotelPage() { - - {/* Add ContentCard here */} + {/* NOTE: These are added here for testing. Remove before PR */} + {/* Example of ContentCard with Button CTA's */} {/* Example of ContentCard with SidePeek */} {googleMapsApiKey ? ( diff --git a/components/TempDesignSystem/ContentCard/contentCard.module.css b/components/TempDesignSystem/ContentCard/contentCard.module.css index e1fe789e0..9a90d97c7 100644 --- a/components/TempDesignSystem/ContentCard/contentCard.module.css +++ b/components/TempDesignSystem/ContentCard/contentCard.module.css @@ -14,6 +14,11 @@ 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 */ @@ -29,9 +34,9 @@ .content { display: flex; flex-direction: column; - gap: var(--Spacing-x2); + gap: var(--Spacing-x-one-and-half); align-items: flex-start; - padding: var(--Spacing-x4); + padding: var(--Spacing-x2) var(--Spacing-x3); } .description { @@ -39,9 +44,24 @@ } .ctaContainer { - display: flex; - gap: var(--Spacing-x2); - margin-top: var(--Spacing-x2); + 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 { diff --git a/components/TempDesignSystem/ContentCard/index.tsx b/components/TempDesignSystem/ContentCard/index.tsx index d65542389..0185ad239 100644 --- a/components/TempDesignSystem/ContentCard/index.tsx +++ b/components/TempDesignSystem/ContentCard/index.tsx @@ -21,9 +21,10 @@ export default function ContentCard({ sidePeekCTA, backgroundImage, style = "default", + alwaysStack = false, className, }: ContentCardProps) { - const cardClasses = contentCardVariants({ style, className }) + const cardClasses = contentCardVariants({ style, alwaysStack, className }) return (
@@ -58,7 +59,12 @@ export default function ContentCard({ ) : (
{primaryCTA && ( -
- {/* NOTE: These are added here for testing. Remove before PR */} - {/* Example of ContentCard with Button CTA's */} - - - {/* Example of ContentCard with SidePeek */} - {googleMapsApiKey ? ( <> diff --git a/components/TempDesignSystem/ContentCard/index.tsx b/components/TempDesignSystem/ContentCard/index.tsx index 0185ad239..bdde9de6f 100644 --- a/components/TempDesignSystem/ContentCard/index.tsx +++ b/components/TempDesignSystem/ContentCard/index.tsx @@ -16,9 +16,9 @@ import type { ContentCardProps } from "@/types/components/contentCard" export default function ContentCard({ title, description, - primaryCTA, - secondaryCTA, - sidePeekCTA, + primaryButton, + secondaryButton, + sidePeekButton, backgroundImage, style = "default", alwaysStack = false, @@ -31,8 +31,8 @@ export default function ContentCard({ {backgroundImage && (
{description} - {sidePeekCTA ? ( + {!!sidePeekButton ? ( ) : (
- {primaryCTA && ( + {primaryButton && ( )} - {secondaryCTA && ( + {secondaryButton && ( )} diff --git a/lib/graphql/Fragments/Blocks/Card.graphql b/lib/graphql/Fragments/Blocks/Card.graphql index 0fb2e416c..d10ee4d5c 100644 --- a/lib/graphql/Fragments/Blocks/Card.graphql +++ b/lib/graphql/Fragments/Blocks/Card.graphql @@ -1,30 +1,13 @@ fragment CardBlock on Card { + is_content_card heading body_text background_image scripted_top_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_secondary_button + has_sidepeek_button primary_button { is_contentstack_link 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 { locale uid diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index b6b920a78..1aa324041 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -68,6 +68,7 @@ export const contentPageDynamicContent = z.object({ export const cardBlock = z.object({ __typename: z.literal(CardsGridEnum.Card), + isContentCard: z.boolean(), heading: z.string().nullable(), body_text: z.string().nullable(), background_image: z.any(), @@ -88,6 +89,11 @@ export const cardBlock = z.object({ isExternal: z.boolean(), }) .optional(), + sidePeekButton: z + .object({ + title: z.string(), + }) + .optional(), system: z.object({ locale: z.nativeEnum(Lang), uid: z.string(), diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index c37adbd68..16ff7e7c1 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -102,6 +102,7 @@ export const contentPageQueryRouter = router({ case CardsGridEnum.Card: return { ...card, + isContentCard: !!card.is_content_card, backgroundImage: makeImageVaultImage( card.background_image ), @@ -111,6 +112,14 @@ export const contentPageQueryRouter = router({ secondaryButton: card.has_secondary_button ? makeButtonObject(card.secondary_button) : 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: return { diff --git a/types/components/contentCard.ts b/types/components/contentCard.ts index cc5d27279..8e05bfbdc 100644 --- a/types/components/contentCard.ts +++ b/types/components/contentCard.ts @@ -1,27 +1,21 @@ import { VariantProps } from "class-variance-authority" +import { CardProps } from "@/components/TempDesignSystem/Card/card" import { contentCardVariants } from "@/components/TempDesignSystem/ContentCard/variants" -export interface CTA { - label: string - href: string - openInNewTab?: boolean -} +import { ImageVaultAsset } from "@/types/components/imageVault" -export interface SidePeekCTA { - label: string - // onClick: () => void - // TODO: change back to function. - onClick: boolean +export interface SidePeekButton { + title: string } export interface ContentCardProps extends VariantProps { title: string description: string - primaryCTA?: CTA - secondaryCTA?: CTA - sidePeekCTA?: SidePeekCTA - backgroundImage?: string + primaryButton?: CardProps["primaryButton"] + secondaryButton?: CardProps["secondaryButton"] + sidePeekButton?: SidePeekButton + backgroundImage?: ImageVaultAsset className?: string } From 68437356f002a1036b2d1b841f619e79e488b018 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 13 Sep 2024 06:16:30 +0200 Subject: [PATCH 08/10] fix(SW-219): remove uneeded css --- components/ContentType/HotelPage/hotelPage.module.css | 7 ------- 1 file changed, 7 deletions(-) diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index f2f0377b6..ceea1bfbe 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -63,10 +63,3 @@ align-items: end; } } - -/* Add this to your existing styles */ -.contentCardContainer { - margin: var(--Spacing-x4) 0; - display: grid; - gap: var(--Spacing-x4); -} From 31515721086e9e545136ce6abce6ae0e7c05a98b Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Mon, 16 Sep 2024 08:53:44 +0200 Subject: [PATCH 09/10] fix(SW-219): review comments --- components/Content/Blocks/CardsGrid/index.tsx | 51 +++++++++---------- types/components/contentCard.ts | 4 +- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/components/Content/Blocks/CardsGrid/index.tsx b/components/Content/Blocks/CardsGrid/index.tsx index a647560f7..a03666bf3 100644 --- a/components/Content/Blocks/CardsGrid/index.tsx +++ b/components/Content/Blocks/CardsGrid/index.tsx @@ -22,34 +22,29 @@ export default function CardsGrid({ {cards_grid.cards.map((card) => { switch (card.__typename) { - case CardsGridEnum.Card: { - if (card.isContentCard) { - return ( - - ) - } else { - return ( - - ) - } - } + case CardsGridEnum.Card: + return card.isContentCard ? ( + + ) : ( + + ) case CardsGridEnum.LoyaltyCard: return ( Date: Mon, 16 Sep 2024 15:15:55 +0200 Subject: [PATCH 10/10] fix: change procedure for booking widget toggle call to not require cms uid --- server/routers/contentstack/bookingwidget/query.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/contentstack/bookingwidget/query.ts b/server/routers/contentstack/bookingwidget/query.ts index 89f8d68b6..9b67dba16 100644 --- a/server/routers/contentstack/bookingwidget/query.ts +++ b/server/routers/contentstack/bookingwidget/query.ts @@ -8,7 +8,7 @@ import { GetLoyaltyPageSettings, } from "@/lib/graphql/Query/BookingWidgetToggle.graphql" import { request } from "@/lib/graphql/request" -import { contentstackExtendedProcedureUID, router } from "@/server/trpc" +import { contentstackBaseProcedure, router } from "@/server/trpc" import { generateTag } from "@/utils/generateTag" @@ -21,7 +21,7 @@ import { affix as bookingwidgetAffix } from "./utils" import { ContentTypeEnum } from "@/types/requests/contentType" export const bookingwidgetQueryRouter = router({ - getToggle: contentstackExtendedProcedureUID.query(async ({ ctx }) => { + getToggle: contentstackBaseProcedure.query(async ({ ctx }) => { const failedResponse = { hideBookingWidget: false } const { contentType, uid, lang } = ctx