From 39e84688864a8470496507bd3d54be6200fb07b9 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Tue, 20 Aug 2024 10:24:11 +0200 Subject: [PATCH 001/319] feat(SW-185): Created mockup for footer --- app/[lang]/(live)/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index 35144b8d9..83db732d1 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -7,8 +7,8 @@ import TrpcProvider from "@/lib/trpc/Provider" import TokenRefresher from "@/components/Auth/TokenRefresher" import AdobeSDKScript from "@/components/Current/AdobeSDKScript" -import Footer from "@/components/Current/Footer" import VwoScript from "@/components/Current/VwoScript" +import Footer from "@/components/Footer" import { preloadUserTracking } from "@/components/TrackingSDK" import { getIntl } from "@/i18n" import ServerIntlProvider from "@/i18n/Provider" From cfcb05a70a4ee397e1348d3c9b4d0744597a5cec Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Tue, 20 Aug 2024 10:24:44 +0200 Subject: [PATCH 002/319] feat(SW-185): basic setup for new footer with mocked data --- components/Footer/Details/details.module.css | 13 +++ components/Footer/Details/index.tsx | 25 +++++ .../Footer/Navigation/MainNav/index.tsx | 30 ++++++ .../Navigation/MainNav/mainnav.module.css | 23 ++++ .../Footer/Navigation/SecondaryNav/index.tsx | 35 ++++++ .../SecondaryNav/secondarynav.module.css | 25 +++++ components/Footer/Navigation/index.tsx | 19 ++++ .../Footer/Navigation/navigation.module.css | 11 ++ components/Footer/index.tsx | 11 ++ components/Footer/mockedData.ts | 101 ++++++++++++++++++ 10 files changed, 293 insertions(+) create mode 100644 components/Footer/Details/details.module.css create mode 100644 components/Footer/Details/index.tsx create mode 100644 components/Footer/Navigation/MainNav/index.tsx create mode 100644 components/Footer/Navigation/MainNav/mainnav.module.css create mode 100644 components/Footer/Navigation/SecondaryNav/index.tsx create mode 100644 components/Footer/Navigation/SecondaryNav/secondarynav.module.css create mode 100644 components/Footer/Navigation/index.tsx create mode 100644 components/Footer/Navigation/navigation.module.css create mode 100644 components/Footer/index.tsx create mode 100644 components/Footer/mockedData.ts diff --git a/components/Footer/Details/details.module.css b/components/Footer/Details/details.module.css new file mode 100644 index 000000000..19f1494fe --- /dev/null +++ b/components/Footer/Details/details.module.css @@ -0,0 +1,13 @@ +.details { + background: var(--Main-Grey-100); + color: var(--Main-Grey-White); + padding: var(--Spacing-x5) var(--Spacing-x5) var(--Spacing-x9) + var(--Spacing-x5); +} + +.imageContainer { + display: flex; + justify-content: space-between; + border-bottom: 1px solid rgba(227, 217, 209, 0.2); + padding: 0 0 var(--Spacing-x2) 0; +} diff --git a/components/Footer/Details/index.tsx b/components/Footer/Details/index.tsx new file mode 100644 index 000000000..061105620 --- /dev/null +++ b/components/Footer/Details/index.tsx @@ -0,0 +1,25 @@ +import Image from "@/components/Image" + +import styles from "./details.module.css" + +export default async function FooterDetails() { + return ( +
+
+ Scandic Hotels logo +
+ +
+
+
+ ) +} diff --git a/components/Footer/Navigation/MainNav/index.tsx b/components/Footer/Navigation/MainNav/index.tsx new file mode 100644 index 000000000..f3158ba4f --- /dev/null +++ b/components/Footer/Navigation/MainNav/index.tsx @@ -0,0 +1,30 @@ +import { ArrowRightIcon } from "@/components/Icons" +import Link from "@/components/TempDesignSystem/Link" + +import styles from "./mainnav.module.css" + +export default async function FooterMainNav({ + mainLinks, +}: { + mainLinks: Array<{ id: string; href: string; title: string }> +}) { + return ( + + ) +} diff --git a/components/Footer/Navigation/MainNav/mainnav.module.css b/components/Footer/Navigation/MainNav/mainnav.module.css new file mode 100644 index 000000000..b4cbffc88 --- /dev/null +++ b/components/Footer/Navigation/MainNav/mainnav.module.css @@ -0,0 +1,23 @@ +.mainNavigation { + max-width: 360px; + width: 100%; +} + +.mainNavigationList { + list-style: none; +} + +.mainNavigationItem { + padding: var(--Spacing-x3) 0; + border-bottom: 1px solid var(--Scandic-Peach-20); + &:last-child { + border-bottom: 0; + } +} + +.mainNavigationLink { + font-size: 20px; + font-weight: 500; + display: flex; + justify-content: space-between; +} diff --git a/components/Footer/Navigation/SecondaryNav/index.tsx b/components/Footer/Navigation/SecondaryNav/index.tsx new file mode 100644 index 000000000..914dcf123 --- /dev/null +++ b/components/Footer/Navigation/SecondaryNav/index.tsx @@ -0,0 +1,35 @@ +import Link from "@/components/TempDesignSystem/Link" + +import styles from "./secondarynav.module.css" + +export default async function FooterSecondaryNav({ + secondaryLinks, +}: { + secondaryLinks: Record< + string, + { title: string; links: Array<{ id: string; href: string; title: string }> } + > +}) { + return ( +
+ {Object.entries(secondaryLinks).map(([key, group]) => ( + + ))} +
+ ) +} diff --git a/components/Footer/Navigation/SecondaryNav/secondarynav.module.css b/components/Footer/Navigation/SecondaryNav/secondarynav.module.css new file mode 100644 index 000000000..3d7d6a6ec --- /dev/null +++ b/components/Footer/Navigation/SecondaryNav/secondarynav.module.css @@ -0,0 +1,25 @@ +.secondaryNavigation { + display: flex; + gap: 80px; +} + +.secondaryNavigationList { + display: flex; + flex-direction: column; + list-style: none; + gap: var(--Spacing-x3); +} + +.secondaryNavigationGroup { + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); +} + +.secondaryNavigationGroupTitle { + font-size: 14px; + color: var(--Scandic-Peach-80); + font-weight: 500; + font-family: var(--typography-Body-Bold-fontFamily); + margin: 0; +} diff --git a/components/Footer/Navigation/index.tsx b/components/Footer/Navigation/index.tsx new file mode 100644 index 000000000..13b0e0f47 --- /dev/null +++ b/components/Footer/Navigation/index.tsx @@ -0,0 +1,19 @@ +import MaxWidth from "@/components/MaxWidth" + +import { navigationData } from "../mockedData" +import FooterMainNav from "./MainNav" +import FooterSecondaryNav from "./SecondaryNav" + +import styles from "./navigation.module.css" + +export default async function FooterNavigation() { + const { mainLinks, secondaryLinks } = navigationData + return ( +
+ + + + +
+ ) +} diff --git a/components/Footer/Navigation/navigation.module.css b/components/Footer/Navigation/navigation.module.css new file mode 100644 index 000000000..fa45a3d99 --- /dev/null +++ b/components/Footer/Navigation/navigation.module.css @@ -0,0 +1,11 @@ +.section { + background: var(--Scandic-Brand-Pale-Peach); + padding: 160px var(--Spacing-x9); + color: var(--Scandic-Brand-Burgundy); +} + +.maxWidth { + margin: 0 auto; + display: flex; + justify-content: space-between; +} diff --git a/components/Footer/index.tsx b/components/Footer/index.tsx new file mode 100644 index 000000000..4ce45da0f --- /dev/null +++ b/components/Footer/index.tsx @@ -0,0 +1,11 @@ +import FooterDetails from "./Details" +import FooterNavigation from "./Navigation" + +export default async function Footer() { + return ( +
+ + +
+ ) +} diff --git a/components/Footer/mockedData.ts b/components/Footer/mockedData.ts new file mode 100644 index 000000000..491bc6a56 --- /dev/null +++ b/components/Footer/mockedData.ts @@ -0,0 +1,101 @@ +export const navigationData = { + mainLinks: [ + { + title: "Travel guides", + href: "/travel-guides", + id: "travel-guides", + }, + { + title: "New hotels", + href: "/new-hotels", + id: "new-hotels", + }, + { + title: "Accessibililty", + href: "/accessibility", + id: "accessibility", + }, + { + title: "Sustanability", + href: "/sustainability", + id: "sustainability", + }, + ], + secondaryLinks: { + app: { + title: "Scandic App", + links: [ + { + title: "App Store", + href: "https://apps.apple.com/se/app/scandic-hotels/id1020208712", + id: "app-store", + }, + { + title: "Google Play", + href: "https://play.google.com/store/apps/details?id=com.scandichotels.scandichotels", + id: "google-play", + }, + ], + }, + customerService: { + title: "Customer service", + links: [ + { + title: "Contact us", + href: "/contact-us", + id: "contact-us", + }, + { + title: "Frequntly asked questions", + href: "/frequently-asked-questions", + id: "frequently-asked-questions", + }, + { + title: "Rates and policys", + href: "/rates-and-policies", + id: "rates-and-policies", + }, + { + title: "Terms and conditions", + href: "/terms-and-conditions", + id: "terms-and-conditions", + }, + ], + }, + about: { + title: "About Scandic Hotels", + links: [ + { + title: "Scandic Group", + href: "/scandic-group", + id: "scandic-group", + }, + { + title: "Investors", + href: "/investors", + id: "investors", + }, + { + title: "Press", + href: "/press", + id: "press", + }, + { + title: "Sponsors", + href: "/sponsors", + id: "sponsors", + }, + { + title: "Partners", + href: "/partners", + id: "partners", + }, + { + title: "Career", + href: "/career", + id: "career", + }, + ], + }, + }, +} From 9d416f593dafdd44acc06c44418602961866af29 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 21 Aug 2024 14:08:15 +0200 Subject: [PATCH 003/319] feat(SW-185): Footer mobile adjustments --- components/Footer/Details/details.module.css | 47 ++++++++++- components/Footer/Details/index.tsx | 78 +++++++++++++++---- .../Navigation/MainNav/mainnav.module.css | 10 ++- .../SecondaryNav/secondarynav.module.css | 12 ++- .../Footer/Navigation/navigation.module.css | 12 ++- components/Footer/mockedData.ts | 64 +++++++++++++++ components/Icons/Facebook.tsx | 27 +++++++ components/Icons/Instagram.tsx | 27 +++++++ components/Icons/get-icon-by-icon-name.ts | 9 +++ types/components/icon.ts | 3 + 10 files changed, 269 insertions(+), 20 deletions(-) create mode 100644 components/Icons/Facebook.tsx create mode 100644 components/Icons/Instagram.tsx diff --git a/components/Footer/Details/details.module.css b/components/Footer/Details/details.module.css index 19f1494fe..a6b184ea2 100644 --- a/components/Footer/Details/details.module.css +++ b/components/Footer/Details/details.module.css @@ -1,13 +1,52 @@ .details { background: var(--Main-Grey-100); color: var(--Main-Grey-White); - padding: var(--Spacing-x5) var(--Spacing-x5) var(--Spacing-x9) - var(--Spacing-x5); + padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7); } -.imageContainer { +.topContainer { display: flex; justify-content: space-between; - border-bottom: 1px solid rgba(227, 217, 209, 0.2); padding: 0 0 var(--Spacing-x2) 0; + margin-bottom: var(--Spacing-x2); +} + +.bottomContainer { + display: flex; + justify-content: space-between; + flex-direction: column-reverse; +} + +.socialNav { + display: flex; + gap: var(--Spacing-x2); +} + +.navigationContainer { + display: flex; + justify-content: space-between; + margin-bottom: var(--Spacing-x2); + padding-bottom: var(--Spacing-x2); + border-bottom: 1px solid #e3d9d120; +} + +.navigation { + display: flex; + gap: var(--Spacing-x2); +} + +@media screen and (min-width: 1367px) { + .details { + background: var(--Main-Grey-100); + color: var(--Main-Grey-White); + padding: var(--Spacing-x5) var(--Spacing-x5) var(--Spacing-x9); + } + .bottomContainer { + flex-direction: row; + } + .navigationContainer { + border-bottom: 0; + padding-bottom: 0; + margin-bottom: 0; + } } diff --git a/components/Footer/Details/index.tsx b/components/Footer/Details/index.tsx index 061105620..3cb94e99a 100644 --- a/components/Footer/Details/index.tsx +++ b/components/Footer/Details/index.tsx @@ -1,23 +1,75 @@ +import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher" +import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import Image from "@/components/Image" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" + +import { detailsData } from "../mockedData" import styles from "./details.module.css" +import { IconName } from "@/types/components/icon" + +function socialIcon(iconName: string): JSX.Element | null { + const SocialIcon = getIconByIconName(iconName as IconName) + return SocialIcon ? : {iconName} +} + export default async function FooterDetails() { return (
-
- Scandic Hotels logo -
- +
+ + Scandic Hotels logo + + +
+
+ + {detailsData.copyrightCompany}{" "} + {detailsData.copyrightInfo} + +
+ +
diff --git a/components/Footer/Navigation/MainNav/mainnav.module.css b/components/Footer/Navigation/MainNav/mainnav.module.css index b4cbffc88..f3c21e23e 100644 --- a/components/Footer/Navigation/MainNav/mainnav.module.css +++ b/components/Footer/Navigation/MainNav/mainnav.module.css @@ -1,5 +1,4 @@ .mainNavigation { - max-width: 360px; width: 100%; } @@ -10,6 +9,9 @@ .mainNavigationItem { padding: var(--Spacing-x3) 0; border-bottom: 1px solid var(--Scandic-Peach-20); + &:first-child { + padding-top: 0; + } &:last-child { border-bottom: 0; } @@ -21,3 +23,9 @@ display: flex; justify-content: space-between; } + +@media screen and (min-width: 1367px) { + .mainNavigation { + max-width: 360px; + } +} diff --git a/components/Footer/Navigation/SecondaryNav/secondarynav.module.css b/components/Footer/Navigation/SecondaryNav/secondarynav.module.css index 3d7d6a6ec..314f69c3e 100644 --- a/components/Footer/Navigation/SecondaryNav/secondarynav.module.css +++ b/components/Footer/Navigation/SecondaryNav/secondarynav.module.css @@ -1,6 +1,8 @@ .secondaryNavigation { display: flex; - gap: 80px; + flex-direction: column-reverse; + gap: var(--Spacing-x6); + margin-top: var(--Spacing-x6); } .secondaryNavigationList { @@ -23,3 +25,11 @@ font-family: var(--typography-Body-Bold-fontFamily); margin: 0; } + +@media screen and (min-width: 1367px) { + .secondaryNavigation { + margin-top: 0; + gap: 80px; + flex-direction: row; + } +} diff --git a/components/Footer/Navigation/navigation.module.css b/components/Footer/Navigation/navigation.module.css index fa45a3d99..c37481cbd 100644 --- a/components/Footer/Navigation/navigation.module.css +++ b/components/Footer/Navigation/navigation.module.css @@ -1,6 +1,6 @@ .section { background: var(--Scandic-Brand-Pale-Peach); - padding: 160px var(--Spacing-x9); + padding: var(--Spacing-x9) var(--Spacing-x2); color: var(--Scandic-Brand-Burgundy); } @@ -8,4 +8,14 @@ margin: 0 auto; display: flex; justify-content: space-between; + flex-direction: column; +} + +@media screen and (min-width: 1367px) { + .section { + padding: 160px var(--Spacing-x9); + } + .maxWidth { + flex-direction: row; + } } diff --git a/components/Footer/mockedData.ts b/components/Footer/mockedData.ts index 491bc6a56..8ae41a2df 100644 --- a/components/Footer/mockedData.ts +++ b/components/Footer/mockedData.ts @@ -99,3 +99,67 @@ export const navigationData = { }, }, } + +export const detailsData = { + copyrightCompany: "© 2024 Scandic AB", + copyrightInfo: "All rights reserved.", + social: { + links: [ + { + title: "Facebook", + href: "https://www.facebook.com/scandichotels/", + id: "facebook", + }, + { + title: "Instagram", + href: "https://www.instagram.com/scandichotels/", + id: "instagram", + }, + { + title: "Tripadvisor", + href: "https://www.tripadvisor.com/Hotel_Review-g297628-d1020208712-Reviews-Scandic_Hotels-Stockholm_Sweden.html", + id: "tripadvisor", + }, + ], + }, + links: [ + { + title: "Cookies", + href: "/cookies", + id: "cookies", + }, + { + title: "Privacy policy", + href: "/privacy", + id: "privacy", + }, + ], + languageSwitcher: { + urls: { + da: { + url: "https://www.scandichotels.com/da", + isExternal: true, + }, + de: { + url: "https://www.scandichotels.com/de", + isExternal: true, + }, + en: { + url: "https://www.scandichotels.com/en", + isExternal: true, + }, + fi: { + url: "https://www.scandichotels.com/fi", + isExternal: true, + }, + no: { + url: "https://www.scandichotels.com/no", + isExternal: true, + }, + sv: { + url: "https://www.scandichotels.com/sv", + isExternal: true, + }, + }, + }, +} diff --git a/components/Icons/Facebook.tsx b/components/Icons/Facebook.tsx new file mode 100644 index 000000000..20e7b6499 --- /dev/null +++ b/components/Icons/Facebook.tsx @@ -0,0 +1,27 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function FacebookIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + ) +} diff --git a/components/Icons/Instagram.tsx b/components/Icons/Instagram.tsx new file mode 100644 index 000000000..5297456d2 --- /dev/null +++ b/components/Icons/Instagram.tsx @@ -0,0 +1,27 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function InstagramIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + ) +} diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 897ca8b28..620792bae 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -1,5 +1,8 @@ import { FC } from "react" +import FacebookIcon from "./Facebook" +import InstagramIcon from "./Instagram" +import TripAdvisorIcon from "./TripAdvisor" import { AccessibilityIcon, AccountCircleIcon, @@ -77,6 +80,8 @@ export function getIconByIconName(icon?: IconName): FC | null { return ElectricBikeIcon case IconName.Email: return EmailIcon + case IconName.Facebook: + return FacebookIcon case IconName.Fitness: return FitnessIcon case IconName.Globe: @@ -87,6 +92,8 @@ export function getIconByIconName(icon?: IconName): FC | null { return ImageIcon case IconName.InfoCircle: return InfoCircleIcon + case IconName.Instagram: + return InstagramIcon case IconName.Location: return LocationIcon case IconName.Lock: @@ -105,6 +112,8 @@ export function getIconByIconName(icon?: IconName): FC | null { return PlusCircleIcon case IconName.Restaurant: return RestaurantIcon + case IconName.Tripadvisor: + return TripAdvisorIcon case IconName.TshirtWash: return TshirtWashIcon case IconName.Wifi: diff --git a/types/components/icon.ts b/types/components/icon.ts index ff76c28eb..7859d0039 100644 --- a/types/components/icon.ts +++ b/types/components/icon.ts @@ -25,11 +25,13 @@ export enum IconName { DoorOpen = "DoorOpen", ElectricBike = "ElectricBike", Email = "Email", + Facebook = "Facebook", Fitness = "Fitness", Globe = "Globe", House = "House", Image = "Image", InfoCircle = "InfoCircle", + Instagram = "Instagram", Location = "Location", Lock = "Lock", Parking = "Parking", @@ -39,6 +41,7 @@ export enum IconName { Phone = "Phone", PlusCircle = "PlusCircle", Restaurant = "Restaurant", + Tripadvisor = "Tripadvisor", TshirtWash = "TshirtWash", Wifi = "Wifi", } From e2d53c4e011420a83fca3c751f92d765c1d81e41 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 21 Aug 2024 14:24:06 +0200 Subject: [PATCH 004/319] feat(SW-185): Added comment for later work --- components/Footer/Details/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/Footer/Details/index.tsx b/components/Footer/Details/index.tsx index 3cb94e99a..ac43a04d0 100644 --- a/components/Footer/Details/index.tsx +++ b/components/Footer/Details/index.tsx @@ -65,10 +65,10 @@ export default async function FooterDetails() { {link.title} ))} - { - // This will be changed to the new LangueSwitcher that is done in the header branch, when implementing contentstack - } + { + // This will be changed to the new LangueSwitcher that is done in the header branch, when implementing contentstack + } From d72080ffe371bdac37409748f76e3dd2305d9d05 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 21 Aug 2024 14:51:33 +0200 Subject: [PATCH 005/319] feat(SW-185): Added images for app stores --- .../Footer/Navigation/SecondaryNav/index.tsx | 22 ++++++++++++++-- components/Footer/mockedData.ts | 7 ++++++ public/_static/img/app-store-badge.svg | 25 +++++++++++++++++++ public/_static/img/google-play-badge.svg | 24 ++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 public/_static/img/app-store-badge.svg create mode 100644 public/_static/img/google-play-badge.svg diff --git a/components/Footer/Navigation/SecondaryNav/index.tsx b/components/Footer/Navigation/SecondaryNav/index.tsx index 914dcf123..a6df1ddee 100644 --- a/components/Footer/Navigation/SecondaryNav/index.tsx +++ b/components/Footer/Navigation/SecondaryNav/index.tsx @@ -1,3 +1,4 @@ +import Image from "@/components/Image" import Link from "@/components/TempDesignSystem/Link" import styles from "./secondarynav.module.css" @@ -7,7 +8,15 @@ export default async function FooterSecondaryNav({ }: { secondaryLinks: Record< string, - { title: string; links: Array<{ id: string; href: string; title: string }> } + { + title: string + links: Array<{ + id: string + href: string + title: string + image?: { src: string } + }> + } > }) { return ( @@ -23,7 +32,16 @@ export default async function FooterSecondaryNav({ href={link.href} className={styles.secondaryNavigationLink} > - {link.title} + {link.image ? ( + {link.title} + ) : ( + link.title + )} ))} diff --git a/components/Footer/mockedData.ts b/components/Footer/mockedData.ts index 8ae41a2df..a9ba3f248 100644 --- a/components/Footer/mockedData.ts +++ b/components/Footer/mockedData.ts @@ -29,11 +29,18 @@ export const navigationData = { title: "App Store", href: "https://apps.apple.com/se/app/scandic-hotels/id1020208712", id: "app-store", + image: { + src: "/_static/img/app-store-badge.svg", + alt: "Download on the App Store", + }, }, { title: "Google Play", href: "https://play.google.com/store/apps/details?id=com.scandichotels.scandichotels", id: "google-play", + image: { + src: "/_static/img/google-play-badge.svg", + }, }, ], }, diff --git a/public/_static/img/app-store-badge.svg b/public/_static/img/app-store-badge.svg new file mode 100644 index 000000000..fe8aeaa23 --- /dev/null +++ b/public/_static/img/app-store-badge.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/google-play-badge.svg b/public/_static/img/google-play-badge.svg new file mode 100644 index 000000000..90936a6a4 --- /dev/null +++ b/public/_static/img/google-play-badge.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + From 72e28307deb3e732865e51be5356fb653496dbb9 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 22 Aug 2024 11:01:31 +0200 Subject: [PATCH 006/319] feat(SW-185): Fixing comments --- components/Footer/Details/details.module.css | 33 ++++++++++---- components/Footer/Details/index.tsx | 45 +++++++++++-------- .../Footer/Navigation/MainNav/index.tsx | 27 +++++------ .../Navigation/MainNav/mainnav.module.css | 2 - .../Footer/Navigation/SecondaryNav/index.tsx | 29 ++++++------ .../SecondaryNav/secondarynav.module.css | 1 - components/Footer/Navigation/index.tsx | 2 +- .../Footer/Navigation/navigation.module.css | 4 +- components/Footer/index.tsx | 2 +- components/Footer/mockedData.ts | 2 +- .../TempDesignSystem/Link/link.module.css | 4 ++ components/TempDesignSystem/Link/variants.ts | 1 + .../Text/Body/body.module.css | 14 ++++++ .../TempDesignSystem/Text/Body/index.tsx | 18 ++++---- .../TempDesignSystem/Text/Body/variants.ts | 3 ++ .../Text/Footnote/footnote.module.css | 14 ++++++ .../Text/Footnote/variants.ts | 3 ++ types/components/footer/navigation.ts | 26 +++++++++++ 18 files changed, 158 insertions(+), 72 deletions(-) create mode 100644 types/components/footer/navigation.ts diff --git a/components/Footer/Details/details.module.css b/components/Footer/Details/details.module.css index a6b184ea2..d8bd17d93 100644 --- a/components/Footer/Details/details.module.css +++ b/components/Footer/Details/details.module.css @@ -1,13 +1,13 @@ .details { - background: var(--Main-Grey-100); - color: var(--Main-Grey-White); - padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7); + background: var(--Main-Red-100); + color: var(--Main-Brand-PalePeach); + padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x6); } .topContainer { display: flex; justify-content: space-between; - padding: 0 0 var(--Spacing-x2) 0; + padding-bottom: var(--Spacing-x2); margin-bottom: var(--Spacing-x2); } @@ -27,21 +27,38 @@ justify-content: space-between; margin-bottom: var(--Spacing-x2); padding-bottom: var(--Spacing-x2); - border-bottom: 1px solid #e3d9d120; + border-bottom: 1px solid var(--Scandic-Peach-80); } .navigation { display: flex; - gap: var(--Spacing-x2); + gap: var(--Spacing-x1); +} + +.link { + &::after { + content: "·"; + margin-left: var(--Spacing-x1); + } + &:last-child { + &::after { + content: ""; + } + } +} + +.copyrightContainer { + display: flex; + gap: var(--Spacing-x1); } @media screen and (min-width: 1367px) { .details { - background: var(--Main-Grey-100); color: var(--Main-Grey-White); - padding: var(--Spacing-x5) var(--Spacing-x5) var(--Spacing-x9); + padding: var(--Spacing-x6) var(--Spacing-x5) var(--Spacing-x4); } .bottomContainer { + border-top: 1px solid var(--Scandic-Peach-80); flex-direction: row; } .navigationContainer { diff --git a/components/Footer/Details/index.tsx b/components/Footer/Details/index.tsx index ac43a04d0..3768005d6 100644 --- a/components/Footer/Details/index.tsx +++ b/components/Footer/Details/index.tsx @@ -3,6 +3,8 @@ import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import Image from "@/components/Image" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" +import { getLang } from "@/i18n/serverContext" import { detailsData } from "../mockedData" @@ -15,11 +17,13 @@ function socialIcon(iconName: string): JSX.Element | null { return SocialIcon ? : {iconName} } -export default async function FooterDetails() { +export default function FooterDetails() { + const lang = getLang() + const currentYear = new Date().getFullYear() return (
- + Scandic Hotels logo
- - {detailsData.copyrightCompany}{" "} - {detailsData.copyrightInfo} - +
+ + © {currentYear} {detailsData.copyrightCompany} + + + {detailsData.copyrightInfo} + +
{ diff --git a/components/Footer/Navigation/MainNav/index.tsx b/components/Footer/Navigation/MainNav/index.tsx index f3158ba4f..31fa31338 100644 --- a/components/Footer/Navigation/MainNav/index.tsx +++ b/components/Footer/Navigation/MainNav/index.tsx @@ -1,27 +1,28 @@ import { ArrowRightIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./mainnav.module.css" -export default async function FooterMainNav({ - mainLinks, -}: { - mainLinks: Array<{ id: string; href: string; title: string }> -}) { +import { FooterMainNavProps } from "@/types/components/footer/navigation" + +export default function FooterMainNav({ mainLinks }: FooterMainNavProps) { return (
From 93a0b29075a31ebbf6610fe4eaebf08e7f71058a Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 21 Aug 2024 14:51:33 +0200 Subject: [PATCH 015/319] feat(SW-185): Added images for app stores --- .../Footer/Navigation/SecondaryNav/index.tsx | 22 ++++++++++++++-- components/Footer/mockedData.ts | 7 ++++++ public/_static/img/app-store-badge.svg | 25 +++++++++++++++++++ public/_static/img/google-play-badge.svg | 24 ++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 public/_static/img/app-store-badge.svg create mode 100644 public/_static/img/google-play-badge.svg diff --git a/components/Footer/Navigation/SecondaryNav/index.tsx b/components/Footer/Navigation/SecondaryNav/index.tsx index 914dcf123..a6df1ddee 100644 --- a/components/Footer/Navigation/SecondaryNav/index.tsx +++ b/components/Footer/Navigation/SecondaryNav/index.tsx @@ -1,3 +1,4 @@ +import Image from "@/components/Image" import Link from "@/components/TempDesignSystem/Link" import styles from "./secondarynav.module.css" @@ -7,7 +8,15 @@ export default async function FooterSecondaryNav({ }: { secondaryLinks: Record< string, - { title: string; links: Array<{ id: string; href: string; title: string }> } + { + title: string + links: Array<{ + id: string + href: string + title: string + image?: { src: string } + }> + } > }) { return ( @@ -23,7 +32,16 @@ export default async function FooterSecondaryNav({ href={link.href} className={styles.secondaryNavigationLink} > - {link.title} + {link.image ? ( + {link.title} + ) : ( + link.title + )} ))} diff --git a/components/Footer/mockedData.ts b/components/Footer/mockedData.ts index 8ae41a2df..a9ba3f248 100644 --- a/components/Footer/mockedData.ts +++ b/components/Footer/mockedData.ts @@ -29,11 +29,18 @@ export const navigationData = { title: "App Store", href: "https://apps.apple.com/se/app/scandic-hotels/id1020208712", id: "app-store", + image: { + src: "/_static/img/app-store-badge.svg", + alt: "Download on the App Store", + }, }, { title: "Google Play", href: "https://play.google.com/store/apps/details?id=com.scandichotels.scandichotels", id: "google-play", + image: { + src: "/_static/img/google-play-badge.svg", + }, }, ], }, diff --git a/public/_static/img/app-store-badge.svg b/public/_static/img/app-store-badge.svg new file mode 100644 index 000000000..fe8aeaa23 --- /dev/null +++ b/public/_static/img/app-store-badge.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/google-play-badge.svg b/public/_static/img/google-play-badge.svg new file mode 100644 index 000000000..90936a6a4 --- /dev/null +++ b/public/_static/img/google-play-badge.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + From 6f28aee349f0f4bba9cd6985ce7606ace614560a Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 22 Aug 2024 11:01:31 +0200 Subject: [PATCH 016/319] feat(SW-185): Fixing comments --- components/Footer/Details/details.module.css | 33 ++++++++++---- components/Footer/Details/index.tsx | 45 +++++++++++-------- .../Footer/Navigation/MainNav/index.tsx | 27 +++++------ .../Navigation/MainNav/mainnav.module.css | 2 - .../Footer/Navigation/SecondaryNav/index.tsx | 29 ++++++------ .../SecondaryNav/secondarynav.module.css | 1 - components/Footer/Navigation/index.tsx | 2 +- .../Footer/Navigation/navigation.module.css | 4 +- components/Footer/index.tsx | 2 +- components/Footer/mockedData.ts | 2 +- .../TempDesignSystem/Link/link.module.css | 4 ++ components/TempDesignSystem/Link/variants.ts | 1 + .../Text/Body/body.module.css | 14 ++++++ .../TempDesignSystem/Text/Body/index.tsx | 18 ++++---- .../TempDesignSystem/Text/Body/variants.ts | 3 ++ .../Text/Footnote/footnote.module.css | 14 ++++++ .../Text/Footnote/variants.ts | 3 ++ types/components/footer/navigation.ts | 26 +++++++++++ 18 files changed, 158 insertions(+), 72 deletions(-) create mode 100644 types/components/footer/navigation.ts diff --git a/components/Footer/Details/details.module.css b/components/Footer/Details/details.module.css index a6b184ea2..d8bd17d93 100644 --- a/components/Footer/Details/details.module.css +++ b/components/Footer/Details/details.module.css @@ -1,13 +1,13 @@ .details { - background: var(--Main-Grey-100); - color: var(--Main-Grey-White); - padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7); + background: var(--Main-Red-100); + color: var(--Main-Brand-PalePeach); + padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x6); } .topContainer { display: flex; justify-content: space-between; - padding: 0 0 var(--Spacing-x2) 0; + padding-bottom: var(--Spacing-x2); margin-bottom: var(--Spacing-x2); } @@ -27,21 +27,38 @@ justify-content: space-between; margin-bottom: var(--Spacing-x2); padding-bottom: var(--Spacing-x2); - border-bottom: 1px solid #e3d9d120; + border-bottom: 1px solid var(--Scandic-Peach-80); } .navigation { display: flex; - gap: var(--Spacing-x2); + gap: var(--Spacing-x1); +} + +.link { + &::after { + content: "·"; + margin-left: var(--Spacing-x1); + } + &:last-child { + &::after { + content: ""; + } + } +} + +.copyrightContainer { + display: flex; + gap: var(--Spacing-x1); } @media screen and (min-width: 1367px) { .details { - background: var(--Main-Grey-100); color: var(--Main-Grey-White); - padding: var(--Spacing-x5) var(--Spacing-x5) var(--Spacing-x9); + padding: var(--Spacing-x6) var(--Spacing-x5) var(--Spacing-x4); } .bottomContainer { + border-top: 1px solid var(--Scandic-Peach-80); flex-direction: row; } .navigationContainer { diff --git a/components/Footer/Details/index.tsx b/components/Footer/Details/index.tsx index ac43a04d0..3768005d6 100644 --- a/components/Footer/Details/index.tsx +++ b/components/Footer/Details/index.tsx @@ -3,6 +3,8 @@ import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import Image from "@/components/Image" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" +import { getLang } from "@/i18n/serverContext" import { detailsData } from "../mockedData" @@ -15,11 +17,13 @@ function socialIcon(iconName: string): JSX.Element | null { return SocialIcon ? : {iconName} } -export default async function FooterDetails() { +export default function FooterDetails() { + const lang = getLang() + const currentYear = new Date().getFullYear() return (
- + Scandic Hotels logo
- - {detailsData.copyrightCompany}{" "} - {detailsData.copyrightInfo} - +
+ + © {currentYear} {detailsData.copyrightCompany} + + + {detailsData.copyrightInfo} + +
{ diff --git a/components/Footer/Navigation/MainNav/index.tsx b/components/Footer/Navigation/MainNav/index.tsx index f3158ba4f..31fa31338 100644 --- a/components/Footer/Navigation/MainNav/index.tsx +++ b/components/Footer/Navigation/MainNav/index.tsx @@ -1,27 +1,28 @@ import { ArrowRightIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./mainnav.module.css" -export default async function FooterMainNav({ - mainLinks, -}: { - mainLinks: Array<{ id: string; href: string; title: string }> -}) { +import { FooterMainNavProps } from "@/types/components/footer/navigation" + +export default function FooterMainNav({ mainLinks }: FooterMainNavProps) { return (
From 4db97c43f87151b946045e252a45b7624c818bf3 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 21 Aug 2024 14:51:33 +0200 Subject: [PATCH 030/319] feat(SW-185): Added images for app stores --- .../Footer/Navigation/SecondaryNav/index.tsx | 22 ++++++++++++++-- components/Footer/mockedData.ts | 7 ++++++ public/_static/img/app-store-badge.svg | 25 +++++++++++++++++++ public/_static/img/google-play-badge.svg | 24 ++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 public/_static/img/app-store-badge.svg create mode 100644 public/_static/img/google-play-badge.svg diff --git a/components/Footer/Navigation/SecondaryNav/index.tsx b/components/Footer/Navigation/SecondaryNav/index.tsx index 914dcf123..a6df1ddee 100644 --- a/components/Footer/Navigation/SecondaryNav/index.tsx +++ b/components/Footer/Navigation/SecondaryNav/index.tsx @@ -1,3 +1,4 @@ +import Image from "@/components/Image" import Link from "@/components/TempDesignSystem/Link" import styles from "./secondarynav.module.css" @@ -7,7 +8,15 @@ export default async function FooterSecondaryNav({ }: { secondaryLinks: Record< string, - { title: string; links: Array<{ id: string; href: string; title: string }> } + { + title: string + links: Array<{ + id: string + href: string + title: string + image?: { src: string } + }> + } > }) { return ( @@ -23,7 +32,16 @@ export default async function FooterSecondaryNav({ href={link.href} className={styles.secondaryNavigationLink} > - {link.title} + {link.image ? ( + {link.title} + ) : ( + link.title + )} ))} diff --git a/components/Footer/mockedData.ts b/components/Footer/mockedData.ts index 8ae41a2df..a9ba3f248 100644 --- a/components/Footer/mockedData.ts +++ b/components/Footer/mockedData.ts @@ -29,11 +29,18 @@ export const navigationData = { title: "App Store", href: "https://apps.apple.com/se/app/scandic-hotels/id1020208712", id: "app-store", + image: { + src: "/_static/img/app-store-badge.svg", + alt: "Download on the App Store", + }, }, { title: "Google Play", href: "https://play.google.com/store/apps/details?id=com.scandichotels.scandichotels", id: "google-play", + image: { + src: "/_static/img/google-play-badge.svg", + }, }, ], }, diff --git a/public/_static/img/app-store-badge.svg b/public/_static/img/app-store-badge.svg new file mode 100644 index 000000000..fe8aeaa23 --- /dev/null +++ b/public/_static/img/app-store-badge.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/google-play-badge.svg b/public/_static/img/google-play-badge.svg new file mode 100644 index 000000000..90936a6a4 --- /dev/null +++ b/public/_static/img/google-play-badge.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + From 4ae1c390f26916ae87b2f34a5bc9e194aaead5e1 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 22 Aug 2024 11:01:31 +0200 Subject: [PATCH 031/319] feat(SW-185): Fixing comments --- components/Footer/Details/details.module.css | 33 ++++++++++---- components/Footer/Details/index.tsx | 45 +++++++++++-------- .../Footer/Navigation/MainNav/index.tsx | 27 +++++------ .../Navigation/MainNav/mainnav.module.css | 2 - .../Footer/Navigation/SecondaryNav/index.tsx | 29 ++++++------ .../SecondaryNav/secondarynav.module.css | 1 - components/Footer/Navigation/index.tsx | 2 +- .../Footer/Navigation/navigation.module.css | 4 +- components/Footer/index.tsx | 2 +- components/Footer/mockedData.ts | 2 +- .../TempDesignSystem/Link/link.module.css | 4 ++ components/TempDesignSystem/Link/variants.ts | 1 + .../Text/Body/body.module.css | 14 ++++++ .../TempDesignSystem/Text/Body/index.tsx | 18 ++++---- .../TempDesignSystem/Text/Body/variants.ts | 3 ++ .../Text/Footnote/footnote.module.css | 14 ++++++ .../Text/Footnote/variants.ts | 3 ++ types/components/footer/navigation.ts | 26 +++++++++++ 18 files changed, 158 insertions(+), 72 deletions(-) create mode 100644 types/components/footer/navigation.ts diff --git a/components/Footer/Details/details.module.css b/components/Footer/Details/details.module.css index a6b184ea2..d8bd17d93 100644 --- a/components/Footer/Details/details.module.css +++ b/components/Footer/Details/details.module.css @@ -1,13 +1,13 @@ .details { - background: var(--Main-Grey-100); - color: var(--Main-Grey-White); - padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7); + background: var(--Main-Red-100); + color: var(--Main-Brand-PalePeach); + padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x6); } .topContainer { display: flex; justify-content: space-between; - padding: 0 0 var(--Spacing-x2) 0; + padding-bottom: var(--Spacing-x2); margin-bottom: var(--Spacing-x2); } @@ -27,21 +27,38 @@ justify-content: space-between; margin-bottom: var(--Spacing-x2); padding-bottom: var(--Spacing-x2); - border-bottom: 1px solid #e3d9d120; + border-bottom: 1px solid var(--Scandic-Peach-80); } .navigation { display: flex; - gap: var(--Spacing-x2); + gap: var(--Spacing-x1); +} + +.link { + &::after { + content: "·"; + margin-left: var(--Spacing-x1); + } + &:last-child { + &::after { + content: ""; + } + } +} + +.copyrightContainer { + display: flex; + gap: var(--Spacing-x1); } @media screen and (min-width: 1367px) { .details { - background: var(--Main-Grey-100); color: var(--Main-Grey-White); - padding: var(--Spacing-x5) var(--Spacing-x5) var(--Spacing-x9); + padding: var(--Spacing-x6) var(--Spacing-x5) var(--Spacing-x4); } .bottomContainer { + border-top: 1px solid var(--Scandic-Peach-80); flex-direction: row; } .navigationContainer { diff --git a/components/Footer/Details/index.tsx b/components/Footer/Details/index.tsx index ac43a04d0..3768005d6 100644 --- a/components/Footer/Details/index.tsx +++ b/components/Footer/Details/index.tsx @@ -3,6 +3,8 @@ import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import Image from "@/components/Image" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" +import { getLang } from "@/i18n/serverContext" import { detailsData } from "../mockedData" @@ -15,11 +17,13 @@ function socialIcon(iconName: string): JSX.Element | null { return SocialIcon ? : {iconName} } -export default async function FooterDetails() { +export default function FooterDetails() { + const lang = getLang() + const currentYear = new Date().getFullYear() return (
- + Scandic Hotels logo
- - {detailsData.copyrightCompany}{" "} - {detailsData.copyrightInfo} - +
+ + © {currentYear} {detailsData.copyrightCompany} + + + {detailsData.copyrightInfo} + +
{ diff --git a/components/Footer/Navigation/MainNav/index.tsx b/components/Footer/Navigation/MainNav/index.tsx index f3158ba4f..31fa31338 100644 --- a/components/Footer/Navigation/MainNav/index.tsx +++ b/components/Footer/Navigation/MainNav/index.tsx @@ -1,27 +1,28 @@ import { ArrowRightIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./mainnav.module.css" -export default async function FooterMainNav({ - mainLinks, -}: { - mainLinks: Array<{ id: string; href: string; title: string }> -}) { +import { FooterMainNavProps } from "@/types/components/footer/navigation" + +export default function FooterMainNav({ mainLinks }: FooterMainNavProps) { return (
+
+
+ + + {intl.formatMessage({ id: "Public price from" })} + + 2820 SEK / night + approx 280 eur +
+
+ + + {intl.formatMessage({ id: "Member price from" })} + + 2820 SEK / night + approx 280 eur +
+ +
) } diff --git a/components/Icons/PriceTag.tsx b/components/Icons/PriceTag.tsx new file mode 100644 index 000000000..d91e28900 --- /dev/null +++ b/components/Icons/PriceTag.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function PriceTagIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index c441f38fb..7f0259352 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -33,6 +33,7 @@ export { default as PersonIcon } from "./Person" export { default as PetsIcon } from "./Pets" export { default as PhoneIcon } from "./Phone" export { default as PlusCircleIcon } from "./PlusCircle" +export { default as PriceTagIcon } from "./PriceTag" export { default as RestaurantIcon } from "./Restaurant" export { default as SaunaIcon } from "./Sauna" export { default as ScandicLogoIcon } from "./ScandicLogo" diff --git a/components/TempDesignSystem/Chip/chip.module.css b/components/TempDesignSystem/Chip/chip.module.css index dbfb0e98b..5f3ac60d4 100644 --- a/components/TempDesignSystem/Chip/chip.module.css +++ b/components/TempDesignSystem/Chip/chip.module.css @@ -12,3 +12,11 @@ div.chip { background-color: var(--Scandic-Red-90); color: var(--Primary-Dark-On-Surface-Accent); } + +.subtle { + background-color: var(--Base-Surface-Subtle-Normal); +} + +.pale { + background-color: var(--Scandic-Brand-Pale-Peach); +} diff --git a/components/TempDesignSystem/Chip/variants.ts b/components/TempDesignSystem/Chip/variants.ts index a17e6d98d..420f61ec3 100644 --- a/components/TempDesignSystem/Chip/variants.ts +++ b/components/TempDesignSystem/Chip/variants.ts @@ -6,6 +6,8 @@ export const chipVariants = cva(styles.chip, { variants: { intent: { primary: styles.primary, + sublte: styles.subtle, + pale: styles.pale, }, variant: { default: styles.default, diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index e93e834d1..59fc352bc 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -206,4 +206,4 @@ "Your level": "Your level", "Your points to spend": "Your points to spend", "Zip code": "Zip code" -} \ No newline at end of file +} From bb820afd0eef18415a334d523e13aca211480dd6 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 26 Aug 2024 14:02:23 +0200 Subject: [PATCH 043/319] feat(SW-252): add icons --- .../HotelCard/hotelCard.module.css | 16 ++++++++------ .../HotelReservation/HotelCard/index.tsx | 22 +++++++++++++------ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 3ffb34cde..72f6d2877 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -9,8 +9,7 @@ background-color: var(--Base-Surface-Primary-light-Normal); border: 1px solid var(--Base-Border-Subtle); border-radius: var(--Corner-radius-Medium); - width: 100%; - max-width: 307px; + width: 307px; } .header { @@ -39,6 +38,12 @@ .facilities { display: flex; flex-wrap: wrap; + gap: var(--Spacing-x1); +} + +.facilitiesItem { + display: flex; + align-items: center; gap: var(--Spacing-x-half); } @@ -71,17 +76,14 @@ "image header" "image hotel" "image prices"; - grid-template-columns: auto-fill; overflow: hidden; - width: 100%; - max-width: 1050px; + width: 1050px; padding: 0; } .image { max-height: 100%; - width: 100%; - max-width: 518px; + width: 518px; } .header { diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index ae5548bba..7ef9b14a3 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -1,13 +1,12 @@ +import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { ChevronRightIcon, PriceTagIcon, ScandicLogoIcon, } from "@/components/Icons" -import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" import Chip from "@/components/TempDesignSystem/Chip" -import Divider from "@/components/TempDesignSystem/Divider" import Link from "@/components/TempDesignSystem/Link" import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" @@ -20,6 +19,11 @@ import { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/ export default async function HotelCard({ hotel }: HotelCardProps) { const intl = await getIntl() + + const sortedAmenities = hotel.detailedFacilities + .sort((a, b) => b.sortOrder - a.sortOrder) + .slice(0, 5) + return (
- {hotel.detailedFacilities.slice(0, 6).map((data) => ( - - {data.name} - - ))} + {sortedAmenities.map((facility) => { + const IconComponent = mapFacilityToIcon(facility.name) + return ( +
+ {IconComponent && } + {facility.name} +
+ ) + })}
{intl.formatMessage({ id: "See hotel details" })} From 8c4ade719f171b8be5bc1492558a5fdea55f198f Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Tue, 27 Aug 2024 09:38:01 +0200 Subject: [PATCH 044/319] feat(SW-252): wrap image for layout --- .../HotelCard/hotelCard.module.css | 36 ++++++++++----- .../HotelReservation/HotelCard/index.tsx | 46 +++++++++++++------ .../TempDesignSystem/Chip/chip.module.css | 8 ---- components/TempDesignSystem/Chip/variants.ts | 2 - 4 files changed, 55 insertions(+), 37 deletions(-) diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 72f6d2877..159207d78 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -12,26 +12,27 @@ width: 307px; } -.header { - grid-area: header; +.imageContainer { + grid-area: image; +} + +.tripAdvisor { + display: none; } .image { height: 100%; - max-height: 95px; width: 116px; object-fit: cover; - grid-area: image; } -.logo { - margin-bottom: var(--Spacing-x-half); +.header { + grid-area: header; } .hotel { display: flex; flex-direction: column; - gap: var(--Spacing-x1); grid-area: hotel; } @@ -49,7 +50,7 @@ .link { display: flex; - padding: var(--Spacing-x1) var(--Spacing-x0); + padding: var(--Spacing-x2) var(--Spacing-x0); border-bottom: 1px solid var(--Base-Border-Subtle); } @@ -67,7 +68,7 @@ } .button { - display: none; + justify-content: center; } @media screen and (min-width: 1367px) { @@ -81,8 +82,18 @@ padding: 0; } + .imageContainer { + position: relative; + } + + .tripAdvisor { + display: block; + position: absolute; + left: 7px; + top: 7px; + } + .image { - max-height: 100%; width: 518px; } @@ -92,11 +103,14 @@ } .hotel { + gap: var(--Spacing-x2); padding-right: var(--Spacing-x2); } .prices { flex-direction: row; + align-items: center; + justify-content: space-between; padding-right: var(--Spacing-x2); padding-bottom: var(--Spacing-x2); } @@ -106,8 +120,6 @@ } .button { - display: flex; width: 160px; - justify-content: center; } } diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 7ef9b14a3..c4721abb5 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -4,6 +4,7 @@ import { PriceTagIcon, ScandicLogoIcon, } from "@/components/Icons" +import TripAdvisorIcon from "@/components/Icons/TripAdvisor" import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" import Chip from "@/components/TempDesignSystem/Chip" @@ -26,20 +27,32 @@ export default async function HotelCard({ hotel }: HotelCardProps) { return (
- {hotel.hotelContent.images.metaData.altText} +
+ {hotel.hotelContent.images.metaData.altText} +
+ + + {hotel.ratings?.tripAdvisor.rating} + +
+
- + {hotel.name} - {`${hotel.address.streetAddress}, ${hotel.address.city}`} - {`${hotel.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} + + {`${hotel.address.streetAddress}, ${hotel.address.city}`} + + + {`${hotel.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} +
@@ -60,28 +73,31 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
- - + + {intl.formatMessage({ id: "Public price from" })} 2820 SEK / night approx 280 eur
- - + + {intl.formatMessage({ id: "Member price from" })} 2820 SEK / night approx 280 eur
diff --git a/components/TempDesignSystem/Chip/chip.module.css b/components/TempDesignSystem/Chip/chip.module.css index 5f3ac60d4..dbfb0e98b 100644 --- a/components/TempDesignSystem/Chip/chip.module.css +++ b/components/TempDesignSystem/Chip/chip.module.css @@ -12,11 +12,3 @@ div.chip { background-color: var(--Scandic-Red-90); color: var(--Primary-Dark-On-Surface-Accent); } - -.subtle { - background-color: var(--Base-Surface-Subtle-Normal); -} - -.pale { - background-color: var(--Scandic-Brand-Pale-Peach); -} diff --git a/components/TempDesignSystem/Chip/variants.ts b/components/TempDesignSystem/Chip/variants.ts index 420f61ec3..a17e6d98d 100644 --- a/components/TempDesignSystem/Chip/variants.ts +++ b/components/TempDesignSystem/Chip/variants.ts @@ -6,8 +6,6 @@ export const chipVariants = cva(styles.chip, { variants: { intent: { primary: styles.primary, - sublte: styles.subtle, - pale: styles.pale, }, variant: { default: styles.default, From 51285f359f91f69210ceaeb67e9ee51728626a0a Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Tue, 27 Aug 2024 09:44:47 +0200 Subject: [PATCH 045/319] feat(SW-252): change semantic element --- .../HotelReservation/HotelCard/hotelCard.module.css | 6 +++--- components/HotelReservation/HotelCard/index.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 159207d78..3db576744 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -26,7 +26,7 @@ object-fit: cover; } -.header { +.hotelInformation { grid-area: header; } @@ -87,8 +87,8 @@ } .tripAdvisor { - display: block; position: absolute; + display: block; left: 7px; top: 7px; } @@ -97,7 +97,7 @@ width: 518px; } - .header { + .hotelInformation { padding-top: var(--Spacing-x2); padding-right: var(--Spacing-x2); } diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index c4721abb5..c294e5c0d 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -27,7 +27,7 @@ export default async function HotelCard({ hotel }: HotelCardProps) { return (
-
+
{hotel.hotelContent.images.metaData.altText}
-
-
+
+
{hotel.name} @@ -53,7 +53,7 @@ export default async function HotelCard({ hotel }: HotelCardProps) { <Footnote color="textMediumContrast"> {`${hotel.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} </Footnote> - </header> + </section> <section className={styles.hotel}> <div className={styles.facilities}> {sortedAmenities.map((facility) => { From e7df1143d993da96e11ea3580d41e6a738da00f8 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson <fredrik.thorsson@scandichotels.com> Date: Tue, 27 Aug 2024 10:01:43 +0200 Subject: [PATCH 046/319] feat(Sw-252): add translations --- i18n/dictionaries/da.json | 2 +- i18n/dictionaries/fi.json | 2 +- i18n/dictionaries/no.json | 2 +- i18n/dictionaries/sv.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index c5efa0a8d..b4858d309 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -200,4 +200,4 @@ "Your level": "Dit niveau", "Your points to spend": "Dine brugbare point", "Zip code": "Postnummer" -} \ No newline at end of file +} diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 1a24a14ab..4211d726f 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -200,4 +200,4 @@ "Your level": "Tasosi", "Your points to spend": "Käytettävissä olevat pisteesi", "Zip code": "Postinumero" -} \ No newline at end of file +} diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 0f669ed51..de6bdc286 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -200,4 +200,4 @@ "Your level": "Ditt nivå", "Your points to spend": "Dine brukbare poeng", "Zip code": "Post kode" -} \ No newline at end of file +} diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 7b7f17465..d39f76a10 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -202,4 +202,4 @@ "Your level": "Din nivå", "Your points to spend": "Dina spenderbara poäng", "Zip code": "Postnummer" -} \ No newline at end of file +} From 9c8bb4a862dcb25e738cadd89b30cc4fa024173f Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson <fredrik.thorsson@scandichotels.com> Date: Wed, 28 Aug 2024 16:18:55 +0200 Subject: [PATCH 047/319] feat(SW-252): sort translations --- i18n/dictionaries/da.json | 6 +++++- i18n/dictionaries/de.json | 6 +++++- i18n/dictionaries/en.json | 6 +++++- i18n/dictionaries/fi.json | 5 ++++- i18n/dictionaries/no.json | 6 +++++- i18n/dictionaries/sv.json | 6 +++++- 6 files changed, 29 insertions(+), 6 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index b4858d309..e8bc2b2ed 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -69,6 +69,7 @@ "Hotel surroundings": "Hotel omgivelser", "How it works": "Hvordan det virker", "Join Scandic Friends": "Tilmeld dig Scandic Friends", + "km to city center": "km til byens centrum", "Language": "Sprog", "Level": "Niveau", "Level 1": "Niveau 1", @@ -121,14 +122,15 @@ "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", - "points": "Point", "Points": "Point", + "points": "Point", "Points being calculated": "Point udregnes", "Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021", "Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.", "Points needed to level up": "Point nødvendige for at stige i niveau", "Points needed to stay on level": "Point nødvendige for at holde sig på niveau", "Previous victories": "Tidligere sejre", + "Public price from": "Offentlig pris fra", "Read more": "Læs mere", "Read more about the hotel": "Læs mere om hotellet", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", @@ -141,6 +143,8 @@ "Save": "Gemme", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "See hotel details": "Se hoteloplysninger", + "See rooms": "Se værelser", "Select a country": "Vælg et land", "Select country of residence": "Vælg bopælsland", "Select date of birth": "Vælg fødselsdato", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 9de24aa0a..dcfdbc9e3 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -68,6 +68,7 @@ "Hotel surroundings": "Umgebung des Hotels", "How it works": "Wie es funktioniert", "Join Scandic Friends": "Treten Sie Scandic Friends bei", + "km to city center": "km bis zum Stadtzentrum", "Language": "Sprache", "Level": "Level", "Level 1": "Level 1", @@ -118,14 +119,15 @@ "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", - "Points": "Punkte", "points": "Punkte", + "Points": "Punkte", "Points being calculated": "Punkte werden berechnet", "Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021", "Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.", "Points needed to level up": "Punkte, die zum Levelaufstieg benötigt werden", "Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben", "Previous victories": "Bisherige Siege", + "Public price from": "Öffentlicher Preis ab", "Read more": "Mehr lesen", "Read more about the hotel": "Lesen Sie mehr über das Hotel", "Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen", @@ -136,6 +138,8 @@ "Save": "Speichern", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "See hotel details": "Hotelinformationen ansehen", + "See rooms": "Zimmer ansehen", "Select a country": "Wähle ein Land", "Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus", "Select date of birth": "Geburtsdatum auswählen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 59fc352bc..ada044bb7 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -74,6 +74,7 @@ "hotelPages.rooms.roomCard.seeRoomDetails": "See room details", "How it works": "How it works", "Join Scandic Friends": "Join Scandic Friends", + "km to city center": "km to city center", "Language": "Language", "Level": "Level", "Level 1": "Level 1", @@ -126,14 +127,15 @@ "Phone is required": "Phone is required", "Phone number": "Phone number", "Please enter a valid phone number": "Please enter a valid phone number", - "points": "Points", "Points": "Points", + "points": "Points", "Points being calculated": "Points being calculated", "Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021", "Points may take up to 10 days to be displayed.": "Points may take up to 10 days to be displayed.", "Points needed to level up": "Points needed to level up", "Points needed to stay on level": "Points needed to stay on level", "Previous victories": "Previous victories", + "Public price from": "Public price from", "Read more": "Read more", "Read more about the hotel": "Read more about the hotel", "Remove card from member profile": "Remove card from member profile", @@ -146,7 +148,9 @@ "Save": "Save", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "See hotel details": "See hotel details", "See room details": "See room details", + "See rooms": "See rooms", "Select a country": "Select a country", "Select country of residence": "Select country of residence", "Select date of birth": "Select date of birth", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 4211d726f..a3dadbcd9 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -69,6 +69,7 @@ "Hotel surroundings": "Hotellin ympäristö", "How it works": "Kuinka se toimii", "Join Scandic Friends": "Liity jäseneksi", + "km to city center": "km keskustaan", "Language": "Kieli", "Level": "Level", "Level 1": "Taso 1", @@ -121,8 +122,8 @@ "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", - "Points": "Pisteet", "points": "pistettä", + "Points": "Pisteet", "Points being calculated": "Pisteitä lasketaan", "Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021", "Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.", @@ -141,6 +142,8 @@ "Save": "Tallentaa", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "See hotel details": "Katso hotellin tiedot", + "See rooms": "Katso huoneet", "Select a country": "Valitse maa", "Select country of residence": "Valitse asuinmaa", "Select date of birth": "Valitse syntymäaika", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index de6bdc286..94fef19ab 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -69,6 +69,7 @@ "Hotel surroundings": "Hotellomgivelser", "How it works": "Hvordan det fungerer", "Join Scandic Friends": "Bli med i Scandic Friends", + "km to city center": "km til sentrum", "Language": "Språk", "Level": "Nivå", "Level 1": "Nivå 1", @@ -121,14 +122,15 @@ "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", - "Points": "Poeng", "points": "Poeng", + "Points": "Poeng", "Points being calculated": "Poeng beregnes", "Points earned prior to May 1, 2021": "Opptjente poeng før 1. mai 2021", "Points may take up to 10 days to be displayed.": "Det kan ta opptil 10 dager før poeng vises.", "Points needed to level up": "Poeng som trengs for å komme opp i nivå", "Points needed to stay on level": "Poeng som trengs for å holde seg på nivå", "Previous victories": "Tidligere seire", + "Public price from": "Offentlig pris fra", "Read more": "Les mer", "Read more about the hotel": "Les mer om hotellet", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", @@ -141,6 +143,8 @@ "Save": "Lagre", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "See hotel details": "Se hotellinformasjon", + "See rooms": "Se rom", "Select a country": "Velg et land", "Select country of residence": "Velg bostedsland", "Select date of birth": "Velg fødselsdato", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index d39f76a10..7785ca847 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -71,6 +71,7 @@ "hotelPages.rooms.roomCard.persons": "personer", "How it works": "Hur det fungerar", "Join Scandic Friends": "Gå med i Scandic Friends", + "km to city center": "km till stadens centrum", "Language": "Språk", "Level": "Nivå", "Level 1": "Nivå 1", @@ -123,14 +124,15 @@ "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", - "Points": "Poäng", "points": "poäng", + "Points": "Poäng", "Points being calculated": "Poäng beräknas", "Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021", "Points may take up to 10 days to be displayed.": "Det kan ta upp till 10 dagar innan poäng visas.", "Points needed to level up": "Poäng som behövs för att gå upp i nivå", "Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå", "Previous victories": "Tidigare segrar", + "Public price from": "Offentligt pris från", "Read more": "Läs mer", "Read more about the hotel": "Läs mer om hotellet", "Remove card from member profile": "Ta bort kortet från medlemsprofilen", @@ -143,7 +145,9 @@ "Save": "Spara", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "See hotel details": "Se hotellinformation", "See room details": "Se rumsdetaljer", + "See rooms": "Se rum", "Select a country": "Välj ett land", "Select country of residence": "Välj bosättningsland", "Select date of birth": "Välj födelsedatum", From ee879f60a3e93cbf77ab343e0f4972417cfcbbd0 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson <fredrik.thorsson@scandichotels.com> Date: Thu, 29 Aug 2024 15:51:35 +0200 Subject: [PATCH 048/319] feat(SW-252): translations --- i18n/dictionaries/da.json | 3 ++- i18n/dictionaries/de.json | 3 ++- i18n/dictionaries/en.json | 3 ++- i18n/dictionaries/fi.json | 3 ++- i18n/dictionaries/no.json | 3 ++- i18n/dictionaries/sv.json | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index e8bc2b2ed..e6a339181 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -86,6 +86,7 @@ "Manage preferences": "Administrer præferencer", "Meetings & Conferences": "Møder & Konferencer", "Member price": "Medlemspris", + "Member price from": "Medlemspris fra", "Members": "Medlemmer", "Membership cards": "Medlemskort", "Membership ID": "Medlems-id", @@ -122,8 +123,8 @@ "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", - "Points": "Point", "points": "Point", + "Points": "Point", "Points being calculated": "Point udregnes", "Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021", "Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index dcfdbc9e3..6f730539d 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -84,6 +84,7 @@ "Log out": "Ausloggen", "Manage preferences": "Verwalten von Voreinstellungen", "Member price": "Mitgliederpreis", + "Member price from": "Mitgliederpreis ab", "Members": "Mitglieder", "Membership cards": "Mitgliedskarten", "Membership ID": "Mitglieds-ID", @@ -119,8 +120,8 @@ "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", - "points": "Punkte", "Points": "Punkte", + "points": "Punkte", "Points being calculated": "Punkte werden berechnet", "Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021", "Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index ada044bb7..da33379f2 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -91,6 +91,7 @@ "Manage preferences": "Manage preferences", "Meetings & Conferences": "Meetings & Conferences", "Member price": "Member price", + "Member price from": "Member price from", "Members": "Members", "Membership cards": "Membership cards", "Membership ID": "Membership ID", @@ -127,8 +128,8 @@ "Phone is required": "Phone is required", "Phone number": "Phone number", "Please enter a valid phone number": "Please enter a valid phone number", - "Points": "Points", "points": "Points", + "Points": "Points", "Points being calculated": "Points being calculated", "Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021", "Points may take up to 10 days to be displayed.": "Points may take up to 10 days to be displayed.", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index a3dadbcd9..1c71ec2a7 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -86,6 +86,7 @@ "Manage preferences": "Asetusten hallinta", "Meetings & Conferences": "Kokoukset & Konferenssit", "Member price": "Jäsenhinta", + "Member price from": "Jäsenhinta alkaen", "Members": "Jäsenet", "Membership cards": "Jäsenkortit", "Membership ID": "Jäsentunnus", @@ -122,8 +123,8 @@ "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", - "points": "pistettä", "Points": "Pisteet", + "points": "pistettä", "Points being calculated": "Pisteitä lasketaan", "Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021", "Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 94fef19ab..eeb1cfbc6 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -86,6 +86,7 @@ "Manage preferences": "Administrer preferanser", "Meetings & Conferences": "Møter & Konferanser", "Member price": "Medlemspris", + "Member price from": "Medlemspris fra", "Members": "Medlemmer", "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", @@ -122,8 +123,8 @@ "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", - "points": "Poeng", "Points": "Poeng", + "points": "Poeng", "Points being calculated": "Poeng beregnes", "Points earned prior to May 1, 2021": "Opptjente poeng før 1. mai 2021", "Points may take up to 10 days to be displayed.": "Det kan ta opptil 10 dager før poeng vises.", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 7785ca847..721037d7f 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -88,6 +88,7 @@ "Manage preferences": "Hantera inställningar", "Meetings & Conferences": "Möten & Konferenser", "Member price": "Medlemspris", + "Member price from": "Medlemspris från", "Members": "Medlemmar", "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", @@ -124,8 +125,8 @@ "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", - "points": "poäng", "Points": "Poäng", + "points": "poäng", "Points being calculated": "Poäng beräknas", "Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021", "Points may take up to 10 days to be displayed.": "Det kan ta upp till 10 dagar innan poäng visas.", From 342885d71a6af1f02b6416c6771e399b17a1a558 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson <fredrik.thorsson@scandichotels.com> Date: Thu, 29 Aug 2024 15:58:40 +0200 Subject: [PATCH 049/319] feat(SW-252): add missing translation --- i18n/dictionaries/fi.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 1c71ec2a7..1993807d4 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -203,5 +203,6 @@ "Your details": "Tietosi", "Your level": "Tasosi", "Your points to spend": "Käytettävissä olevat pisteesi", - "Zip code": "Postinumero" + "Zip code": "Postinumero", + "Public price from": "Julkinen hinta alkaen" } From 7742fc1259125b4cfe10edeef4314ebe42a78bc3 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson <fredrik.thorsson@scandichotels.com> Date: Thu, 29 Aug 2024 16:00:47 +0200 Subject: [PATCH 050/319] feat(SW-252): sorting --- i18n/dictionaries/fi.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 1993807d4..d362b3dbe 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -123,14 +123,15 @@ "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", - "Points": "Pisteet", "points": "pistettä", + "Points": "Pisteet", "Points being calculated": "Pisteitä lasketaan", "Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021", "Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.", "Points needed to level up": "Tarvitset vielä", "Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet", "Previous victories": "Edelliset voitot", + "Public price from": "Julkinen hinta alkaen", "Read more": "Lue lisää", "Read more about the hotel": "Lue lisää hotellista", "Remove card from member profile": "Poista kortti jäsenprofiilista", @@ -203,6 +204,5 @@ "Your details": "Tietosi", "Your level": "Tasosi", "Your points to spend": "Käytettävissä olevat pisteesi", - "Zip code": "Postinumero", - "Public price from": "Julkinen hinta alkaen" + "Zip code": "Postinumero" } From 93e54b4ca1a027e9e7972c3dc2d4956a68d8a672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20Landstr=C3=B6m?= <matilda.landstrom@scandichotels.com> Date: Thu, 29 Aug 2024 16:51:19 +0200 Subject: [PATCH 051/319] feat(SW-93): add mocked facility cards --- .../Facilities/CardGrid/cardGrid.module.css | 31 ++++ .../HotelPage/Facilities/CardGrid/index.tsx | 34 ++++ .../Facilities/facilities.module.css | 9 + .../HotelPage/Facilities/index.tsx | 17 ++ .../HotelPage/Facilities/mockData.ts | 159 ++++++++++++++++++ .../ContentType/HotelPage/SidePeeks.tsx | 37 +++- components/ContentType/HotelPage/index.tsx | 3 + .../Card/CardImage/cardImage.module.css | 22 +++ .../TempDesignSystem/Card/CardImage/index.tsx | 34 ++++ .../TempDesignSystem/Card/card.module.css | 59 ++++++- components/TempDesignSystem/Card/card.ts | 4 +- components/TempDesignSystem/Card/index.tsx | 119 +++++++------ components/TempDesignSystem/Card/variants.ts | 7 + .../TempDesignSystem/Link/link.module.css | 4 + components/TempDesignSystem/Link/variants.ts | 1 + .../Text/BiroScript/biroScript.module.css | 12 +- .../Text/BiroScript/variants.ts | 2 + constants/routes/hotelPageParams.js | 45 ++++- types/components/cardImage.ts | 7 + types/components/hotelPage/facilities.ts | 19 +++ utils/cardTheme.ts | 52 ++++++ utils/imageCard.ts | 18 ++ 22 files changed, 632 insertions(+), 63 deletions(-) create mode 100644 components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css create mode 100644 components/ContentType/HotelPage/Facilities/CardGrid/index.tsx create mode 100644 components/ContentType/HotelPage/Facilities/facilities.module.css create mode 100644 components/ContentType/HotelPage/Facilities/index.tsx create mode 100644 components/ContentType/HotelPage/Facilities/mockData.ts create mode 100644 components/TempDesignSystem/Card/CardImage/cardImage.module.css create mode 100644 components/TempDesignSystem/Card/CardImage/index.tsx create mode 100644 types/components/cardImage.ts create mode 100644 types/components/hotelPage/facilities.ts create mode 100644 utils/cardTheme.ts create mode 100644 utils/imageCard.ts diff --git a/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css b/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css new file mode 100644 index 000000000..c205f68fe --- /dev/null +++ b/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css @@ -0,0 +1,31 @@ +.one { + grid-column: span 1; +} + +.two { + grid-column: span 2; +} + +.three { + grid-column: 1/-1; +} + +.desktopGrid { + display: none; +} + +.mobileGrid { + display: grid; + gap: var(--Spacing-x-quarter); +} + +@media screen and (min-width: 768px) { + .desktopGrid { + display: grid; + gap: var(--Spacing-x1); + } + + .mobileGrid { + display: none; + } +} diff --git a/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx b/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx new file mode 100644 index 000000000..8d98c689d --- /dev/null +++ b/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx @@ -0,0 +1,34 @@ +import Card from "@/components/TempDesignSystem/Card" +import CardImage from "@/components/TempDesignSystem/Card/CardImage" +import Grids from "@/components/TempDesignSystem/Grids" +import { sortCards } from "@/utils/imageCard" + +import styles from "./cardGrid.module.css" + +import type { CardGridProps } from "@/types/components/hotelPage/facilities" + +export default async function CardGrid({ facility }: CardGridProps) { + const imageCard = sortCards(facility) + return ( + <section id={imageCard.card?.id}> + <Grids.Stackable className={styles.desktopGrid}> + {facility.map((card: any, idx: number) => ( + <Card + theme={card.theme || "primaryDark"} + key={idx} + scriptedTopTitle={card.scriptedTopTitle} + heading={card.heading} + bodyText={card.bodyText} + secondaryButton={card.secondaryButton} + primaryButton={card.primaryButton} + backgroundImage={card.backgroundImage} + className={styles[card.columnSpan]} + /> + ))} + </Grids.Stackable> + <Grids.Stackable className={styles.mobileGrid}> + <CardImage card={imageCard.card} imageCards={imageCard.images} /> + </Grids.Stackable> + </section> + ) +} diff --git a/components/ContentType/HotelPage/Facilities/facilities.module.css b/components/ContentType/HotelPage/Facilities/facilities.module.css new file mode 100644 index 000000000..aea674734 --- /dev/null +++ b/components/ContentType/HotelPage/Facilities/facilities.module.css @@ -0,0 +1,9 @@ +.grid { + gap: var(--Spacing-x2); +} + +@media screen and (min-width: 768px) { + .grid { + gap: var(--Spacing-x7); + } +} diff --git a/components/ContentType/HotelPage/Facilities/index.tsx b/components/ContentType/HotelPage/Facilities/index.tsx new file mode 100644 index 000000000..8775d0de2 --- /dev/null +++ b/components/ContentType/HotelPage/Facilities/index.tsx @@ -0,0 +1,17 @@ +import SectionContainer from "@/components/Section/Container" + +import CardGrid from "./CardGrid" + +import styles from "./facilities.module.css" + +import type { FacilityProps } from "@/types/components/hotelPage/facilities" + +export default async function Facilities({ facilities }: FacilityProps) { + return ( + <SectionContainer className={styles.grid}> + {facilities.map((facility: any, idx: number) => ( + <CardGrid key={`grid_${idx}`} facility={facility} /> + ))} + </SectionContainer> + ) +} diff --git a/components/ContentType/HotelPage/Facilities/mockData.ts b/components/ContentType/HotelPage/Facilities/mockData.ts new file mode 100644 index 000000000..a54a39309 --- /dev/null +++ b/components/ContentType/HotelPage/Facilities/mockData.ts @@ -0,0 +1,159 @@ +import { + activities, + meetingsAndConferences, + restaurantAndBar, + wellnessAndExercise, +} from "@/constants/routes/hotelPageParams" + +import { getLang } from "@/i18n/serverContext" + +import type { Facilities } from "@/types/components/hotelPage/facilities" + +const lang = getLang() +/* +Most of this will be available from the api. Some will need to come from Contentstack. "Activities" will most likely come from Contentstack, which is prepped for. + */ +export const MOCK_FACILITIES: Facilities = [ + [ + { + id: "restaurant-and-bar", + theme: "primaryDark", + scriptedTopTitle: "Restaurant & Bar", + heading: "Enjoy relaxed restaurant experience", + secondaryButton: { + href: `?s=${restaurantAndBar[lang]}`, + title: "Read more & book a table", + isExternal: false, + }, + columnSpan: "one", + }, + { + backgroundImage: { + url: "https://imagevault.scandichotels.com/publishedmedia/79xttlmnum0kjbwhyh18/scandic-helsinki-hub-restaurant-food-tuna.jpg", + title: "scandic-helsinki-hub-restaurant-food-tuna.jpg", + meta: { + alt: "food in restaurant at scandic helsinki hub", + caption: "food in restaurant at scandic helsinki hub", + }, + id: 81751, + dimensions: { + width: 5935, + height: 3957, + aspectRatio: 1.499873641647713, + }, + }, + columnSpan: "one", + }, + { + backgroundImage: { + url: "https://imagevault.scandichotels.com/publishedmedia/48sb3eyhhzj727l2j1af/Scandic-helsinki-hub-II-centro-41.jpg", + meta: { + alt: "restaurant il centro at scandic helsinki hu", + caption: "restaurant il centro at scandic helsinki hub", + }, + id: 82457, + title: "Scandic-helsinki-hub-II-centro-41.jpg", + dimensions: { + width: 4200, + height: 2800, + aspectRatio: 1.5, + }, + }, + columnSpan: "one", + }, + ], + [ + { + backgroundImage: { + url: "https://imagevault.scandichotels.com/publishedmedia/csef06n329hjfiet1avj/Scandic-spectrum-8.jpg", + meta: { + alt: "man with a laptop", + caption: "man with a laptop", + }, + id: 82713, + title: "Scandic-spectrum-8.jpg", + dimensions: { + width: 7499, + height: 4999, + aspectRatio: 1.500100020004001, + }, + }, + columnSpan: "two", + }, + { + id: "meetings-and-conferences", + theme: "primaryDim", + scriptedTopTitle: "Meetings & Conferences", + heading: "Events that make an impression", + secondaryButton: { + href: `?s=${meetingsAndConferences[lang]}`, + title: "About meetings & conferences", + isExternal: false, + }, + columnSpan: "one", + }, + ], + [ + { + id: "wellness-and-exercise", + theme: "one", + scriptedTopTitle: "Wellness & Exercise", + heading: "Sauna and gym", + secondaryButton: { + href: `?s=${wellnessAndExercise[lang]}`, + title: "Read more about wellness & exercise", + isExternal: false, + }, + columnSpan: "one", + }, + { + backgroundImage: { + url: "https://imagevault.scandichotels.com/publishedmedia/69acct5i3pk5be7d6ub0/scandic-helsinki-hub-sauna.jpg", + meta: { + alt: "sauna at scandic helsinki hub", + caption: "sauna at scandic helsinki hub", + }, + id: 81814, + title: "scandic-helsinki-hub-sauna.jpg", + dimensions: { + width: 4000, + height: 2667, + aspectRatio: 1.4998125234345707, + }, + }, + columnSpan: "one", + }, + { + backgroundImage: { + url: "https://imagevault.scandichotels.com/publishedmedia/eu70o6z85idy24r92ysf/Scandic-Helsinki-Hub-gym-22.jpg", + meta: { + alt: "Gym at hotel Scandic Helsinki Hub", + caption: "Gym at hotel Scandic Helsinki Hub", + }, + id: 81867, + title: "Scandic-Helsinki-Hub-gym-22.jpg", + dimensions: { + width: 4000, + height: 2667, + aspectRatio: 1.4998125234345707, + }, + }, + columnSpan: "one", + }, + ], + [ + { + id: "activities", + theme: "primaryDark", + scriptedTopTitle: "Activities", + heading: "Upcoming activities at DownTown Camper", + bodyText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit", + secondaryButton: { + href: `?s=${activities[lang]}`, + title: "Discover activities", + isExternal: false, + }, + columnSpan: "three", + }, + ], +] diff --git a/components/ContentType/HotelPage/SidePeeks.tsx b/components/ContentType/HotelPage/SidePeeks.tsx index 272ea4287..2fcf9b46f 100644 --- a/components/ContentType/HotelPage/SidePeeks.tsx +++ b/components/ContentType/HotelPage/SidePeeks.tsx @@ -4,7 +4,14 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation" import { useEffect, useState } from "react" import { useIntl } from "react-intl" -import { about, amenities } from "@/constants/routes/hotelPageParams" +import { + about, + activities, + amenities, + meetingsAndConferences, + restaurantAndBar, + wellnessAndExercise, +} from "@/constants/routes/hotelPageParams" import SidePeek from "@/components/TempDesignSystem/SidePeek" import SidePeekItem from "@/components/TempDesignSystem/SidePeek/Item" @@ -57,6 +64,34 @@ function SidePeekContainer() { > Some additional information about the hotel </SidePeekItem> + <SidePeekItem + contentKey={restaurantAndBar[lang]} + title={intl.formatMessage({ id: "Restaurant & Bar" })} + > + {/* TODO */} + Restaurant & Bar + </SidePeekItem> + <SidePeekItem + contentKey={wellnessAndExercise[lang]} + title={intl.formatMessage({ id: "Wellness & Exercise" })} + > + {/* TODO */} + Wellness & Exercise + </SidePeekItem> + <SidePeekItem + contentKey={activities[lang]} + title={intl.formatMessage({ id: "Activities" })} + > + {/* TODO */} + Activities + </SidePeekItem> + <SidePeekItem + contentKey={meetingsAndConferences[lang]} + title={intl.formatMessage({ id: "Meetings & Conferences" })} + > + {/* TODO */} + Meetings & Conferences + </SidePeekItem> </SidePeek> ) } diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 4c686d174..f29e8e28c 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -2,7 +2,9 @@ import { serverClient } from "@/lib/trpc/server" import { getLang } from "@/i18n/serverContext" +import { MOCK_FACILITIES } from "./Facilities/mockData" import AmenitiesList from "./AmenitiesList" +import Facilities from "./Facilities" import IntroSection from "./IntroSection" import { Rooms } from "./Rooms" import SidePeeks from "./SidePeeks" @@ -45,6 +47,7 @@ export default async function HotelPage() { <AmenitiesList detailedFacilities={hotel.detailedFacilities} /> </div> <Rooms rooms={roomCategories} /> + <Facilities facilities={MOCK_FACILITIES} /> </main> </div> ) diff --git a/components/TempDesignSystem/Card/CardImage/cardImage.module.css b/components/TempDesignSystem/Card/CardImage/cardImage.module.css new file mode 100644 index 000000000..5b647da30 --- /dev/null +++ b/components/TempDesignSystem/Card/CardImage/cardImage.module.css @@ -0,0 +1,22 @@ +.image { + object-fit: cover; + overflow: hidden; + width: 100%; + min-height: 180px; /* Fixed height from Figma */ + border-radius: var(--Corner-radius-Medium); +} + +.imageContainer { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: var(--Spacing-x-quarter); +} + +.card { + height: 254px; /* Fixed height from Figma */ +} + +.container { + display: grid; + gap: var(--Spacing-x-quarter); +} diff --git a/components/TempDesignSystem/Card/CardImage/index.tsx b/components/TempDesignSystem/Card/CardImage/index.tsx new file mode 100644 index 000000000..dc9c4f7f9 --- /dev/null +++ b/components/TempDesignSystem/Card/CardImage/index.tsx @@ -0,0 +1,34 @@ +import Image from "@/components/Image" + +import Card from ".." + +import styles from "./cardImage.module.css" + +import type { CardImageProps } from "@/types/components/cardImage" + +export default function CardImage({ + card, + imageCards, + className, +}: CardImageProps) { + return ( + <article className={`${styles.container} ${className}`}> + <div className={styles.imageContainer}> + {imageCards.map( + ({ backgroundImage }) => + backgroundImage && ( + <Image + key={backgroundImage.id} + src={backgroundImage.url} + className={styles.image} + alt={backgroundImage.title} + width={180} + height={180} + /> + ) + )} + </div> + <Card {...card} className={styles.card} /> + </article> + ) +} diff --git a/components/TempDesignSystem/Card/card.module.css b/components/TempDesignSystem/Card/card.module.css index e7a2124d8..099701852 100644 --- a/components/TempDesignSystem/Card/card.module.css +++ b/components/TempDesignSystem/Card/card.module.css @@ -1,15 +1,29 @@ .container { align-items: center; display: flex; - border-radius: var(--Corner-radius-xLarge); + border-radius: var(--Corner-radius-Medium); flex-direction: column; - gap: var(--Spacing-x2); - height: 480px; + height: 320px; /* Fixed height from Figma */ justify-content: center; margin-right: var(--Spacing-x2); - padding: var(--Spacing-x0) var(--Spacing-x4); text-align: center; width: 100%; + text-wrap: balance; + overflow: hidden; +} + +.image { + object-fit: cover; + overflow: hidden; + width: 100%; + height: auto; + min-height: 320px; /* Fixed height from Figma */ +} + +.content { + margin: var(--Spacing-x0) var(--Spacing-x4); + display: grid; + gap: var(--Spacing-x2); } .themeOne { @@ -33,6 +47,42 @@ background: var(--Tertiary-Light-Surface-Normal); } +.themePrimaryDark { + --font-color: var(--Primary-Dark-On-Surface-Text); + --script-color: var(--Primary-Dark-On-Surface-Accent); + + background: var(--Primary-Dark-Surface-Normal); +} + +.themePrimaryDim { + --font-color: var(--Primary-Light-On-Surface-Text); + --script-color: var(--Primary-Dim-On-Surface-Accent); + + background: var(--Primary-Dim-Surface-Normal); +} + +.themePrimaryInverted { + --font-color: var(--Primary-Light-On-Surface-Text); + --script-color: var(--Primary-Light-On-Surface-Accent); + + background: var(--Base-Surface-Primary-light-Normal); +} + +.themePrimaryStrong { + --font-color: var(--Primary-Strong-On-Surface-Text); + --script-color: var(--Primary-Strong-On-Surface-Accent); + + background: var(--Primary-Strong-Surface-Normal); +} + +.themeImage { + --font-color: var(--Base-Text-Inverted); + --script-color: var(--Base-Text-Inverted); + + border: 1px; /* px from Figma */ + border-color: var(--Base-Border-Subtle); +} + .scriptContainer { display: grid; gap: var(--Spacing-x1); @@ -42,7 +92,6 @@ span.scriptedTitle { color: var(--script-color); padding: var(--Spacing-x1); margin: 0; - transform: rotate(-3deg); } .heading { diff --git a/components/TempDesignSystem/Card/card.ts b/components/TempDesignSystem/Card/card.ts index bf6cb0b7c..a27e0f962 100644 --- a/components/TempDesignSystem/Card/card.ts +++ b/components/TempDesignSystem/Card/card.ts @@ -2,6 +2,8 @@ import { cardVariants } from "./variants" import type { VariantProps } from "class-variance-authority" +import type { ImageVaultAsset } from "@/types/components/imageVault" + export interface CardProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof cardVariants> { @@ -20,5 +22,5 @@ export interface CardProps scriptedTopTitle?: string | null heading?: string | null bodyText?: string | null - backgroundImage?: { url: string } + backgroundImage?: ImageVaultAsset } diff --git a/components/TempDesignSystem/Card/index.tsx b/components/TempDesignSystem/Card/index.tsx index 8c51da7aa..d611c784a 100644 --- a/components/TempDesignSystem/Card/index.tsx +++ b/components/TempDesignSystem/Card/index.tsx @@ -1,14 +1,15 @@ +import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" import BiroScript from "@/components/TempDesignSystem/Text/BiroScript" import Body from "@/components/TempDesignSystem/Text/Body" import Title from "@/components/TempDesignSystem/Text/Title" +import { getTheme } from "@/utils/cardTheme" import { cardVariants } from "./variants" import styles from "./card.module.css" -import type { ButtonProps } from "@/components/TempDesignSystem/Button/button" import type { CardProps } from "./card" export default function Card({ @@ -19,20 +20,9 @@ export default function Card({ bodyText, className, theme, + backgroundImage, }: CardProps) { - let buttonTheme: ButtonProps["theme"] = "primaryLight" - - switch (theme) { - case "one": - buttonTheme = "primaryLight" - break - case "two": - buttonTheme = "secondaryLight" - break - case "three": - buttonTheme = "tertiaryLight" - break - } + const { buttonTheme, primaryLinkColor, secondaryLinkColor } = getTheme(theme) return ( <article @@ -41,48 +31,71 @@ export default function Card({ theme, })} > - {scriptedTopTitle ? ( - <section className={styles.scriptContainer}> - <BiroScript className={styles.scriptedTitle} type="two"> - {scriptedTopTitle} - </BiroScript> - </section> - ) : null} - <Title as="h5" className={styles.heading} level="h3"> - {heading} - - {bodyText ? ( - - {bodyText} - - ) : null} -
- {primaryButton ? ( - + {scriptedTopTitle} + +
) : null} - {secondaryButton ? ( - + ) : null} + {secondaryButton ? ( + - ) : null} + + {secondaryButton.title} + + + ) : null} +
) diff --git a/components/TempDesignSystem/Card/variants.ts b/components/TempDesignSystem/Card/variants.ts index 1d660ac17..8c4ab4e73 100644 --- a/components/TempDesignSystem/Card/variants.ts +++ b/components/TempDesignSystem/Card/variants.ts @@ -8,6 +8,13 @@ export const cardVariants = cva(styles.container, { one: styles.themeOne, two: styles.themeTwo, three: styles.themeThree, + + primaryDark: styles.themePrimaryDark, + primaryDim: styles.themePrimaryDim, + primaryInverted: styles.themePrimaryInverted, + primaryStrong: styles.themePrimaryStrong, + + image: styles.themeImage, }, }, defaultVariants: { diff --git a/components/TempDesignSystem/Link/link.module.css b/components/TempDesignSystem/Link/link.module.css index 0261002de..6e43601b9 100644 --- a/components/TempDesignSystem/Link/link.module.css +++ b/components/TempDesignSystem/Link/link.module.css @@ -130,6 +130,10 @@ color: var(--Primary-Light-On-Surface-Accent); } +.red { + color: var(--Primary-Strong-Button-Primary-On-Fill-Normal); +} + .peach80:hover, .peach80:active { color: var(--Primary-Light-On-Surface-Hover); diff --git a/components/TempDesignSystem/Link/variants.ts b/components/TempDesignSystem/Link/variants.ts index fa0738c50..ceb0960b5 100644 --- a/components/TempDesignSystem/Link/variants.ts +++ b/components/TempDesignSystem/Link/variants.ts @@ -14,6 +14,7 @@ export const linkVariants = cva(styles.link, { pale: styles.pale, peach80: styles.peach80, white: styles.white, + red: styles.red, }, size: { small: styles.small, diff --git a/components/TempDesignSystem/Text/BiroScript/biroScript.module.css b/components/TempDesignSystem/Text/BiroScript/biroScript.module.css index a357a0c88..87db768a7 100644 --- a/components/TempDesignSystem/Text/BiroScript/biroScript.module.css +++ b/components/TempDesignSystem/Text/BiroScript/biroScript.module.css @@ -26,10 +26,14 @@ line-height: var(--typography-Script-2-lineHeight); } -.tiltedSmall { +.tiltedExtraSmall { transform: rotate(-2deg); } +.tiltedSmall { + transform: rotate(-3deg); +} + .tiltedMedium { transform: rotate(-4deg) translate(0px, -15px); } @@ -59,7 +63,7 @@ } .peach80 { - color: var(--Scandic-Peach-80); + color: var(--Base-Text-Medium-contrast); } .plosa { @@ -69,3 +73,7 @@ .red { color: var(--Scandic-Brand-Scandic-Red); } + +.pink { + color: var(--Primary-Dark-On-Surface-Accent); +} diff --git a/components/TempDesignSystem/Text/BiroScript/variants.ts b/components/TempDesignSystem/Text/BiroScript/variants.ts index 3e0aaa4fa..f7e330b48 100644 --- a/components/TempDesignSystem/Text/BiroScript/variants.ts +++ b/components/TempDesignSystem/Text/BiroScript/variants.ts @@ -11,6 +11,7 @@ const config = { peach80: styles.peach80, primaryLightOnSurfaceAccent: styles.plosa, red: styles.red, + pink: styles.pink, }, textAlign: { center: styles.center, @@ -21,6 +22,7 @@ const config = { two: styles.two, }, tilted: { + extraSmall: styles.tiltedExtraSmall, small: styles.tiltedSmall, medium: styles.tiltedMedium, large: styles.tiltedLarge, diff --git a/constants/routes/hotelPageParams.js b/constants/routes/hotelPageParams.js index a6fcb1a5a..9eadf2996 100644 --- a/constants/routes/hotelPageParams.js +++ b/constants/routes/hotelPageParams.js @@ -16,6 +16,49 @@ export const amenities = { de: "annehmlichkeiten", } -const params = { about, amenities } +export const wellnessAndExercise = { + en: "wellness-and-exercise", + sv: "halsa-och-träning", + no: "velvære-og-trening", + da: "wellness-og-motion", + fi: "hyvinvointia-ja-liikuntaa", + de: "Wellness-und-Bewegung", +} + +export const activities = { + en: "activities", + sv: "aktiviteter", + no: "aktiviteter", + da: "aktiviteter", + fi: "toimintaa", + de: "Aktivitäten", +} + +export const meetingsAndConferences = { + en: "meetings-and-conferences", + sv: "moten-och-konferenser", + no: "møter-og-konferansers", + da: "møder-og-konferencer", + fi: "kokoukset-ja-konferenssit", + de: "Tagungen-und-Konferenzen", +} + +export const restaurantAndBar = { + en: "restaurant-and-bar", + sv: "restaurant-och-bar", + no: "restaurant-og-bar", + da: "restaurant-og-bar", + fi: "ravintola-ja-baari", + de: "Restaurant-und-Bar", +} + +const params = { + about, + amenities, + wellnessAndExercise, + activities, + meetingsAndConferences, + restaurantAndBar, +} export default params diff --git a/types/components/cardImage.ts b/types/components/cardImage.ts new file mode 100644 index 000000000..9976a2db3 --- /dev/null +++ b/types/components/cardImage.ts @@ -0,0 +1,7 @@ +import type { CardProps } from "@/components/TempDesignSystem/Card/card" +import type { FacilityCard } from "./hotelPage/facilities" + +export interface CardImageProps extends React.HTMLAttributes { + card: FacilityCard | undefined + imageCards: Pick[] +} diff --git a/types/components/hotelPage/facilities.ts b/types/components/hotelPage/facilities.ts new file mode 100644 index 000000000..228093fe0 --- /dev/null +++ b/types/components/hotelPage/facilities.ts @@ -0,0 +1,19 @@ +import type { CardProps } from "@/components/TempDesignSystem/Card/card" + +interface ColumnSpanOptions { + columnSpan: "one" | "two" | "three" +} + +export type FacilityCard = CardProps & ColumnSpanOptions + +export type Facility = Array + +export type Facilities = Array + +export type FacilityProps = { + facilities: Facilities +} + +export type CardGridProps = { + facility: Facility +} diff --git a/utils/cardTheme.ts b/utils/cardTheme.ts new file mode 100644 index 000000000..39f42a4c1 --- /dev/null +++ b/utils/cardTheme.ts @@ -0,0 +1,52 @@ +import type { ButtonProps } from "@/components/TempDesignSystem/Button/button" +import type { CardProps } from "@/components/TempDesignSystem/Card/card" +import type { LinkProps } from "@/components/TempDesignSystem/Link/link" + +export function getTheme(theme: CardProps["theme"]) { + let buttonTheme: ButtonProps["theme"] = "primaryLight" + let primaryLinkColor: LinkProps["color"] = "pale" + let secondaryLinkColor: LinkProps["color"] = "burgundy" + + switch (theme) { + case "one": + buttonTheme = "primaryLight" + primaryLinkColor = "pale" + secondaryLinkColor = "burgundy" + break + case "two": + buttonTheme = "secondaryLight" + primaryLinkColor = "pale" + secondaryLinkColor = "burgundy" + break + case "three": + buttonTheme = "tertiaryLight" + primaryLinkColor = "pale" + secondaryLinkColor = "burgundy" + break + case "primaryDark": + buttonTheme = "primaryDark" + primaryLinkColor = "burgundy" + secondaryLinkColor = "pale" + break + case "primaryDim": + buttonTheme = "primaryLight" + primaryLinkColor = "pale" + secondaryLinkColor = "burgundy" + break + case "primaryInverted": + buttonTheme = "primaryLight" + primaryLinkColor = "pale" + secondaryLinkColor = "burgundy" + break + case "primaryStrong" || "image": + buttonTheme = "primaryStrong" + primaryLinkColor = "red" + secondaryLinkColor = "white" + } + + return { + buttonTheme: buttonTheme, + primaryLinkColor: primaryLinkColor, + secondaryLinkColor: secondaryLinkColor, + } +} diff --git a/utils/imageCard.ts b/utils/imageCard.ts new file mode 100644 index 000000000..b66c65954 --- /dev/null +++ b/utils/imageCard.ts @@ -0,0 +1,18 @@ +import type { + Facility, + FacilityCard, +} from "@/types/components/hotelPage/facilities" + +export function sortCards(grid: Facility) { + const sortedCards = grid.slice(0).sort((a: FacilityCard, b: FacilityCard) => { + if (!a.backgroundImage && b.backgroundImage) { + return 1 + } + if (a.backgroundImage && !b.backgroundImage) { + return -1 + } + return 0 + }) + + return { card: sortedCards.pop(), images: sortedCards } +} From 7fd2fd7ec6ac0e648bf5484138e9eee08df45104 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 12 Aug 2024 16:36:17 +0200 Subject: [PATCH 052/319] feat(SW-159): merged contentstack and api requests for hotel query --- components/ContentType/HotelPage/index.tsx | 12 ---- server/errors/trpc.ts | 14 +++++ server/routers/hotels/input.ts | 4 -- server/routers/hotels/output.ts | 6 +- server/routers/hotels/query.ts | 69 ++++++++++++++++++---- server/trpc.ts | 7 ++- 6 files changed, 80 insertions(+), 32 deletions(-) diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index f29e8e28c..32be4113d 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -1,7 +1,5 @@ import { serverClient } from "@/lib/trpc/server" -import { getLang } from "@/i18n/serverContext" - import { MOCK_FACILITIES } from "./Facilities/mockData" import AmenitiesList from "./AmenitiesList" import Facilities from "./Facilities" @@ -13,17 +11,7 @@ import TabNavigation from "./TabNavigation" import styles from "./hotelPage.module.css" export default async function HotelPage() { - const hotelPageIdentifierData = - await serverClient().contentstack.hotelPage.get() - - if (!hotelPageIdentifierData) { - return null - } - const lang = getLang() - const hotelData = await serverClient().hotel.get({ - hotelId: hotelPageIdentifierData.hotel_page_id, - language: lang, include: ["RoomCategories"], }) if (!hotelData) { diff --git a/server/errors/trpc.ts b/server/errors/trpc.ts index e9884d2d9..ac61d9015 100644 --- a/server/errors/trpc.ts +++ b/server/errors/trpc.ts @@ -59,3 +59,17 @@ export function publicUnauthorizedError() { cause: new PublicUnauthorizedError(PUBLIC_UNAUTHORIZED), }) } + +export function serverErrorByStatus(status: number, cause?: unknown) { + switch (status) { + case 401: + return unauthorizedError(cause) + case 403: + return forbiddenError(cause) + case 404: + return notFound(cause) + case 500: + default: + return internalServerError(cause) + } +} diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 0b5d16b00..eb31e729d 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -1,10 +1,6 @@ import { z } from "zod" -import { Lang } from "@/constants/languages" - export const getHotelInputSchema = z.object({ - hotelId: z.string(), - language: z.nativeEnum(Lang), include: z .array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"])) .optional(), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index e7b85f7a3..9c55c56eb 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -233,7 +233,7 @@ const parkingPricingSchema = z.object({ localCurrency: z.object({ currency: z.string(), range: z.object({ - min: z.number(), + min: z.number().optional(), max: z.number().optional(), }), ordinary: z.array( @@ -284,8 +284,8 @@ const parkingSchema = z.object({ type: z.string(), name: z.string(), address: z.string(), - numberOfParkingSpots: z.number(), - numberOfChargingSpaces: z.number(), + numberOfParkingSpots: z.number().optional(), + numberOfChargingSpaces: z.number().optional(), distanceToHotel: z.number(), canMakeReservation: z.boolean(), pricing: parkingPricingSchema, diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 404af8510..5397fc66d 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -1,10 +1,24 @@ import { metrics } from "@opentelemetry/api" import * as api from "@/lib/api" -import { badRequestError } from "@/server/errors/trpc" -import { publicProcedure, router, serviceProcedure } from "@/server/trpc" +import { GetHotelPage } from "@/lib/graphql/Query/HotelPage.graphql" +import { request } from "@/lib/graphql/request" +import { + badRequestError, + notFound, + serverErrorByStatus, +} from "@/server/errors/trpc" +import { + contentStackUidWithServiceProcedure, + publicProcedure, + router, +} from "@/server/trpc" import { toApiLang } from "@/server/utils" +import { + HotelPageDataRaw, + validateHotelPageSchema, +} from "../contentstack/hotelPage/output" import { getFiltersInputSchema, getHotelInputSchema, @@ -24,23 +38,54 @@ const getHotelCounter = meter.createCounter("trpc.hotel.get") const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success") const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail") +const getHotelId = async (locale: string, uid: string | null | undefined) => { + const rawContentStackData = await request(GetHotelPage, { + locale, + uid, + }) + + if (!rawContentStackData.data) { + throw notFound(rawContentStackData) + } + + const hotelPageData = validateHotelPageSchema.safeParse( + rawContentStackData.data + ) + + if (!hotelPageData.success) { + console.error( + `Failed to validate Hotel Page - (uid: ${uid}, lang: ${locale})` + ) + console.error(hotelPageData.error) + return null + } + + return hotelPageData.data.hotel_page.hotel_page_id +} + export const hotelQueryRouter = router({ - get: serviceProcedure + get: contentStackUidWithServiceProcedure .input(getHotelInputSchema) .query(async ({ ctx, input }) => { - const { hotelId, language, include } = input - getHotelCounter.add(1, { hotelId, language, include }) + const { lang, uid } = ctx + const { include } = input + const hotelId = await getHotelId(lang, uid) - const apiLang = toApiLang(language) + if (!hotelId) { + throw notFound(`Hotel not found for uid: ${uid}`) + } + + const apiLang = toApiLang(lang) const params: Record = { hotelId, language: apiLang, } + if (include) { params.include = include.join(",") } - getHotelCounter.add(1, { hotelId, language, include }) + getHotelCounter.add(1, { hotelId, lang, include }) console.info( "api.hotels.hotel start", JSON.stringify({ @@ -62,7 +107,7 @@ export const hotelQueryRouter = router({ const text = await apiResponse.text() getHotelFailCounter.add(1, { hotelId, - language, + lang, include, error_type: "http_error", error: JSON.stringify({ @@ -82,7 +127,7 @@ export const hotelQueryRouter = router({ }, }) ) - return null + throw serverErrorByStatus(apiResponse.status, apiResponse) } const apiJson = await apiResponse.json() const validatedHotelData = getHotelDataSchema.safeParse(apiJson) @@ -90,7 +135,7 @@ export const hotelQueryRouter = router({ if (!validatedHotelData.success) { getHotelFailCounter.add(1, { hotelId, - language, + lang, include, error_type: "validation_error", error: JSON.stringify(validatedHotelData.error), @@ -116,7 +161,7 @@ export const hotelQueryRouter = router({ if (!validatedRoom.success) { getHotelFailCounter.add(1, { hotelId, - language, + lang, include, error_type: "validation_error", error: JSON.stringify( @@ -140,7 +185,7 @@ export const hotelQueryRouter = router({ }) : [] - getHotelSuccessCounter.add(1, { hotelId, language, include }) + getHotelSuccessCounter.add(1, { hotelId, lang, include }) console.info( "api.hotels.hotel success", JSON.stringify({ diff --git a/server/trpc.ts b/server/trpc.ts index 4f3a6f4f9..7a7a7f7ea 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -4,13 +4,13 @@ import { ZodError } from "zod" import { env } from "@/env/server" +import { type Context, createContext } from "./context" import { badRequestError, internalServerError, sessionExpiredError, unauthorizedError, } from "./errors/trpc" -import { type Context, createContext } from "./context" import { fetchServiceToken } from "./tokenManager" import { transformer } from "./transformer" @@ -146,3 +146,8 @@ export const protectedServerActionProcedure = serverActionProcedure.use( }) } ) + +// NOTE: This is actually save to use, just the implementation could change +// in minor version bumps. Please read: https://trpc.io/docs/faq#unstable +export const contentStackUidWithServiceProcedure = + contentstackExtendedProcedureUID.unstable_concat(serviceProcedure) From 1803e8e1fbb4f587b1bcccdd8bbcbe49945aae1a Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 22 Aug 2024 09:48:18 +0200 Subject: [PATCH 053/319] fix(SW-159): renamed route to get instead of getHotel and changed getHotelId to function declaration --- server/routers/hotels/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 5397fc66d..2b99bf9d3 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -38,7 +38,7 @@ const getHotelCounter = meter.createCounter("trpc.hotel.get") const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success") const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail") -const getHotelId = async (locale: string, uid: string | null | undefined) => { +async function getHotelId(locale: string, uid: string | null | undefined) { const rawContentStackData = await request(GetHotelPage, { locale, uid, From db042dbfde904f2312123f7f281ff4fd2349db0c Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 30 Aug 2024 08:48:28 +0200 Subject: [PATCH 054/319] feat(SW-187) Secondary navigation --- components/Footer/Details/details.module.css | 2 +- .../Navigation/MainNav/mainnav.module.css | 2 +- .../Footer/Navigation/SecondaryNav/index.tsx | 51 +++++++++++-------- .../SecondaryNav/secondarynav.module.css | 2 +- components/Footer/Navigation/index.tsx | 4 +- .../Footer/Navigation/navigation.module.css | 2 +- server/routers/contentstack/base/output.ts | 4 +- server/routers/contentstack/base/query.ts | 12 ++++- types/components/footer/navigation.ts | 14 ++--- 9 files changed, 57 insertions(+), 36 deletions(-) diff --git a/components/Footer/Details/details.module.css b/components/Footer/Details/details.module.css index adbf86603..51ee8f102 100644 --- a/components/Footer/Details/details.module.css +++ b/components/Footer/Details/details.module.css @@ -52,7 +52,7 @@ gap: var(--Spacing-x1); } -@media screen and (min-width: 1367px) { +@media screen and (min-width: 767px) { .details { padding: var(--Spacing-x6) var(--Spacing-x5) var(--Spacing-x4); } diff --git a/components/Footer/Navigation/MainNav/mainnav.module.css b/components/Footer/Navigation/MainNav/mainnav.module.css index 933c9e774..496fa9618 100644 --- a/components/Footer/Navigation/MainNav/mainnav.module.css +++ b/components/Footer/Navigation/MainNav/mainnav.module.css @@ -22,7 +22,7 @@ justify-content: space-between; } -@media screen and (min-width: 1367px) { +@media screen and (min-width: 767px) { .mainNavigation { max-width: 360px; } diff --git a/components/Footer/Navigation/SecondaryNav/index.tsx b/components/Footer/Navigation/SecondaryNav/index.tsx index 7414a23e4..798035069 100644 --- a/components/Footer/Navigation/SecondaryNav/index.tsx +++ b/components/Footer/Navigation/SecondaryNav/index.tsx @@ -13,6 +13,7 @@ export default function FooterSecondaryNav({ secondaryLinks, appDownloads, }: FooterSecondaryNavProps) { + console.log("secondaryLinks", secondaryLinks[0].links) return (
))} diff --git a/components/Footer/Navigation/SecondaryNav/secondarynav.module.css b/components/Footer/Navigation/SecondaryNav/secondarynav.module.css index 3f47d32c7..057c1daa5 100644 --- a/components/Footer/Navigation/SecondaryNav/secondarynav.module.css +++ b/components/Footer/Navigation/SecondaryNav/secondarynav.module.css @@ -25,7 +25,7 @@ margin: 0; } -@media screen and (min-width: 1367px) { +@media screen and (min-width: 767px) { .secondaryNavigation { margin-top: 0; gap: 80px; diff --git a/components/Footer/Navigation/index.tsx b/components/Footer/Navigation/index.tsx index 0e66fb5dd..a49148541 100644 --- a/components/Footer/Navigation/index.tsx +++ b/components/Footer/Navigation/index.tsx @@ -1,12 +1,10 @@ -import { footer } from "../mockedData" import FooterMainNav from "./MainNav" import FooterSecondaryNav from "./SecondaryNav" import styles from "./navigation.module.css" export default function FooterNavigation({ ...props }) { - const { mainLinks } = props - const { secondaryLinks, appDownloads } = footer + const { mainLinks, secondaryLinks, appDownloads } = props return (
diff --git a/components/Footer/Navigation/navigation.module.css b/components/Footer/Navigation/navigation.module.css index df5dfbcf5..040577f93 100644 --- a/components/Footer/Navigation/navigation.module.css +++ b/components/Footer/Navigation/navigation.module.css @@ -11,7 +11,7 @@ max-width: var(--max-width-content); } -@media screen and (min-width: 1367px) { +@media screen and (min-width: 767px) { .section { padding: var(--Spacing-x9) 0; } diff --git a/server/routers/contentstack/base/output.ts b/server/routers/contentstack/base/output.ts index 9b1773847..7202a04ed 100644 --- a/server/routers/contentstack/base/output.ts +++ b/server/routers/contentstack/base/output.ts @@ -273,8 +273,8 @@ const validateInternalLink = z edges: z.array( z.object({ node: z.object({ - title: z.string(), - url: z.string(), + title: z.string().optional(), + url: z.string().optional(), }), }) ), diff --git a/server/routers/contentstack/base/query.ts b/server/routers/contentstack/base/query.ts index 52beab486..3ecf39e35 100644 --- a/server/routers/contentstack/base/query.ts +++ b/server/routers/contentstack/base/query.ts @@ -390,11 +390,21 @@ export const baseQueryRouter = router({ const mainLinks = transformPageConnectionLinks( validatedFooterData.main_links ) + console.log( + "secondary_links ", + validatedFooterData.secondary_links[0].links[0].pageConnection?.edges + ) + const secondaryLinks = validatedFooterData.secondary_links.map( + (section) => ({ + ...section, + links: transformPageConnectionLinks(section.links), + }) + ) return { mainLinks: mainLinks, appDownloads: validatedFooterData.app_downloads, - secondaryLinks: validatedFooterData.secondary_links, + secondaryLinks: secondaryLinks, } }), }) diff --git a/types/components/footer/navigation.ts b/types/components/footer/navigation.ts index c485431cc..341b4d117 100644 --- a/types/components/footer/navigation.ts +++ b/types/components/footer/navigation.ts @@ -8,10 +8,10 @@ export type FooterMainNavProps = { export type FooterSecondaryNav = { id: string - href: string - title: string - openInNewTab: boolean isExternal: boolean + openInNewTab: boolean + title: string + url: string } export type FooterSecondaryNavProps = { secondaryLinks: { @@ -21,9 +21,11 @@ export type FooterSecondaryNavProps = { appDownloads: { title: string links: { - title: string - href: string - id: string + href: { + href: string + title: string + } + type: string }[] } } From 95bbbe41526621cc34a3df38b958e8994f313982 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 21 Aug 2024 14:08:15 +0200 Subject: [PATCH 055/319] feat(SW-185): Footer mobile adjustments --- components/Footer/mockedData.ts | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/components/Footer/mockedData.ts b/components/Footer/mockedData.ts index 0d6a57780..c1bb57e80 100644 --- a/components/Footer/mockedData.ts +++ b/components/Footer/mockedData.ts @@ -190,3 +190,67 @@ export const footer = { }, }, } + +export const detailsData = { + copyrightCompany: "© 2024 Scandic AB", + copyrightInfo: "All rights reserved.", + social: { + links: [ + { + title: "Facebook", + href: "https://www.facebook.com/scandichotels/", + id: "facebook", + }, + { + title: "Instagram", + href: "https://www.instagram.com/scandichotels/", + id: "instagram", + }, + { + title: "Tripadvisor", + href: "https://www.tripadvisor.com/Hotel_Review-g297628-d1020208712-Reviews-Scandic_Hotels-Stockholm_Sweden.html", + id: "tripadvisor", + }, + ], + }, + links: [ + { + title: "Cookies", + href: "/cookies", + id: "cookies", + }, + { + title: "Privacy policy", + href: "/privacy", + id: "privacy", + }, + ], + languageSwitcher: { + urls: { + da: { + url: "https://www.scandichotels.com/da", + isExternal: true, + }, + de: { + url: "https://www.scandichotels.com/de", + isExternal: true, + }, + en: { + url: "https://www.scandichotels.com/en", + isExternal: true, + }, + fi: { + url: "https://www.scandichotels.com/fi", + isExternal: true, + }, + no: { + url: "https://www.scandichotels.com/no", + isExternal: true, + }, + sv: { + url: "https://www.scandichotels.com/sv", + isExternal: true, + }, + }, + }, +} From 59615cef94d8cddb45af4042a28e3c8531518673 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 20 Aug 2024 08:07:21 +0200 Subject: [PATCH 056/319] feat(SW-96): Ship reusable desktop lightbox --- components/ContentType/HotelPage/data.ts | 42 ++ components/ContentType/HotelPage/index.tsx | 9 +- .../Desktop/desktopLightbox.module.css | 157 ++++++ components/Lightbox/Desktop/index.tsx | 249 ++++++++++ next.config.js | 8 + package-lock.json | 461 +++++++++++++++++- package.json | 2 + types/components/lightbox/desktopLightbox.ts | 26 + 8 files changed, 937 insertions(+), 17 deletions(-) create mode 100644 components/Lightbox/Desktop/desktopLightbox.module.css create mode 100644 components/Lightbox/Desktop/index.tsx create mode 100644 types/components/lightbox/desktopLightbox.ts diff --git a/components/ContentType/HotelPage/data.ts b/components/ContentType/HotelPage/data.ts index ace3a0408..e6aa284a7 100644 --- a/components/ContentType/HotelPage/data.ts +++ b/components/ContentType/HotelPage/data.ts @@ -3,6 +3,7 @@ import { FC } from "react" import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import { IconName, IconProps } from "@/types/components/icon" +import { ImageItem } from "@/types/components/lightbox/desktopLightbox" const facilityToIconMap: { [key: string]: IconName } = { Bar: IconName.Bar, @@ -21,3 +22,44 @@ export function mapFacilityToIcon(facilityName: string): FC | null { const iconName = facilityToIconMap[facilityName] return getIconByIconName(iconName) || null } + +/** + * NOTE: + * Images from the test API are hosted on test3.scandichotels.com, + * which can't be accessed unless on Scandic's Wifi or using Citrix. + * So I've added some placeholder images here for use when running the app locally. + */ +export const tempHotelLightboxImage: ImageItem[] = [ + { + url: "https://www.scandichotels.com/imagevault/publishedmedia/kd4e35n41mrqinfrkzok/scandic-continental-entrance-vasagatan.jpg", + alt: "Hotel entrance", + }, + { + url: "https://www.scandichotels.com/imageVault/publishedmedia/ip4a2rc93qda3aq9ph73/scandic-continental-room-standard.jpg", + alt: "Standard room", + }, + { + url: "https://www.scandichotels.com/imageVault/publishedmedia/75y2b2x8uvma4s7zvy50/scandic-continental-room-juniorsuite-detail-1.jpg", + alt: "Junior suite", + }, + { + url: "https://www.scandichotels.com/imageVault/publishedmedia/pwotq99pbr68djxnym4a/scandic-continental-diningroom-themarket.jpg", + alt: "Dining room, The Market", + }, + { + url: "https://www.scandichotels.com/imageVault/publishedmedia/yc2cjoxq2j01gp0f80gt/scandic-continental-room-juniorsuite-bathroom-1.jpg", + alt: "Junior suite bathroom", + }, + { + url: "https://www.scandichotels.com/imageVault/publishedmedia/xocam1f69h7lh57u8f9v/scandic-continental-themarket-detail.jpg", + alt: "The Market detail", + }, + { + url: "https://www.scandichotels.com/imageVault/publishedmedia/0nnwt72vwzfvkkx5whf0/scandic-continental-caldo.jpg", + alt: "Caldo", + }, + { + url: "https://www.scandichotels.com/imageVault/publishedmedia/mg9bbtbumfxbfjhovk2e/Scandic_Continental_Capitol_The_View_6.jpg", + alt: "Rooftop bar", + }, +] diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 32be4113d..63a40ad2f 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -1,8 +1,11 @@ import { serverClient } from "@/lib/trpc/server" import { MOCK_FACILITIES } from "./Facilities/mockData" -import AmenitiesList from "./AmenitiesList" import Facilities from "./Facilities" +import { DesktopLightbox } from "@/components/Lightbox/Desktop" + +import AmenitiesList from "./AmenitiesList" +import { tempHotelLightboxImage } from "./data" import IntroSection from "./IntroSection" import { Rooms } from "./Rooms" import SidePeeks from "./SidePeeks" @@ -21,6 +24,10 @@ export default async function HotelPage() { return (
+ + + +
diff --git a/components/Lightbox/Desktop/desktopLightbox.module.css b/components/Lightbox/Desktop/desktopLightbox.module.css new file mode 100644 index 000000000..5272d98eb --- /dev/null +++ b/components/Lightbox/Desktop/desktopLightbox.module.css @@ -0,0 +1,157 @@ +.content { + background-color: white; + border-radius: 6px; + box-shadow: + hsl(206 22% 7% / 35%) 0px 10px 38px -10px, + hsl(206 22% 7% / 20%) 0px 10px 20px -15px; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 1090px; + height: 725px; + overflow: hidden; +} + +.overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); +} + +.galleryContainer { + padding: var(--Spacing-x5) var(--Spacing-x6); + height: 100%; + display: flex; + flex-direction: column; + position: relative; +} + +.closeButton { + position: absolute; + top: var(--Spacing-x-one-and-half); + right: var(--Spacing-x-half); +} + +.backButton { + position: absolute; + top: var(--Spacing-x-one-and-half); + left: var(--Spacing-x-half); +} + +.galleryHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--Spacing-x1); +} + +.imageCaption { + background-color: #f0f0f0; + padding: 5px 10px; + border-radius: 4px; +} + +.mainImageContainer { + flex: 1; + position: relative; + margin-bottom: 20px; +} + +.mainImageContainer img, +.thumbnailContainer img { + border-radius: var(--Corner-radius-Small); + cursor: pointer; +} + +.thumbnailGrid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 10px; +} + +.thumbnailContainer { + position: relative; + height: 125px; +} + +.fullViewContainer { + background-color: var(--UI-Text-High-contrast); + color: #fff; + height: 100%; + padding: var(--Spacing-x5); + display: flex; + flex-direction: column; + align-items: center; +} + +.fullViewHeader { + display: flex; + justify-content: center; + margin-bottom: var(--Spacing-x5); + width: 100%; +} + +.fullViewImageContainer { + position: relative; + width: 100%; + max-width: 1054px; + height: 700px; + margin-bottom: var(--Spacing-x5); +} + +.fullViewImage { + position: absolute; + width: 100%; + height: 100%; +} + +.fullViewFooter { + width: 100%; + max-width: 1054px; + text-align: left; + padding-left: 116px; +} + +.imagePosition { + background-color: var(--UI-Grey-90); + padding: var(--Spacing-x-quarter) var(--Spacing-x-half); + border-radius: var(--Corner-radius-Small); +} + +.fullViewImageContainer img { + border-radius: var(--Corner-radius-Medium); + cursor: pointer; +} + +.navigationButton { + position: absolute; + top: 50%; + transform: translateY(-50%); + background-color: var(--Base-Button-Inverted-Fill-Normal); + border-radius: 50%; + padding: var(--Spacing-x1); + cursor: pointer; + border: none; + display: flex; +} + +.navigationButton:hover { + background-color: var(--Base-Button-Inverted-Fill-Hover); +} + +.prevButton { + left: 10px; +} + +.leftTransformIcon { + transform: scaleX(-1); +} + +.nextButton { + right: 10px; +} + +.portraitImage { + max-width: 548px; +} diff --git a/components/Lightbox/Desktop/index.tsx b/components/Lightbox/Desktop/index.tsx new file mode 100644 index 000000000..2148e8226 --- /dev/null +++ b/components/Lightbox/Desktop/index.tsx @@ -0,0 +1,249 @@ +"use client" +import * as Dialog from "@radix-ui/react-dialog" +import { AnimatePresence, motion } from "framer-motion" +import Image from "next/image" +import React, { useState } from "react" + +import { ChevronRightIcon } from "@/components/Icons" +import ArrowRightIcon from "@/components/Icons/ArrowRight" +import CloseIcon from "@/components/Icons/Close" +import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import styles from "./desktopLightbox.module.css" + +import { + DesktopLightboxProps, + FullViewProps, + GalleryProps, +} from "@/types/components/lightbox/desktopLightbox" + +export function DesktopLightbox({ images, children }: DesktopLightboxProps) { + const [isOpen, setIsOpen] = useState(false) + const [selectedImageIndex, setSelectedImageIndex] = useState(0) + const [isFullView, setIsFullView] = useState(false) + + const handleOpenChange = (open: boolean) => { + if (!open) { + setTimeout(() => { + setIsOpen(false) + setSelectedImageIndex(0) + setIsFullView(false) + }, 300) // 300ms delay + } else { + setIsOpen(true) + } + } + + const handleNext = () => { + setSelectedImageIndex((prevIndex) => (prevIndex + 1) % images.length) + } + + const handlePrev = () => { + setSelectedImageIndex( + (prevIndex) => (prevIndex - 1 + images.length) % images.length + ) + } + + const triggerElement = React.Children.map(children, (child) => { + if (React.isValidElement(child) && child.props.id === "lightboxTrigger") { + return React.cloneElement(child, { + onClick: () => setIsOpen(true), + } as React.HTMLAttributes) + } + return child + }) + + return ( + <> + {triggerElement} + + + {isOpen && ( + + + + + + + {isFullView ? ( + setIsFullView(false)} + onNext={handleNext} + onPrev={handlePrev} + currentIndex={selectedImageIndex} + totalImages={images.length} + /> + ) : ( + setIsOpen(false)} + onSelectImage={(image) => { + setSelectedImageIndex( + images.findIndex((img) => img.url === image.url) + ) + }} + onImageClick={() => setIsFullView(true)} + selectedImage={images[selectedImageIndex]} + /> + )} + + + + )} + + + + ) +} + +function Gallery({ + images, + onClose, + onSelectImage, + onImageClick, + selectedImage, +}: GalleryProps) { + const mainImage = selectedImage || images[0] + const mainImageIndex = images.findIndex((img) => img.url === mainImage.url) + + function getThumbImages() { + const thumbs = [] + for (let i = 1; i <= 5; i++) { + const index = (mainImageIndex + i) % images.length + thumbs.push(images[index]) + } + return thumbs + } + + return ( +
+ +
+
+ {mainImage.alt} +
+
+
+ {mainImage.alt} +
+
+ {getThumbImages().map((image, index) => ( + onSelectImage(image)} + > + {image.alt} + + ))} +
+
+ ) +} + +function FullView({ + image, + onClose, + onNext, + onPrev, + currentIndex, + totalImages, +}: FullViewProps) { + return ( +
+ +
+ + + {`${currentIndex + 1} / ${totalImages}`} + + +
+
+ + + {image.alt} + + + + + + + + +
+
+ {image.alt} +
+
+ ) +} diff --git a/next.config.js b/next.config.js index 01fb628ea..e48838c84 100644 --- a/next.config.js +++ b/next.config.js @@ -43,6 +43,14 @@ const nextConfig = { protocol: "https", hostname: "imagevault.scandichotels.com", }, + { + protocol: "https", + hostname: "test3.scandichotels.com", + }, + { + protocol: "https", + hostname: "www.scandichotels.com", + }, ], }, diff --git a/package-lock.json b/package-lock.json index 05aaa94fb..dd2ab62da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@netlify/plugin-nextjs": "^5.1.1", "@opentelemetry/api": "^1.9.0", "@opentelemetry/sdk-metrics": "^1.25.1", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-slot": "^1.0.2", "@react-aria/ssr": "^3.9.5", "@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.8", @@ -30,6 +31,7 @@ "dayjs": "^1.11.10", "deepmerge": "^4.3.1", "fetch-retry": "^6.0.0", + "framer-motion": "^11.3.28", "graphql": "^16.8.1", "graphql-request": "^6.1.0", "graphql-tag": "^2.12.6", @@ -2363,6 +2365,7 @@ "version": "7.24.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3697,16 +3700,18 @@ "node": ">=14" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, "node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -3714,17 +3719,276 @@ } } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", + "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", + "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", + "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", + "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", + "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", + "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -6142,7 +6406,7 @@ "version": "18.2.24", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.24.tgz", "integrity": "sha512-cN6upcKd8zkGy4HU9F1+/s98Hrp6D4MOcippK4PoE8OZRngohHZpbJn1GsaDLz87MqvHNoT13nHvNqM9ocRHZg==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } @@ -6596,6 +6860,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -8591,6 +8866,11 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/devtools-protocol": { "version": "0.0.1045489", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1045489.tgz", @@ -9924,6 +10204,30 @@ "node": ">= 0.12" } }, + "node_modules/framer-motion": { + "version": "11.3.28", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.28.tgz", + "integrity": "sha512-dqhoawipEAjqdv32zbv72sOMJZjol7dROWn7t/FOq23WXJ40O4OUybgnO2ldnuS+3YquSn8xO/KKRavZ+TBVOQ==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -10065,6 +10369,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -10770,6 +11082,14 @@ "tslib": "^2.4.0" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -15749,6 +16069,51 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-remove-scroll": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.4", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-stately": { "version": "3.30.1", "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.30.1.tgz", @@ -15783,6 +16148,28 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -15888,7 +16275,8 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -17898,6 +18286,47 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index 426eef100..948db7f1a 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@netlify/plugin-nextjs": "^5.1.1", "@opentelemetry/api": "^1.9.0", "@opentelemetry/sdk-metrics": "^1.25.1", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-slot": "^1.0.2", "@react-aria/ssr": "^3.9.5", "@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.8", @@ -46,6 +47,7 @@ "dayjs": "^1.11.10", "deepmerge": "^4.3.1", "fetch-retry": "^6.0.0", + "framer-motion": "^11.3.28", "graphql": "^16.8.1", "graphql-request": "^6.1.0", "graphql-tag": "^2.12.6", diff --git a/types/components/lightbox/desktopLightbox.ts b/types/components/lightbox/desktopLightbox.ts new file mode 100644 index 000000000..b12e823e3 --- /dev/null +++ b/types/components/lightbox/desktopLightbox.ts @@ -0,0 +1,26 @@ +export interface ImageItem { + url: string + alt: string +} + +export interface DesktopLightboxProps { + images: ImageItem[] + children: React.ReactNode +} + +export interface GalleryProps { + images: ImageItem[] + onClose: () => void + onSelectImage: (image: ImageItem) => void + onImageClick: () => void + selectedImage: ImageItem | null +} + +export interface FullViewProps { + image: ImageItem + onClose: () => void + onNext: () => void + onPrev: () => void + currentIndex: number + totalImages: number +} From b04b5ca23be2380065452ddd75dc385c2563eea9 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 20 Aug 2024 08:29:30 +0200 Subject: [PATCH 057/319] feat(SW-96): ship lightbox preview images on hotel page --- .../HotelPage/hotelPage.module.css | 47 +++++++++++++++++++ components/ContentType/HotelPage/index.tsx | 28 ++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index 3709e478b..037fbb47a 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -7,6 +7,11 @@ padding-right: var(--Spacing-x3); } +.desktopGrid, +.seeAllButton { + display: none; +} + .mainSection { display: grid; gap: var(--Spacing-x9); @@ -34,4 +39,46 @@ gap: var(--Spacing-x9); grid-template-columns: 607px 340px; } + + .desktopGrid { + display: grid; + grid-template-columns: 72% 28%; + grid-template-rows: repeat(2, 1fr); + gap: 8px; + position: relative; + width: 100%; + padding-top: var(--Spacing-x2); + background-color: var(--Base-Surface-Subtle-Normal); + } + + .desktopGrid > :first-child { + grid-row: 1 / span 2; + height: 100%; + } + + .desktopGrid > :nth-child(2) { + grid-row: 1; + grid-column: 2; + } + .desktopGrid > :nth-child(3) { + grid-row: 2; + grid-column: 2; + } + + .seeAllButton { + position: absolute; + bottom: var(--Spacing-x4); + right: var(--Spacing-x4); + z-index: 1; + display: block; + } + + .baseImage { + cursor: pointer; + object-fit: cover; + border-radius: var(--Corner-radius-Medium); + display: block; + width: 100%; + height: 100%; + } } diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 63a40ad2f..85b76ebb8 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -1,8 +1,11 @@ +import Image from "next/image" + import { serverClient } from "@/lib/trpc/server" import { MOCK_FACILITIES } from "./Facilities/mockData" import Facilities from "./Facilities" import { DesktopLightbox } from "@/components/Lightbox/Desktop" +import Button from "@/components/TempDesignSystem/Button" import AmenitiesList from "./AmenitiesList" import { tempHotelLightboxImage } from "./data" @@ -25,8 +28,29 @@ export default async function HotelPage() { return (
- - +
+ {/*TODO: Replace with images from API once SW-189 is merged. */} + {tempHotelLightboxImage.slice(0, 3).map((image, index) => ( + {image.alt} + ))} +
+
From ee4211d20c0c3913b163a071d907c8d43889013b Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 20 Aug 2024 09:35:36 +0200 Subject: [PATCH 058/319] fix(SW-96): allow nested children to trigger lightbox --- components/Lightbox/Desktop/index.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/components/Lightbox/Desktop/index.tsx b/components/Lightbox/Desktop/index.tsx index 2148e8226..9ad9a6771 100644 --- a/components/Lightbox/Desktop/index.tsx +++ b/components/Lightbox/Desktop/index.tsx @@ -46,14 +46,23 @@ export function DesktopLightbox({ images, children }: DesktopLightboxProps) { ) } - const triggerElement = React.Children.map(children, (child) => { - if (React.isValidElement(child) && child.props.id === "lightboxTrigger") { - return React.cloneElement(child, { - onClick: () => setIsOpen(true), - } as React.HTMLAttributes) + const triggerElement = React.Children.map( + children, + function mapChild(child): React.ReactNode { + if (React.isValidElement(child)) { + if (child.props.id === "lightboxTrigger") { + return React.cloneElement(child, { + onClick: () => setIsOpen(true), + } as React.HTMLAttributes) + } else if (child.props.children) { + return React.cloneElement(child, { + children: React.Children.map(child.props.children, mapChild), + } as React.HTMLAttributes) + } + } + return child } - return child - }) + ) return ( <> From 3a7273536cce60e1642d612f75a27c34350e908a Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 20 Aug 2024 09:37:20 +0200 Subject: [PATCH 059/319] chore(SW-96): abstract preview images out of hotelpage component --- .../HotelPage/PreviewImages/index.tsx | 43 +++++++++++++++++ .../PreviewImages/previewImages.module.css | 47 +++++++++++++++++++ .../HotelPage/hotelPage.module.css | 47 ------------------- components/ContentType/HotelPage/index.tsx | 31 +----------- 4 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 components/ContentType/HotelPage/PreviewImages/index.tsx create mode 100644 components/ContentType/HotelPage/PreviewImages/previewImages.module.css diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx new file mode 100644 index 000000000..c8b85698e --- /dev/null +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -0,0 +1,43 @@ +import Image from "@/components/Image" +import { DesktopLightbox } from "@/components/Lightbox/Desktop" +import Button from "@/components/TempDesignSystem/Button" +import { getIntl } from "@/i18n" + +import styles from "./previewImages.module.css" + +import { ImageItem } from "@/types/components/lightbox/desktopLightbox" + +export default async function PreviewImages({ + images, +}: { + images: ImageItem[] +}) { + const intl = await getIntl() + return ( + +
+ {/*TODO: Replace with images from API once SW-189 is merged. */} + {images.slice(0, 3).map((image, index) => ( + {image.alt} + ))} + +
+
+ ) +} diff --git a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css new file mode 100644 index 000000000..fdf7ef319 --- /dev/null +++ b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css @@ -0,0 +1,47 @@ +.desktopGrid { + display: none; +} + +@media screen and (min-width: 1367px) { + .desktopGrid { + display: grid; + grid-template-columns: 72% 28%; + grid-template-rows: repeat(2, 1fr); + gap: 8px; + position: relative; + width: 100%; + padding-top: var(--Spacing-x2); + background-color: var(--Base-Surface-Subtle-Normal); + } + + .desktopGrid > :first-child { + grid-row: 1 / span 2; + height: 100%; + } + + .desktopGrid > :nth-child(2) { + grid-row: 1; + grid-column: 2; + } + .desktopGrid > :nth-child(3) { + grid-row: 2; + grid-column: 2; + } + + .seeAllButton { + position: absolute; + bottom: var(--Spacing-x2); + right: var(--Spacing-x4); + z-index: 1; + display: block; + } + + .baseImage { + cursor: pointer; + object-fit: cover; + border-radius: var(--Corner-radius-Medium); + display: block; + width: 100%; + height: 100%; + } +} diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index 037fbb47a..3709e478b 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -7,11 +7,6 @@ padding-right: var(--Spacing-x3); } -.desktopGrid, -.seeAllButton { - display: none; -} - .mainSection { display: grid; gap: var(--Spacing-x9); @@ -39,46 +34,4 @@ gap: var(--Spacing-x9); grid-template-columns: 607px 340px; } - - .desktopGrid { - display: grid; - grid-template-columns: 72% 28%; - grid-template-rows: repeat(2, 1fr); - gap: 8px; - position: relative; - width: 100%; - padding-top: var(--Spacing-x2); - background-color: var(--Base-Surface-Subtle-Normal); - } - - .desktopGrid > :first-child { - grid-row: 1 / span 2; - height: 100%; - } - - .desktopGrid > :nth-child(2) { - grid-row: 1; - grid-column: 2; - } - .desktopGrid > :nth-child(3) { - grid-row: 2; - grid-column: 2; - } - - .seeAllButton { - position: absolute; - bottom: var(--Spacing-x4); - right: var(--Spacing-x4); - z-index: 1; - display: block; - } - - .baseImage { - cursor: pointer; - object-fit: cover; - border-radius: var(--Corner-radius-Medium); - display: block; - width: 100%; - height: 100%; - } } diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 85b76ebb8..a1838403e 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -1,15 +1,12 @@ -import Image from "next/image" - import { serverClient } from "@/lib/trpc/server" import { MOCK_FACILITIES } from "./Facilities/mockData" import Facilities from "./Facilities" -import { DesktopLightbox } from "@/components/Lightbox/Desktop" -import Button from "@/components/TempDesignSystem/Button" import AmenitiesList from "./AmenitiesList" import { tempHotelLightboxImage } from "./data" import IntroSection from "./IntroSection" +import PreviewImages from "./PreviewImages" import { Rooms } from "./Rooms" import SidePeeks from "./SidePeeks" import TabNavigation from "./TabNavigation" @@ -27,31 +24,7 @@ export default async function HotelPage() { return (
- -
- {/*TODO: Replace with images from API once SW-189 is merged. */} - {tempHotelLightboxImage.slice(0, 3).map((image, index) => ( - {image.alt} - ))} -
- -
+
From e47956816e240a0bfba6e381b7f62ecf3ec461c1 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 20 Aug 2024 10:38:43 +0200 Subject: [PATCH 060/319] refactor(SW-96): improve dialog exit animation + cleanup css module --- .../PreviewImages/previewImages.module.css | 1 - .../Desktop/desktopLightbox.module.css | 25 ++++---- components/Lightbox/Desktop/index.tsx | 61 ++++++++++++------- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css index fdf7ef319..f333c4ead 100644 --- a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css +++ b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css @@ -37,7 +37,6 @@ } .baseImage { - cursor: pointer; object-fit: cover; border-radius: var(--Corner-radius-Medium); display: block; diff --git a/components/Lightbox/Desktop/desktopLightbox.module.css b/components/Lightbox/Desktop/desktopLightbox.module.css index 5272d98eb..15460ab00 100644 --- a/components/Lightbox/Desktop/desktopLightbox.module.css +++ b/components/Lightbox/Desktop/desktopLightbox.module.css @@ -1,9 +1,5 @@ .content { - background-color: white; - border-radius: 6px; - box-shadow: - hsl(206 22% 7% / 35%) 0px 10px 38px -10px, - hsl(206 22% 7% / 20%) 0px 10px 20px -15px; + border-radius: var(--Corner-radius-Large); position: fixed; top: 50%; left: 50%; @@ -11,15 +7,18 @@ width: 1090px; height: 725px; overflow: hidden; + z-index: 10; } .overlay { position: fixed; inset: 0; background-color: rgba(0, 0, 0, 0.5); + z-index: 10; } .galleryContainer { + background-color: var(--Base-Background-Primary-Normal); padding: var(--Spacing-x5) var(--Spacing-x6); height: 100%; display: flex; @@ -48,26 +47,31 @@ .imageCaption { background-color: #f0f0f0; - padding: 5px 10px; + padding: var(--Spacing-x-half) var(--Spacing-x1); border-radius: 4px; } .mainImageContainer { flex: 1; position: relative; - margin-bottom: 20px; + margin-bottom: var(--Spacing-x2); + will-change: transform; + overflow: hidden; } .mainImageContainer img, .thumbnailContainer img { border-radius: var(--Corner-radius-Small); cursor: pointer; + transition: opacity 0.3s ease-in-out; } .thumbnailGrid { display: grid; grid-template-columns: repeat(5, 1fr); - gap: 10px; + gap: var(--Spacing-x1); + max-height: 125px; + overflow: hidden; } .thumbnailContainer { @@ -77,7 +81,6 @@ .fullViewContainer { background-color: var(--UI-Text-High-contrast); - color: #fff; height: 100%; padding: var(--Spacing-x5); display: flex; @@ -141,7 +144,7 @@ } .prevButton { - left: 10px; + left: 0; } .leftTransformIcon { @@ -149,7 +152,7 @@ } .nextButton { - right: 10px; + right: 0; } .portraitImage { diff --git a/components/Lightbox/Desktop/index.tsx b/components/Lightbox/Desktop/index.tsx index 9ad9a6771..c485bb47f 100644 --- a/components/Lightbox/Desktop/index.tsx +++ b/components/Lightbox/Desktop/index.tsx @@ -156,30 +156,45 @@ function Gallery({ {mainImage.alt}
-
- {mainImage.alt} -
+ + + {mainImage.alt} + +
- {getThumbImages().map((image, index) => ( - onSelectImage(image)} - > - {image.alt} - - ))} + + {getThumbImages().map((image, index) => ( + onSelectImage(image)} + initial={{ opacity: 0, x: 50 }} + animate={{ opacity: 1, x: 0 }} + exit={{ opacity: 0, x: -50 }} + transition={{ duration: 0.2, delay: index * 0.05 }} + > + {image.alt} + + ))} +
) From 442f8b6580f2f283f4f8ec4aab86bad54c67e8d3 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 20 Aug 2024 11:05:01 +0200 Subject: [PATCH 061/319] feat(SW-96): add navigation controls to gallery dialog --- .../Desktop/desktopLightbox.module.css | 20 ++++--- components/Lightbox/Desktop/index.tsx | 59 ++++++++++++++----- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/components/Lightbox/Desktop/desktopLightbox.module.css b/components/Lightbox/Desktop/desktopLightbox.module.css index 15460ab00..c5de574e4 100644 --- a/components/Lightbox/Desktop/desktopLightbox.module.css +++ b/components/Lightbox/Desktop/desktopLightbox.module.css @@ -46,15 +46,20 @@ } .imageCaption { - background-color: #f0f0f0; + background-color: var(--Base-Surface-Subtle-Normal); padding: var(--Spacing-x-half) var(--Spacing-x1); - border-radius: 4px; + border-radius: var(--Corner-radius-Small); +} + +.mainImageWrapper { + position: relative; + flex: 1; + margin-bottom: var(--Spacing-x2); } .mainImageContainer { - flex: 1; - position: relative; - margin-bottom: var(--Spacing-x2); + width: 100%; + height: 100%; will-change: transform; overflow: hidden; } @@ -137,6 +142,7 @@ cursor: pointer; border: none; display: flex; + z-index: 1; } .navigationButton:hover { @@ -144,7 +150,7 @@ } .prevButton { - left: 0; + left: var(--Spacing-x2); } .leftTransformIcon { @@ -152,7 +158,7 @@ } .nextButton { - right: 0; + right: var(--Spacing-x2); } .portraitImage { diff --git a/components/Lightbox/Desktop/index.tsx b/components/Lightbox/Desktop/index.tsx index c485bb47f..b8ced2630 100644 --- a/components/Lightbox/Desktop/index.tsx +++ b/components/Lightbox/Desktop/index.tsx @@ -140,6 +140,16 @@ function Gallery({ return thumbs } + const handleNext = () => { + const nextIndex = (mainImageIndex + 1) % images.length + onSelectImage(images[nextIndex]) + } + + const handlePrev = () => { + const prevIndex = (mainImageIndex - 1 + images.length) % images.length + onSelectImage(images[prevIndex]) + } + return (
- - + + + {mainImage.alt} + + + - {mainImage.alt} - - + + + + +
{getThumbImages().map((image, index) => ( From 24e6b5fffea432290fc794c58bcde750713ecba9 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 20 Aug 2024 11:58:01 +0200 Subject: [PATCH 062/319] chore(SW-96): cleanup css --- .../Lightbox/Desktop/desktopLightbox.module.css | 1 - components/Lightbox/Desktop/index.tsx | 11 +++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/components/Lightbox/Desktop/desktopLightbox.module.css b/components/Lightbox/Desktop/desktopLightbox.module.css index c5de574e4..47c91c216 100644 --- a/components/Lightbox/Desktop/desktopLightbox.module.css +++ b/components/Lightbox/Desktop/desktopLightbox.module.css @@ -3,7 +3,6 @@ position: fixed; top: 50%; left: 50%; - transform: translate(-50%, -50%); width: 1090px; height: 725px; overflow: hidden; diff --git a/components/Lightbox/Desktop/index.tsx b/components/Lightbox/Desktop/index.tsx index b8ced2630..a68e97c61 100644 --- a/components/Lightbox/Desktop/index.tsx +++ b/components/Lightbox/Desktop/index.tsx @@ -76,18 +76,17 @@ export function DesktopLightbox({ images, children }: DesktopLightboxProps) { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} + transition={{ duration: 0.2 }} className={styles.overlay} /> - + {isFullView ? ( Date: Tue, 20 Aug 2024 16:31:01 +0200 Subject: [PATCH 063/319] refactor(SW-96): unify lightbox to handle mobile and desktop --- .../HotelPage/PreviewImages/index.tsx | 8 +- components/ContentType/HotelPage/data.ts | 2 +- .../Desktop/desktopLightbox.module.css | 165 ----------- components/Lightbox/Lightbox.module.css | 272 ++++++++++++++++++ components/Lightbox/{Desktop => }/index.tsx | 189 +++++++----- 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 + .../{desktopLightbox.ts => lightbox.ts} | 2 +- 12 files changed, 396 insertions(+), 248 deletions(-) delete mode 100644 components/Lightbox/Desktop/desktopLightbox.module.css create mode 100644 components/Lightbox/Lightbox.module.css rename components/Lightbox/{Desktop => }/index.tsx (62%) rename types/components/lightbox/{desktopLightbox.ts => lightbox.ts} (92%) diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index c8b85698e..ec18ad05d 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -1,11 +1,11 @@ import Image from "@/components/Image" -import { DesktopLightbox } from "@/components/Lightbox/Desktop" +import { Lightbox } from "@/components/Lightbox" import Button from "@/components/TempDesignSystem/Button" import { getIntl } from "@/i18n" import styles from "./previewImages.module.css" -import { ImageItem } from "@/types/components/lightbox/desktopLightbox" +import { ImageItem } from "@/types/components/lightbox/lightbox" export default async function PreviewImages({ images, @@ -14,7 +14,7 @@ export default async function PreviewImages({ }) { const intl = await getIntl() return ( - +
{/*TODO: Replace with images from API once SW-189 is merged. */} {images.slice(0, 3).map((image, index) => ( @@ -38,6 +38,6 @@ export default async function PreviewImages({ {intl.formatMessage({ id: "See all photos" })}
-
+ ) } diff --git a/components/ContentType/HotelPage/data.ts b/components/ContentType/HotelPage/data.ts index e6aa284a7..3fe2e41f4 100644 --- a/components/ContentType/HotelPage/data.ts +++ b/components/ContentType/HotelPage/data.ts @@ -3,7 +3,7 @@ import { FC } from "react" import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import { IconName, IconProps } from "@/types/components/icon" -import { ImageItem } from "@/types/components/lightbox/desktopLightbox" +import { ImageItem } from "@/types/components/lightbox/lightbox" const facilityToIconMap: { [key: string]: IconName } = { Bar: IconName.Bar, diff --git a/components/Lightbox/Desktop/desktopLightbox.module.css b/components/Lightbox/Desktop/desktopLightbox.module.css deleted file mode 100644 index 47c91c216..000000000 --- a/components/Lightbox/Desktop/desktopLightbox.module.css +++ /dev/null @@ -1,165 +0,0 @@ -.content { - border-radius: var(--Corner-radius-Large); - position: fixed; - top: 50%; - left: 50%; - width: 1090px; - height: 725px; - overflow: hidden; - z-index: 10; -} - -.overlay { - position: fixed; - inset: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: 10; -} - -.galleryContainer { - background-color: var(--Base-Background-Primary-Normal); - padding: var(--Spacing-x5) var(--Spacing-x6); - height: 100%; - display: flex; - flex-direction: column; - position: relative; -} - -.closeButton { - position: absolute; - top: var(--Spacing-x-one-and-half); - right: var(--Spacing-x-half); -} - -.backButton { - position: absolute; - top: var(--Spacing-x-one-and-half); - left: var(--Spacing-x-half); -} - -.galleryHeader { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--Spacing-x1); -} - -.imageCaption { - background-color: var(--Base-Surface-Subtle-Normal); - padding: var(--Spacing-x-half) var(--Spacing-x1); - border-radius: var(--Corner-radius-Small); -} - -.mainImageWrapper { - position: relative; - flex: 1; - margin-bottom: var(--Spacing-x2); -} - -.mainImageContainer { - width: 100%; - height: 100%; - will-change: transform; - overflow: hidden; -} - -.mainImageContainer img, -.thumbnailContainer img { - border-radius: var(--Corner-radius-Small); - cursor: pointer; - transition: opacity 0.3s ease-in-out; -} - -.thumbnailGrid { - display: grid; - grid-template-columns: repeat(5, 1fr); - gap: var(--Spacing-x1); - max-height: 125px; - overflow: hidden; -} - -.thumbnailContainer { - position: relative; - height: 125px; -} - -.fullViewContainer { - background-color: var(--UI-Text-High-contrast); - height: 100%; - padding: var(--Spacing-x5); - display: flex; - flex-direction: column; - align-items: center; -} - -.fullViewHeader { - display: flex; - justify-content: center; - margin-bottom: var(--Spacing-x5); - width: 100%; -} - -.fullViewImageContainer { - position: relative; - width: 100%; - max-width: 1054px; - height: 700px; - margin-bottom: var(--Spacing-x5); -} - -.fullViewImage { - position: absolute; - width: 100%; - height: 100%; -} - -.fullViewFooter { - width: 100%; - max-width: 1054px; - text-align: left; - padding-left: 116px; -} - -.imagePosition { - background-color: var(--UI-Grey-90); - padding: var(--Spacing-x-quarter) var(--Spacing-x-half); - border-radius: var(--Corner-radius-Small); -} - -.fullViewImageContainer img { - border-radius: var(--Corner-radius-Medium); - cursor: pointer; -} - -.navigationButton { - position: absolute; - top: 50%; - transform: translateY(-50%); - background-color: var(--Base-Button-Inverted-Fill-Normal); - border-radius: 50%; - padding: var(--Spacing-x1); - cursor: pointer; - border: none; - display: flex; - z-index: 1; -} - -.navigationButton:hover { - background-color: var(--Base-Button-Inverted-Fill-Hover); -} - -.prevButton { - left: var(--Spacing-x2); -} - -.leftTransformIcon { - transform: scaleX(-1); -} - -.nextButton { - right: var(--Spacing-x2); -} - -.portraitImage { - max-width: 548px; -} diff --git a/components/Lightbox/Lightbox.module.css b/components/Lightbox/Lightbox.module.css new file mode 100644 index 000000000..280b80911 --- /dev/null +++ b/components/Lightbox/Lightbox.module.css @@ -0,0 +1,272 @@ +.desktopGallery { + display: none; +} + +.mobileGallery { + margin-top: 70.047px; + height: 100%; + overflow-y: auto; + position: relative; + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.mobileGalleryContent { + display: block; +} + +.fullViewCloseButton { + position: absolute; + top: var(--Spacing-x-one-and-half); + left: var(--Spacing-x-half); + z-index: 1; +} + +.leftTransformIcon { + transform: scaleX(-1); +} + +.content { + width: 100%; + height: 100%; + border-radius: 0; + position: fixed; + top: 50%; + left: 50%; + z-index: 10; +} + +.overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 10; +} + +.galleryContainer { + background-color: var(--Base-Background-Primary-Normal); + padding: var(--Spacing-x2); + height: 100%; + display: flex; + flex-direction: column; + position: relative; +} + +.galleryHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--Spacing-x1); +} + +.imageCaption { + background-color: var(--Base-Surface-Subtle-Normal); + padding: var(--Spacing-x-half) var(--Spacing-x1); + border-radius: var(--Corner-radius-Small); +} + +.mainImageWrapper { + position: relative; + flex: 1; + margin-bottom: var(--Spacing-x2); +} + +.mainImageContainer { + width: 100%; + height: 100%; + will-change: transform; + overflow: hidden; +} + +.mainImageContainer img, +.thumbnailContainer img { + border-radius: var(--Corner-radius-Small); + cursor: pointer; + transition: opacity 0.3s ease-in-out; +} + +.thumbnailGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--Spacing-x1); + max-height: none; +} + +.thumbnailContainer { + position: relative; + height: 242px; +} + +.fullWidthImage { + grid-column: 1 / -1; + height: 240px; +} + +.thumbnailContainer img { + border-radius: var(--Corner-radius-Medium); +} + +.fullViewContainer { + margin-top: 70.047px; + background-color: var(--UI-Text-High-contrast); + height: 100%; + padding: var(--Spacing-x2); + position: relative; + align-items: center; + display: grid; + grid-template-rows: auto 1fr auto; + place-content: center; +} + +.fullViewHeader { + display: flex; + justify-content: center; + margin-bottom: var(--Spacing-x5); + width: 100%; +} + +.fullViewImageContainer { + position: relative; + width: 358px; + height: 240px; + margin-bottom: var(--Spacing-x5); +} + +.fullViewImage { + position: absolute; + width: 100%; + height: 100%; + border-radius: var(--Corner-radius-Medium); +} + +.fullViewImageContainer img { + border-radius: var(--Corner-radius-Medium); + cursor: pointer; + width: 100%; + height: 100%; +} + +.fullViewFooter { + position: absolute; + bottom: calc(-1 * var(--Spacing-x5)); +} + +.imagePosition { + background-color: var(--UI-Grey-90); + padding: var(--Spacing-x-quarter) var(--Spacing-x-half); + border-radius: var(--Corner-radius-Small); +} + +.navigationButton { + display: none; +} + +.portraitImage { + max-width: 548px; +} + +@media (min-width: 1367px) { + .content { + border-radius: var(--Corner-radius-Large); + width: 1090px; + height: 725px; + overflow: hidden; + } + + .galleryContainer { + padding: var(--Spacing-x5) var(--Spacing-x6); + } + + .desktopGallery { + display: flex; + background-color: var(--Base-Background-Primary-Normal); + height: 100%; + flex-direction: column; + position: relative; + } + + .desktopGalleryCloseButton { + position: absolute; + top: var(--Spacing-x-one-and-half); + right: var(--Spacing-x-half); + } + + .mobileGallery { + display: none; + } + + .thumbnailGrid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: var(--Spacing-x1); + max-height: 125px; + overflow: hidden; + } + + .thumbnailContainer { + height: 125px; + } + + .fullViewCloseButton { + position: fixed; + } + + .fullWidthImage { + grid-column: auto; + height: auto; + } + + .thumbnailContainer img { + border-radius: var(--Corner-radius-Small); + } + + .fullViewContainer { + margin-top: 0; + padding: var(--Spacing-x5); + grid-template-rows: auto 1fr auto; + grid-template-columns: 1fr; + justify-items: center; + } + + .fullViewImageContainer { + position: relative; + max-width: 1054px; + width: 80%; + height: 85%; + margin-bottom: var(--Spacing-x5); + } + + .navigationButton { + position: absolute; + top: 50%; + transform: translateY(-50%); + background-color: var(--Base-Button-Inverted-Fill-Normal); + border-radius: 50%; + padding: var(--Spacing-x1); + cursor: pointer; + border: none; + display: flex; + z-index: 1; + } + + .galleryPrevButton { + left: var(--Spacing-x2); + } + + .galleryNextButton { + right: var(--Spacing-x2); + } + + .fullViewNextButton { + right: var(--Spacing-x5); + } + + .fullViewPrevButton { + left: var(--Spacing-x5); + } + + .fullViewFooter { + text-align: left; + } +} diff --git a/components/Lightbox/Desktop/index.tsx b/components/Lightbox/index.tsx similarity index 62% rename from components/Lightbox/Desktop/index.tsx rename to components/Lightbox/index.tsx index a68e97c61..614405467 100644 --- a/components/Lightbox/Desktop/index.tsx +++ b/components/Lightbox/index.tsx @@ -11,15 +11,15 @@ import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" -import styles from "./desktopLightbox.module.css" +import styles from "./Lightbox.module.css" import { - DesktopLightboxProps, FullViewProps, GalleryProps, -} from "@/types/components/lightbox/desktopLightbox" + LightboxProps, +} from "@/types/components/lightbox/lightbox" -export function DesktopLightbox({ images, children }: DesktopLightboxProps) { +export function Lightbox({ images, children }: LightboxProps) { const [isOpen, setIsOpen] = useState(false) const [selectedImageIndex, setSelectedImageIndex] = useState(0) const [isFullView, setIsFullView] = useState(false) @@ -155,72 +155,108 @@ function Gallery({ intent="text" size="small" theme="base" - className={styles.closeButton} + className={styles.desktopGalleryCloseButton} onClick={onClose} > -
-
- {mainImage.alt} + {/* Desktop Gallery */} +
+
+
+ {mainImage.alt} +
-
-
- - - {mainImage.alt} - - - - - - - - -
-
- - {getThumbImages().map((image, index) => ( +
+ onSelectImage(image)} - initial={{ opacity: 0, x: 50 }} + key={mainImage.url} + className={styles.mainImageContainer} + initial={{ opacity: 0, x: 300 }} animate={{ opacity: 1, x: 0 }} - exit={{ opacity: 0, x: -50 }} - transition={{ duration: 0.2, delay: index * 0.05 }} + exit={{ opacity: 0, x: -300 }} + transition={{ duration: 0.3 }} > {image.alt} - ))} - + + + + + + + +
+
+ + {getThumbImages().map((image, index) => ( + onSelectImage(image)} + initial={{ opacity: 0, x: 50 }} + animate={{ opacity: 1, x: 0 }} + exit={{ opacity: 0, x: -50 }} + transition={{ duration: 0.2, delay: index * 0.05 }} + > + {image.alt} + + ))} + +
+
+ + {/* Mobile Gallery */} +
+ +
+
+ {images.map((image, index) => ( + onImageClick()} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.3, delay: index * 0.05 }} + > + {image.alt} + + ))} +
+
) @@ -239,7 +275,7 @@ function FullView({
-
- {image.alt}
+ + + + + + +
) } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index e6a339181..e44cefb3a 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -144,6 +144,7 @@ "Save": "Gemme", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "See all photos": "Se alle billeder", "See hotel details": "Se hoteloplysninger", "See rooms": "Se værelser", "Select a country": "Vælg et land", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 6f730539d..72128e6ea 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -139,6 +139,7 @@ "Save": "Speichern", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "See all photos": "Alle Fotos ansehen", "See hotel details": "Hotelinformationen ansehen", "See rooms": "Zimmer ansehen", "Select a country": "Wähle ein Land", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index da33379f2..9cf0294a6 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -150,6 +150,7 @@ "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", "See hotel details": "See hotel details", + "See all photos": "See all photos", "See room details": "See room details", "See rooms": "See rooms", "Select a country": "Select a country", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index d362b3dbe..265e5bb40 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -144,6 +144,7 @@ "Save": "Tallentaa", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "See all photos": "Katso kaikki kuvat", "See hotel details": "Katso hotellin tiedot", "See rooms": "Katso huoneet", "Select a country": "Valitse maa", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index eeb1cfbc6..1b488e07c 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -144,6 +144,7 @@ "Save": "Lagre", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "See all photos": "Se alle bilder", "See hotel details": "Se hotellinformasjon", "See rooms": "Se rom", "Select a country": "Velg et land", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 721037d7f..77a3758d1 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -146,6 +146,7 @@ "Save": "Spara", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "See all photos": "Se alla foton", "See hotel details": "Se hotellinformation", "See room details": "Se rumsdetaljer", "See rooms": "Se rum", diff --git a/types/components/lightbox/desktopLightbox.ts b/types/components/lightbox/lightbox.ts similarity index 92% rename from types/components/lightbox/desktopLightbox.ts rename to types/components/lightbox/lightbox.ts index b12e823e3..be3eaa108 100644 --- a/types/components/lightbox/desktopLightbox.ts +++ b/types/components/lightbox/lightbox.ts @@ -3,7 +3,7 @@ export interface ImageItem { alt: string } -export interface DesktopLightboxProps { +export interface LightboxProps { images: ImageItem[] children: React.ReactNode } From 79e5d651407a3e979fda29fdf637c99171297290 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 20 Aug 2024 16:51:47 +0200 Subject: [PATCH 064/319] feat(SW-96): add preview image and lightbox CTA on mobile viewports --- .../HotelPage/PreviewImages/index.tsx | 21 +++++++++++++++- .../PreviewImages/previewImages.module.css | 25 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index ec18ad05d..124118b37 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -15,8 +15,27 @@ export default async function PreviewImages({ const intl = await getIntl() return ( + {/*TODO: Replace with images from API once SW-189 is merged. */} +
+ {images[0].alt} + +
- {/*TODO: Replace with images from API once SW-189 is merged. */} {images.slice(0, 3).map((image, index) => ( Date: Wed, 21 Aug 2024 08:29:05 +0200 Subject: [PATCH 065/319] refactor(SW-96): use wildcard matching for scandichotels subdomain --- next.config.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/next.config.js b/next.config.js index e48838c84..d23e3903a 100644 --- a/next.config.js +++ b/next.config.js @@ -37,19 +37,11 @@ const nextConfig = { }, { protocol: "https", - hostname: "imagevault-stage.scandichotels.com", + hostname: "scandichotels.com", }, { protocol: "https", - hostname: "imagevault.scandichotels.com", - }, - { - protocol: "https", - hostname: "test3.scandichotels.com", - }, - { - protocol: "https", - hostname: "www.scandichotels.com", + hostname: "*.scandichotels.com", }, ], }, From f97e5215027f2e2310f741c43496193148c2d9a5 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Wed, 21 Aug 2024 11:13:38 +0200 Subject: [PATCH 066/319] chore(SW-96): move fullview and gallery into own files --- .../HotelPage/PreviewImages/index.tsx | 2 +- .../PreviewImages/previewImages.module.css | 1 + components/Lightbox/FullView.tsx | 84 +++++++ components/Lightbox/Gallery.tsx | 155 ++++++++++++ components/Lightbox/index.tsx | 230 +----------------- 5 files changed, 245 insertions(+), 227 deletions(-) create mode 100644 components/Lightbox/FullView.tsx create mode 100644 components/Lightbox/Gallery.tsx diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index 124118b37..d759c0524 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -1,5 +1,5 @@ import Image from "@/components/Image" -import { Lightbox } from "@/components/Lightbox" +import Lightbox from "@/components/Lightbox/" import Button from "@/components/TempDesignSystem/Button" import { getIntl } from "@/i18n" diff --git a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css index 8d663aac3..04810b3e1 100644 --- a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css +++ b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css @@ -48,6 +48,7 @@ grid-row: 1; grid-column: 2; } + .desktopGrid > :nth-child(3) { grid-row: 2; grid-column: 2; diff --git a/components/Lightbox/FullView.tsx b/components/Lightbox/FullView.tsx new file mode 100644 index 000000000..2410a8b47 --- /dev/null +++ b/components/Lightbox/FullView.tsx @@ -0,0 +1,84 @@ +"use client" +import { AnimatePresence, motion } from "framer-motion" +import Image from "next/image" + +import { ChevronRightIcon } from "@/components/Icons" +import ArrowRightIcon from "@/components/Icons/ArrowRight" +import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import styles from "./Lightbox.module.css" + +import { FullViewProps } from "@/types/components/lightbox/lightbox" + +export default function FullView({ + image, + onClose, + onNext, + onPrev, + currentIndex, + totalImages, +}: FullViewProps) { + return ( +
+ +
+ + + {`${currentIndex + 1} / ${totalImages}`} + + +
+
+ + + {image.alt} + +
+ {image.alt} +
+
+
+
+ + + + + + + +
+ ) +} diff --git a/components/Lightbox/Gallery.tsx b/components/Lightbox/Gallery.tsx new file mode 100644 index 000000000..c0921ba7c --- /dev/null +++ b/components/Lightbox/Gallery.tsx @@ -0,0 +1,155 @@ +"use client" +import { AnimatePresence, motion } from "framer-motion" +import Image from "next/image" + +import { ChevronRightIcon } from "@/components/Icons" +import ArrowRightIcon from "@/components/Icons/ArrowRight" +import CloseIcon from "@/components/Icons/Close" +import Button from "@/components/TempDesignSystem/Button" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import styles from "./Lightbox.module.css" + +import { GalleryProps } from "@/types/components/lightbox/lightbox" + +export default function Gallery({ + images, + onClose, + onSelectImage, + onImageClick, + selectedImage, +}: GalleryProps) { + const mainImage = selectedImage || images[0] + const mainImageIndex = images.findIndex((img) => img.url === mainImage.url) + + function getThumbImages() { + const thumbs = [] + for (let i = 1; i <= 5; i++) { + const index = (mainImageIndex + i) % images.length + thumbs.push(images[index]) + } + return thumbs + } + + const handleNext = () => { + const nextIndex = (mainImageIndex + 1) % images.length + onSelectImage(images[nextIndex]) + } + + const handlePrev = () => { + const prevIndex = (mainImageIndex - 1 + images.length) % images.length + onSelectImage(images[prevIndex]) + } + + return ( +
+ + {/* Desktop Gallery */} +
+
+
+ {mainImage.alt} +
+
+
+ + + {mainImage.alt} + + + + + + + + +
+
+ + {getThumbImages().map((image, index) => ( + onSelectImage(image)} + initial={{ opacity: 0, x: 50 }} + animate={{ opacity: 1, x: 0 }} + exit={{ opacity: 0, x: -50 }} + transition={{ duration: 0.2, delay: index * 0.05 }} + > + {image.alt} + + ))} + +
+
+ + {/* Mobile Gallery */} +
+ +
+
+ {images.map((image, index) => ( + onImageClick()} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.3, delay: index * 0.05 }} + > + {image.alt} + + ))} +
+
+
+
+ ) +} diff --git a/components/Lightbox/index.tsx b/components/Lightbox/index.tsx index 614405467..95709d2ad 100644 --- a/components/Lightbox/index.tsx +++ b/components/Lightbox/index.tsx @@ -1,25 +1,16 @@ "use client" import * as Dialog from "@radix-ui/react-dialog" import { AnimatePresence, motion } from "framer-motion" -import Image from "next/image" import React, { useState } from "react" -import { ChevronRightIcon } from "@/components/Icons" -import ArrowRightIcon from "@/components/Icons/ArrowRight" -import CloseIcon from "@/components/Icons/Close" -import Button from "@/components/TempDesignSystem/Button" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" +import FullView from "./FullView" +import Gallery from "./Gallery" import styles from "./Lightbox.module.css" -import { - FullViewProps, - GalleryProps, - LightboxProps, -} from "@/types/components/lightbox/lightbox" +import { LightboxProps } from "@/types/components/lightbox/lightbox" -export function Lightbox({ images, children }: LightboxProps) { +export default function Lightbox({ images, children }: LightboxProps) { const [isOpen, setIsOpen] = useState(false) const [selectedImageIndex, setSelectedImageIndex] = useState(0) const [isFullView, setIsFullView] = useState(false) @@ -119,216 +110,3 @@ export function Lightbox({ images, children }: LightboxProps) { ) } - -function Gallery({ - images, - onClose, - onSelectImage, - onImageClick, - selectedImage, -}: GalleryProps) { - const mainImage = selectedImage || images[0] - const mainImageIndex = images.findIndex((img) => img.url === mainImage.url) - - function getThumbImages() { - const thumbs = [] - for (let i = 1; i <= 5; i++) { - const index = (mainImageIndex + i) % images.length - thumbs.push(images[index]) - } - return thumbs - } - - const handleNext = () => { - const nextIndex = (mainImageIndex + 1) % images.length - onSelectImage(images[nextIndex]) - } - - const handlePrev = () => { - const prevIndex = (mainImageIndex - 1 + images.length) % images.length - onSelectImage(images[prevIndex]) - } - - return ( -
- - {/* Desktop Gallery */} -
-
-
- {mainImage.alt} -
-
-
- - - {mainImage.alt} - - - - - - - - -
-
- - {getThumbImages().map((image, index) => ( - onSelectImage(image)} - initial={{ opacity: 0, x: 50 }} - animate={{ opacity: 1, x: 0 }} - exit={{ opacity: 0, x: -50 }} - transition={{ duration: 0.2, delay: index * 0.05 }} - > - {image.alt} - - ))} - -
-
- - {/* Mobile Gallery */} -
- -
-
- {images.map((image, index) => ( - onImageClick()} - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ duration: 0.3, delay: index * 0.05 }} - > - {image.alt} - - ))} -
-
-
-
- ) -} - -function FullView({ - image, - onClose, - onNext, - onPrev, - currentIndex, - totalImages, -}: FullViewProps) { - return ( -
- -
- - - {`${currentIndex + 1} / ${totalImages}`} - - -
-
- - - {image.alt} - -
- {image.alt} -
-
-
-
- - - - - - - -
- ) -} From 76e0fb5697fc90ae4d96bc59326c2a78ddd677c2 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Wed, 21 Aug 2024 11:30:02 +0200 Subject: [PATCH 067/319] fix(SW-96): use input props for preview images --- .../ContentType/HotelPage/PreviewImages/index.tsx | 10 +++------- types/components/hotelPage/previewImages.ts | 5 +++++ 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 types/components/hotelPage/previewImages.ts diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index d759c0524..7f0306217 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -5,17 +5,13 @@ import { getIntl } from "@/i18n" import styles from "./previewImages.module.css" -import { ImageItem } from "@/types/components/lightbox/lightbox" +import { PreviewImagesProps } from "@/types/components/hotelPage/previewImages" -export default async function PreviewImages({ - images, -}: { - images: ImageItem[] -}) { +export default async function PreviewImages({ images }: PreviewImagesProps) { const intl = await getIntl() return ( - {/*TODO: Replace with images from API once SW-189 is merged. */} + {/*TODO: Replace with images from API once SW-188 is merged. */}
Date: Wed, 21 Aug 2024 13:00:32 +0200 Subject: [PATCH 068/319] fix(SW-96): use Image component instead of next/image directly --- components/Lightbox/FullView.tsx | 2 +- components/Lightbox/Gallery.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/Lightbox/FullView.tsx b/components/Lightbox/FullView.tsx index 2410a8b47..4a14fac2c 100644 --- a/components/Lightbox/FullView.tsx +++ b/components/Lightbox/FullView.tsx @@ -1,9 +1,9 @@ "use client" import { AnimatePresence, motion } from "framer-motion" -import Image from "next/image" import { ChevronRightIcon } from "@/components/Icons" import ArrowRightIcon from "@/components/Icons/ArrowRight" +import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" diff --git a/components/Lightbox/Gallery.tsx b/components/Lightbox/Gallery.tsx index c0921ba7c..42385acb2 100644 --- a/components/Lightbox/Gallery.tsx +++ b/components/Lightbox/Gallery.tsx @@ -1,10 +1,10 @@ "use client" import { AnimatePresence, motion } from "framer-motion" -import Image from "next/image" import { ChevronRightIcon } from "@/components/Icons" import ArrowRightIcon from "@/components/Icons/ArrowRight" import CloseIcon from "@/components/Icons/Close" +import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" From 7d47994539f18acd533c6a4ff0a0167ea3d7d5a1 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Wed, 21 Aug 2024 14:52:13 +0200 Subject: [PATCH 069/319] refactor(SW-96): use images from API & reduce data returned in getHotel --- .../HotelPage/PreviewImages/index.tsx | 3 +- components/ContentType/HotelPage/data.ts | 41 ------------------- components/ContentType/HotelPage/index.tsx | 26 ++++++++---- components/Lightbox/FullView.tsx | 2 +- components/Lightbox/Gallery.tsx | 2 +- components/Lightbox/Lightbox.module.css | 7 +++- server/routers/hotels/query.ts | 15 ++++++- server/routers/utils/hotels.ts | 30 ++++++++++++++ types/components/lightbox/lightbox.ts | 1 + 9 files changed, 71 insertions(+), 56 deletions(-) create mode 100644 server/routers/utils/hotels.ts diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index 7f0306217..390430cf7 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -11,11 +11,11 @@ export default async function PreviewImages({ images }: PreviewImagesProps) { const intl = await getIntl() return ( - {/*TODO: Replace with images from API once SW-188 is merged. */}
{images[0].alt} | null { const iconName = facilityToIconMap[facilityName] return getIconByIconName(iconName) || null } - -/** - * NOTE: - * Images from the test API are hosted on test3.scandichotels.com, - * which can't be accessed unless on Scandic's Wifi or using Citrix. - * So I've added some placeholder images here for use when running the app locally. - */ -export const tempHotelLightboxImage: ImageItem[] = [ - { - url: "https://www.scandichotels.com/imagevault/publishedmedia/kd4e35n41mrqinfrkzok/scandic-continental-entrance-vasagatan.jpg", - alt: "Hotel entrance", - }, - { - url: "https://www.scandichotels.com/imageVault/publishedmedia/ip4a2rc93qda3aq9ph73/scandic-continental-room-standard.jpg", - alt: "Standard room", - }, - { - url: "https://www.scandichotels.com/imageVault/publishedmedia/75y2b2x8uvma4s7zvy50/scandic-continental-room-juniorsuite-detail-1.jpg", - alt: "Junior suite", - }, - { - url: "https://www.scandichotels.com/imageVault/publishedmedia/pwotq99pbr68djxnym4a/scandic-continental-diningroom-themarket.jpg", - alt: "Dining room, The Market", - }, - { - url: "https://www.scandichotels.com/imageVault/publishedmedia/yc2cjoxq2j01gp0f80gt/scandic-continental-room-juniorsuite-bathroom-1.jpg", - alt: "Junior suite bathroom", - }, - { - url: "https://www.scandichotels.com/imageVault/publishedmedia/xocam1f69h7lh57u8f9v/scandic-continental-themarket-detail.jpg", - alt: "The Market detail", - }, - { - url: "https://www.scandichotels.com/imageVault/publishedmedia/0nnwt72vwzfvkkx5whf0/scandic-continental-caldo.jpg", - alt: "Caldo", - }, - { - url: "https://www.scandichotels.com/imageVault/publishedmedia/mg9bbtbumfxbfjhovk2e/Scandic_Continental_Capitol_The_View_6.jpg", - alt: "Rooftop bar", - }, -] diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index a1838403e..960863cc3 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -4,7 +4,6 @@ import { MOCK_FACILITIES } from "./Facilities/mockData" import Facilities from "./Facilities" import AmenitiesList from "./AmenitiesList" -import { tempHotelLightboxImage } from "./data" import IntroSection from "./IntroSection" import PreviewImages from "./PreviewImages" import { Rooms } from "./Rooms" @@ -20,23 +19,32 @@ export default async function HotelPage() { if (!hotelData) { return null } - const { hotel, roomCategories } = hotelData + const { + hotelName, + hotelDescription, + hotelLocation, + hotelAddress, + hotelRatings, + hotelDetailedFacilities, + hotelImages, + roomCategories, + } = hotelData return (
- +
- +
diff --git a/components/Lightbox/FullView.tsx b/components/Lightbox/FullView.tsx index 4a14fac2c..9e63dbf02 100644 --- a/components/Lightbox/FullView.tsx +++ b/components/Lightbox/FullView.tsx @@ -61,7 +61,7 @@ export default function FullView({ />
- {image.alt} + {image.title}
diff --git a/components/Lightbox/Gallery.tsx b/components/Lightbox/Gallery.tsx index 42385acb2..5134ecf29 100644 --- a/components/Lightbox/Gallery.tsx +++ b/components/Lightbox/Gallery.tsx @@ -56,7 +56,7 @@ export default function Gallery({
- {mainImage.alt} + {mainImage.title}
diff --git a/components/Lightbox/Lightbox.module.css b/components/Lightbox/Lightbox.module.css index 280b80911..fa3ea4f1c 100644 --- a/components/Lightbox/Lightbox.module.css +++ b/components/Lightbox/Lightbox.module.css @@ -5,7 +5,6 @@ .mobileGallery { margin-top: 70.047px; height: 100%; - overflow-y: auto; position: relative; display: flex; flex-direction: column; @@ -51,6 +50,7 @@ display: flex; flex-direction: column; position: relative; + overflow-y: auto; } .galleryHeader { @@ -60,6 +60,10 @@ margin-bottom: var(--Spacing-x1); } +.desktopGalleryCloseButton { + display: none; +} + .imageCaption { background-color: var(--Base-Surface-Subtle-Normal); padding: var(--Spacing-x-half) var(--Spacing-x1); @@ -187,6 +191,7 @@ } .desktopGalleryCloseButton { + display: block; position: absolute; top: var(--Spacing-x-one-and-half); right: var(--Spacing-x-half); diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 2b99bf9d3..bd8d8f14a 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -13,6 +13,7 @@ import { publicProcedure, router, } from "@/server/trpc" +import { extractHotelImages } from "@/server/routers/utils/hotels" import { toApiLang } from "@/server/utils" import { @@ -153,6 +154,10 @@ export const hotelQueryRouter = router({ const included = validatedHotelData.data.included || [] + const hotelAttributes = validatedHotelData.data.data.attributes + + const images = extractHotelImages(hotelAttributes) + const roomCategories = included ? included .filter((item) => item.type === "roomcategories") @@ -193,8 +198,14 @@ export const hotelQueryRouter = router({ }) ) return { - hotel: validatedHotelData.data.data.attributes, - roomCategories: roomCategories, + hotelName: hotelAttributes.name, + hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short, + hotelLocation: hotelAttributes.location, + hotelAddress: hotelAttributes.address, + hotelRatings: hotelAttributes.ratings, + hotelDetailedFacilities: hotelAttributes.detailedFacilities, + hotelImages: images, + roomCategories, } }), rates: router({ diff --git a/server/routers/utils/hotels.ts b/server/routers/utils/hotels.ts new file mode 100644 index 000000000..ba05d20d1 --- /dev/null +++ b/server/routers/utils/hotels.ts @@ -0,0 +1,30 @@ +import { ImageItem } from "@/types/components/lightbox/lightbox" +import { Hotel } from "@/types/hotel" + +export function extractHotelImages(hotelData: Hotel): ImageItem[] { + const images: ImageItem[] = [] + + if (hotelData.hotelContent?.images) { + images.push({ + url: hotelData.hotelContent.images.imageSizes.large, + alt: hotelData.hotelContent.images.metaData.altText, + title: + hotelData.hotelContent.images.metaData.title || + hotelData.hotelContent.images.metaData.altText, + }) + } + + if (hotelData.healthFacilities) { + hotelData.healthFacilities.forEach((facility: any) => { + facility.content.images.forEach((image: any) => { + images.push({ + url: image.imageSizes.large, + alt: image.metaData.altText, + title: image.metaData.title || image.metaData.altText, + }) + }) + }) + } + + return images +} diff --git a/types/components/lightbox/lightbox.ts b/types/components/lightbox/lightbox.ts index be3eaa108..4f5eed250 100644 --- a/types/components/lightbox/lightbox.ts +++ b/types/components/lightbox/lightbox.ts @@ -1,6 +1,7 @@ export interface ImageItem { url: string alt: string + title: string } export interface LightboxProps { From 7e7a5d26460f5bc82d6c373a6d2186b488c91541 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 23 Aug 2024 07:24:35 +0200 Subject: [PATCH 070/319] fix(SW-96): add css variable for use of mobile header height --- app/globals.css | 1 + components/Current/Header/MainMenu/mainMenu.module.css | 2 +- components/Current/Header/header.module.css | 2 +- components/Lightbox/Lightbox.module.css | 4 ++-- components/TempDesignSystem/SidePeek/sidePeek.module.css | 8 ++++---- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/globals.css b/app/globals.css index b33c7954c..6180a72f6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -100,6 +100,7 @@ --max-width: 113.5rem; --max-width-content: 74.75rem; --max-width-text-block: 49.5rem; + --mobile-site-header-height: 70.047px; } * { diff --git a/components/Current/Header/MainMenu/mainMenu.module.css b/components/Current/Header/MainMenu/mainMenu.module.css index 15ac58e6d..af3f3eee4 100644 --- a/components/Current/Header/MainMenu/mainMenu.module.css +++ b/components/Current/Header/MainMenu/mainMenu.module.css @@ -8,7 +8,7 @@ top: 0; width: 100%; z-index: 99999; - height: 70.047px; + height: var(--mobile-site-header-height); } .container { diff --git a/components/Current/Header/header.module.css b/components/Current/Header/header.module.css index 6e1001420..1e91a497c 100644 --- a/components/Current/Header/header.module.css +++ b/components/Current/Header/header.module.css @@ -4,6 +4,6 @@ @media screen and (max-width: 1366px) { .header { - height: 70.047px; + height: var(--mobile-site-header-height); } } diff --git a/components/Lightbox/Lightbox.module.css b/components/Lightbox/Lightbox.module.css index fa3ea4f1c..494368206 100644 --- a/components/Lightbox/Lightbox.module.css +++ b/components/Lightbox/Lightbox.module.css @@ -3,7 +3,7 @@ } .mobileGallery { - margin-top: 70.047px; + margin-top: var(--mobile-site-header-height); height: 100%; position: relative; display: flex; @@ -112,7 +112,7 @@ } .fullViewContainer { - margin-top: 70.047px; + margin-top: var(--mobile-site-header-height); background-color: var(--UI-Text-High-contrast); height: 100%; padding: var(--Spacing-x2); diff --git a/components/TempDesignSystem/SidePeek/sidePeek.module.css b/components/TempDesignSystem/SidePeek/sidePeek.module.css index e08f776e8..0297f9d04 100644 --- a/components/TempDesignSystem/SidePeek/sidePeek.module.css +++ b/components/TempDesignSystem/SidePeek/sidePeek.module.css @@ -1,10 +1,10 @@ .sidePeek { position: fixed; - top: 70.047px; + top: var(--mobile-site-header-height); right: auto; bottom: 0; width: 100%; - height: calc(100vh - 70.047px); + height: calc(100vh - var(--mobile-site-header-height)); background-color: var(--Base-Background-Primary-Normal); z-index: 100; box-shadow: 0 0 10px rgba(0, 0, 0, 0.85); @@ -24,7 +24,7 @@ .overlay { position: absolute; - top: 70.047px; + top: var(--mobile-site-header-height); bottom: 0; left: 0; right: 0; @@ -46,7 +46,7 @@ } to { - top: 70.047px; + top: var(--mobile-site-header-height); } } From 64a44f7dcc7fffcec1a9d2ae69fd269f358d04d2 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 23 Aug 2024 08:15:38 +0200 Subject: [PATCH 071/319] fix(SW-96): use function declarations --- components/Lightbox/Gallery.tsx | 4 ++-- components/Lightbox/index.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/Lightbox/Gallery.tsx b/components/Lightbox/Gallery.tsx index 5134ecf29..22ac59c0b 100644 --- a/components/Lightbox/Gallery.tsx +++ b/components/Lightbox/Gallery.tsx @@ -31,12 +31,12 @@ export default function Gallery({ return thumbs } - const handleNext = () => { + function handleNext() { const nextIndex = (mainImageIndex + 1) % images.length onSelectImage(images[nextIndex]) } - const handlePrev = () => { + function handlePrev() { const prevIndex = (mainImageIndex - 1 + images.length) % images.length onSelectImage(images[prevIndex]) } diff --git a/components/Lightbox/index.tsx b/components/Lightbox/index.tsx index 95709d2ad..3ac645235 100644 --- a/components/Lightbox/index.tsx +++ b/components/Lightbox/index.tsx @@ -15,7 +15,7 @@ export default function Lightbox({ images, children }: LightboxProps) { const [selectedImageIndex, setSelectedImageIndex] = useState(0) const [isFullView, setIsFullView] = useState(false) - const handleOpenChange = (open: boolean) => { + function handleOpenChange(open: boolean) { if (!open) { setTimeout(() => { setIsOpen(false) @@ -27,11 +27,11 @@ export default function Lightbox({ images, children }: LightboxProps) { } } - const handleNext = () => { + function handleNext() { setSelectedImageIndex((prevIndex) => (prevIndex + 1) % images.length) } - const handlePrev = () => { + function handlePrev() { setSelectedImageIndex( (prevIndex) => (prevIndex - 1 + images.length) % images.length ) From bef58ab0a355ea69018a730926d16b48768180fa Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 23 Aug 2024 11:03:20 +0200 Subject: [PATCH 072/319] refactor(SW-99): better responsive styling of preview images --- .../HotelPage/PreviewImages/index.tsx | 24 +------ .../PreviewImages/previewImages.module.css | 71 ++++++++----------- 2 files changed, 31 insertions(+), 64 deletions(-) diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index 390430cf7..2112fc886 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -11,27 +11,7 @@ export default async function PreviewImages({ images }: PreviewImagesProps) { const intl = await getIntl() return ( -
- {images[0].alt} - -
-
+
{images.slice(0, 3).map((image, index) => ( ))}
+import { DetailsProps } from "@/types/components/hotelReservation/selectRate/section" + +export default function Details({ nextPath }: DetailsProps) { + const searchParams = useSearchParams() + + return ( +
+
+ +
+
+ ) } diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index bec7f57e5..e64e51506 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -31,6 +31,8 @@ export interface RoomSelectionProps extends SectionProps { nrOfNights: number } +export interface DetailsProps extends SectionProps {} + export interface SectionPageProps { breakfast?: string bed?: string From bb9b4575255009d8847255db1b43a17fe458ffb0 Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Mon, 2 Sep 2024 10:20:55 +0200 Subject: [PATCH 084/319] Go to select hotel when booking widget submits --- components/Forms/BookingWidget/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index 5df16e817..7dadceeaa 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -1,5 +1,6 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" +import { useRouter } from "next/navigation" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" @@ -19,6 +20,7 @@ const formId = "booking-widget" export default function Form() { const intl = useIntl() + const router = useRouter() const methods = useForm({ defaultValues: { search: { @@ -48,8 +50,9 @@ export default function Form() { function onSubmit(data: BookingWidgetSchema) { console.log(data) - // Parse data and route accordignly to Select hotel or select room-rate page + // TODO: Parse data and route accordignly to Select hotel or select room-rate page console.log("to be routing") + router.push("/en/hotelreservation/select-hotel") } return ( From 56a49ee56dd96e04524fd01c9c4e36f2836c799e Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Mon, 2 Sep 2024 10:28:49 +0200 Subject: [PATCH 085/319] Link select hotel to select rate --- components/HotelReservation/HotelCard/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index c294e5c0d..77d038105 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -95,7 +95,8 @@ export default async function HotelCard({ hotel }: HotelCardProps) { size="small" className={styles.button} > - + {/* TODO: Localize link and also use correct search params */} + {intl.formatMessage({ id: "See rooms" })} From c0b16e8dee96df34c8e5871a40bb75a32561df6b Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Mon, 2 Sep 2024 13:49:10 +0000 Subject: [PATCH 086/319] Merged in feat/SW-212-copy-to-clipboard-feedback (pull request #548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feat/SW-212 copy to clipboard feedback * feat(SW-212): Added toast to give feedback when copying member id * feat(SW-212): Update copy * feat(SW-212): Changed to use success toast and updated copy * fix: update copy in toast for copying membership ID Approved-by: Christel Westerberg Approved-by: Matilda Landström --- components/MyPages/Blocks/Overview/Buttons/CopyButton.tsx | 8 ++++++++ i18n/dictionaries/da.json | 3 ++- i18n/dictionaries/de.json | 3 ++- i18n/dictionaries/en.json | 5 +++-- i18n/dictionaries/fi.json | 3 ++- i18n/dictionaries/no.json | 3 ++- i18n/dictionaries/sv.json | 3 ++- 7 files changed, 21 insertions(+), 7 deletions(-) diff --git a/components/MyPages/Blocks/Overview/Buttons/CopyButton.tsx b/components/MyPages/Blocks/Overview/Buttons/CopyButton.tsx index 274a3ab5b..3ed077ab3 100644 --- a/components/MyPages/Blocks/Overview/Buttons/CopyButton.tsx +++ b/components/MyPages/Blocks/Overview/Buttons/CopyButton.tsx @@ -1,15 +1,23 @@ "use client" +import { useIntl } from "react-intl" + import CopyIcon from "@/components/Icons/Copy" import Button from "@/components/TempDesignSystem/Button" +import { toast } from "@/components/TempDesignSystem/Toasts" import styles from "./copybutton.module.css" import type { CopyButtonProps } from "@/types/components/myPages/membership" export default function CopyButton({ membershipNumber }: CopyButtonProps) { + const intl = useIntl() + function handleCopy() { navigator.clipboard.writeText(membershipNumber) + toast.success( + intl.formatMessage({ id: "Membership ID copied to clipboard" }) + ) } return ( diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index e44cefb3a..a50f0448a 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -85,6 +85,7 @@ "Log out": "Log ud", "Manage preferences": "Administrer præferencer", "Meetings & Conferences": "Møder & Konferencer", + "Membership ID copied to clipboard": "Medlems-ID kopieret til udklipsholder", "Member price": "Medlemspris", "Member price from": "Medlemspris fra", "Members": "Medlemmer", @@ -123,8 +124,8 @@ "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", - "points": "Point", "Points": "Point", + "points": "Point", "Points being calculated": "Point udregnes", "Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021", "Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 72128e6ea..10a8e7fcf 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -83,6 +83,7 @@ "Log in here": "Hier einloggen", "Log out": "Ausloggen", "Manage preferences": "Verwalten von Voreinstellungen", + "Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert", "Member price": "Mitgliederpreis", "Member price from": "Mitgliederpreis ab", "Members": "Mitglieder", @@ -120,8 +121,8 @@ "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", - "Points": "Punkte", "points": "Punkte", + "Points": "Punkte", "Points being calculated": "Punkte werden berechnet", "Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021", "Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 9cf0294a6..8d6ce6f44 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -90,6 +90,7 @@ "Log out": "Log out", "Manage preferences": "Manage preferences", "Meetings & Conferences": "Meetings & Conferences", + "Membership ID copied to clipboard": "Membership ID copied to clipboard", "Member price": "Member price", "Member price from": "Member price from", "Members": "Members", @@ -128,8 +129,8 @@ "Phone is required": "Phone is required", "Phone number": "Phone number", "Please enter a valid phone number": "Please enter a valid phone number", - "points": "Points", "Points": "Points", + "points": "Points", "Points being calculated": "Points being calculated", "Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021", "Points may take up to 10 days to be displayed.": "Points may take up to 10 days to be displayed.", @@ -149,8 +150,8 @@ "Save": "Save", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", - "See hotel details": "See hotel details", "See all photos": "See all photos", + "See hotel details": "See hotel details", "See room details": "See room details", "See rooms": "See rooms", "Select a country": "Select a country", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 265e5bb40..3c33b7848 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -85,6 +85,7 @@ "Log out": "Kirjaudu ulos", "Manage preferences": "Asetusten hallinta", "Meetings & Conferences": "Kokoukset & Konferenssit", + "Membership ID copied to clipboard": "Jäsenyystunnus kopioitu leikepöydälle", "Member price": "Jäsenhinta", "Member price from": "Jäsenhinta alkaen", "Members": "Jäsenet", @@ -123,8 +124,8 @@ "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", - "points": "pistettä", "Points": "Pisteet", + "points": "pistettä", "Points being calculated": "Pisteitä lasketaan", "Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021", "Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 1b488e07c..7cebd9aac 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -85,6 +85,7 @@ "Log out": "Logg ut", "Manage preferences": "Administrer preferanser", "Meetings & Conferences": "Møter & Konferanser", + "Membership ID copied to clipboard": "Medlems-ID kopiert til utklippstavlen", "Member price": "Medlemspris", "Member price from": "Medlemspris fra", "Members": "Medlemmer", @@ -123,8 +124,8 @@ "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", - "Points": "Poeng", "points": "Poeng", + "Points": "Poeng", "Points being calculated": "Poeng beregnes", "Points earned prior to May 1, 2021": "Opptjente poeng før 1. mai 2021", "Points may take up to 10 days to be displayed.": "Det kan ta opptil 10 dager før poeng vises.", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 77a3758d1..e08e9afe3 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -87,6 +87,7 @@ "Log out": "Logga ut", "Manage preferences": "Hantera inställningar", "Meetings & Conferences": "Möten & Konferenser", + "Membership ID copied to clipboard": "Medlems-ID kopierat till urklipp", "Member price": "Medlemspris", "Member price from": "Medlemspris från", "Members": "Medlemmar", @@ -125,8 +126,8 @@ "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", - "Points": "Poäng", "points": "poäng", + "Points": "Poäng", "Points being calculated": "Poäng beräknas", "Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021", "Points may take up to 10 days to be displayed.": "Det kan ta upp till 10 dagar innan poäng visas.", From 5150a870f592fb186d612826a0764673500eea84 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Tue, 3 Sep 2024 09:42:25 +0200 Subject: [PATCH 087/319] SW-352 using loading.ts between page transations --- app/[lang]/(live)/(protected)/my-pages/[...path]/loading.tsx | 5 +++++ app/[lang]/(live)/(protected)/my-pages/loading.tsx | 5 +++++ app/[lang]/(live)/(public)/loading.tsx | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 app/[lang]/(live)/(protected)/my-pages/[...path]/loading.tsx create mode 100644 app/[lang]/(live)/(protected)/my-pages/loading.tsx create mode 100644 app/[lang]/(live)/(public)/loading.tsx diff --git a/app/[lang]/(live)/(protected)/my-pages/[...path]/loading.tsx b/app/[lang]/(live)/(protected)/my-pages/[...path]/loading.tsx new file mode 100644 index 000000000..c739b6635 --- /dev/null +++ b/app/[lang]/(live)/(protected)/my-pages/[...path]/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return +} diff --git a/app/[lang]/(live)/(protected)/my-pages/loading.tsx b/app/[lang]/(live)/(protected)/my-pages/loading.tsx new file mode 100644 index 000000000..c739b6635 --- /dev/null +++ b/app/[lang]/(live)/(protected)/my-pages/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return +} diff --git a/app/[lang]/(live)/(public)/loading.tsx b/app/[lang]/(live)/(public)/loading.tsx new file mode 100644 index 000000000..c739b6635 --- /dev/null +++ b/app/[lang]/(live)/(public)/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return +} From bc3233ff64b206e91b65c27f8547cc7c658d0951 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Tue, 3 Sep 2024 08:43:13 +0000 Subject: [PATCH 088/319] Merged in feat/SW-196-design-fixes (pull request #547) Feat/SW-196 design fixes * feat(SW-196): Updated copy My credit cards -> My payment cards * fix: update design system version * feat(SW-196): Update Header component to use Preamble instead of Subtitle * feat(SW-196): Minor design fixes Approved-by: Christel Westerberg --- .../(protected)/my-pages/profile/@creditCards/page.tsx | 2 +- components/ContentType/HotelPage/Rooms/index.tsx | 2 +- components/Loyalty/Blocks/CardsGrid/index.tsx | 2 +- components/Loyalty/Blocks/DynamicContent/index.tsx | 2 +- components/MyPages/Blocks/Benefits/CurrentLevel/index.tsx | 2 +- components/MyPages/Blocks/Benefits/NextLevel/index.tsx | 2 +- components/MyPages/Blocks/Overview/index.tsx | 2 +- components/MyPages/Blocks/Points/EarnAndBurn/index.tsx | 2 +- components/MyPages/Blocks/Points/Overview/index.tsx | 2 +- components/MyPages/Blocks/Shortcuts/index.tsx | 2 +- components/MyPages/Blocks/Stays/Previous/index.tsx | 2 +- components/MyPages/Blocks/Stays/Soonest/index.tsx | 2 +- components/MyPages/Blocks/Stays/Upcoming/index.tsx | 2 +- components/Section/Header/header.module.css | 4 ++-- components/Section/Header/index.tsx | 6 +++--- components/Section/Link/index.tsx | 2 +- components/TempDesignSystem/Link/link.module.css | 6 +++--- components/TempDesignSystem/Text/Subtitle/variants.ts | 2 +- i18n/dictionaries/da.json | 2 +- i18n/dictionaries/de.json | 2 +- i18n/dictionaries/en.json | 2 +- i18n/dictionaries/fi.json | 2 +- i18n/dictionaries/no.json | 2 +- i18n/dictionaries/sv.json | 4 ++-- package-lock.json | 2 +- package.json | 2 +- types/components/myPages/header.ts | 2 +- 27 files changed, 33 insertions(+), 33 deletions(-) diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx index 400f10f2c..a37b0047b 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx @@ -23,7 +23,7 @@ export default async function CreditCardSlot({ params }: PageArgs) {
- {formatMessage({ id: "My credit cards" })} + {formatMessage({ id: "My payment cards" })} {formatMessage({ diff --git a/components/ContentType/HotelPage/Rooms/index.tsx b/components/ContentType/HotelPage/Rooms/index.tsx index 163f83aa9..08e8f07ce 100644 --- a/components/ContentType/HotelPage/Rooms/index.tsx +++ b/components/ContentType/HotelPage/Rooms/index.tsx @@ -54,7 +54,7 @@ export function Rooms({ rooms }: RoomsProps) { {mappedRooms.map( diff --git a/components/Loyalty/Blocks/CardsGrid/index.tsx b/components/Loyalty/Blocks/CardsGrid/index.tsx index 1cdccc0b3..72da61722 100644 --- a/components/Loyalty/Blocks/CardsGrid/index.tsx +++ b/components/Loyalty/Blocks/CardsGrid/index.tsx @@ -15,7 +15,7 @@ export default function CardsGrid({ diff --git a/components/Loyalty/Blocks/DynamicContent/index.tsx b/components/Loyalty/Blocks/DynamicContent/index.tsx index e033b1028..79908505c 100644 --- a/components/Loyalty/Blocks/DynamicContent/index.tsx +++ b/components/Loyalty/Blocks/DynamicContent/index.tsx @@ -53,7 +53,7 @@ export default function DynamicContent({ ) : displayHeader ? ( diff --git a/components/MyPages/Blocks/Benefits/CurrentLevel/index.tsx b/components/MyPages/Blocks/Benefits/CurrentLevel/index.tsx index b6b2c7179..71f94fdea 100644 --- a/components/MyPages/Blocks/Benefits/CurrentLevel/index.tsx +++ b/components/MyPages/Blocks/Benefits/CurrentLevel/index.tsx @@ -43,7 +43,7 @@ export default async function CurrentBenefitsBlock({ return ( - + {currentLevel.benefits.map((benefit, idx) => (
diff --git a/components/MyPages/Blocks/Benefits/NextLevel/index.tsx b/components/MyPages/Blocks/Benefits/NextLevel/index.tsx index 5716d1495..7b82a3dd0 100644 --- a/components/MyPages/Blocks/Benefits/NextLevel/index.tsx +++ b/components/MyPages/Blocks/Benefits/NextLevel/index.tsx @@ -39,7 +39,7 @@ export default async function NextLevelBenefitsBlock({ // TODO: how to handle different count of unlockable benefits? return ( - + {nextLevel.benefits.map((benefit) => (
diff --git a/components/MyPages/Blocks/Overview/index.tsx b/components/MyPages/Blocks/Overview/index.tsx index db6874997..f9b4deab7 100644 --- a/components/MyPages/Blocks/Overview/index.tsx +++ b/components/MyPages/Blocks/Overview/index.tsx @@ -26,7 +26,7 @@ export default async function Overview({ return ( - + diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/index.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/index.tsx index 83a325a04..9172bf7fd 100644 --- a/components/MyPages/Blocks/Points/EarnAndBurn/index.tsx +++ b/components/MyPages/Blocks/Points/EarnAndBurn/index.tsx @@ -13,7 +13,7 @@ export default async function EarnAndBurn({ }: AccountPageComponentProps) { return ( - + diff --git a/components/MyPages/Blocks/Points/Overview/index.tsx b/components/MyPages/Blocks/Points/Overview/index.tsx index 39f8b6031..047bb6b93 100644 --- a/components/MyPages/Blocks/Points/Overview/index.tsx +++ b/components/MyPages/Blocks/Points/Overview/index.tsx @@ -25,7 +25,7 @@ export default async function PointsOverview({ return ( - + diff --git a/components/MyPages/Blocks/Shortcuts/index.tsx b/components/MyPages/Blocks/Shortcuts/index.tsx index 3f2012708..1188c06fe 100644 --- a/components/MyPages/Blocks/Shortcuts/index.tsx +++ b/components/MyPages/Blocks/Shortcuts/index.tsx @@ -16,7 +16,7 @@ export default function Shortcuts({ }: ShortcutsProps) { return ( - +
{shortcuts.map((shortcut) => ( - + diff --git a/components/MyPages/Blocks/Stays/Soonest/index.tsx b/components/MyPages/Blocks/Stays/Soonest/index.tsx index 990c7b850..5069d5276 100644 --- a/components/MyPages/Blocks/Stays/Soonest/index.tsx +++ b/components/MyPages/Blocks/Stays/Soonest/index.tsx @@ -22,7 +22,7 @@ export default async function SoonestStays({ return ( - + {response.data.length ? ( {response.data.map((stay) => ( diff --git a/components/MyPages/Blocks/Stays/Upcoming/index.tsx b/components/MyPages/Blocks/Stays/Upcoming/index.tsx index c56bf8f41..6c3e91b71 100644 --- a/components/MyPages/Blocks/Stays/Upcoming/index.tsx +++ b/components/MyPages/Blocks/Stays/Upcoming/index.tsx @@ -22,7 +22,7 @@ export default async function UpcomingStays({ return ( - + {initialUpcomingStays?.data.length ? ( ) : ( diff --git a/components/Section/Header/header.module.css b/components/Section/Header/header.module.css index 0a0b6513e..48c539418 100644 --- a/components/Section/Header/header.module.css +++ b/components/Section/Header/header.module.css @@ -6,7 +6,7 @@ } .title, -.subtitle { +.preamble { grid-column: 1 / -1; } @@ -19,7 +19,7 @@ grid-column: 1 / 2; } - .subtitle { + .preamble { grid-column: 1 / 2; } } diff --git a/components/Section/Header/index.tsx b/components/Section/Header/index.tsx index e005a8c07..b52a741dc 100644 --- a/components/Section/Header/index.tsx +++ b/components/Section/Header/index.tsx @@ -1,4 +1,4 @@ -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Preamble from "@/components/TempDesignSystem/Text/Preamble" import Title from "@/components/TempDesignSystem/Text/Title" import SectionLink from "../Link" @@ -9,7 +9,7 @@ import type { HeaderProps } from "@/types/components/myPages/header" export default function SectionHeader({ link, - subtitle, + preamble, title, topTitle = false, textTransform, @@ -24,7 +24,7 @@ export default function SectionHeader({ > {title} - {subtitle && {subtitle}} + {preamble && {preamble}} ) diff --git a/components/Section/Link/index.tsx b/components/Section/Link/index.tsx index c8b27aedd..a4acab664 100644 --- a/components/Section/Link/index.tsx +++ b/components/Section/Link/index.tsx @@ -20,8 +20,8 @@ export default function SectionLink({ link, variant }: SectionLinkProps) { href={link.href} variant="underscored" > - {link.text} + ) } diff --git a/components/TempDesignSystem/Link/link.module.css b/components/TempDesignSystem/Link/link.module.css index ca926d6c6..d7f157a30 100644 --- a/components/TempDesignSystem/Link/link.module.css +++ b/components/TempDesignSystem/Link/link.module.css @@ -131,7 +131,7 @@ } .peach80 { - color: var(--Primary-Light-On-Surface-Accent); + color: var(--Base-Text-Medium-contrast); } .red { @@ -140,12 +140,12 @@ .peach80:hover, .peach80:active { - color: var(--Primary-Light-On-Surface-Hover); + color: var(--Base-Text-High-contrast); } .peach80:hover *, .peach80:active * { - fill: var(--Primary-Light-On-Surface-Hover); + fill: var(--Base-Text-High-contrast); } .white { diff --git a/components/TempDesignSystem/Text/Subtitle/variants.ts b/components/TempDesignSystem/Text/Subtitle/variants.ts index a4cfb6095..afb33bde1 100644 --- a/components/TempDesignSystem/Text/Subtitle/variants.ts +++ b/components/TempDesignSystem/Text/Subtitle/variants.ts @@ -24,7 +24,7 @@ const config = { }, }, defaultVariants: { - color: "burgundy", + color: "black", textAlign: "left", textTransform: "regular", type: "one", diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index a50f0448a..fa4433bbd 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -94,9 +94,9 @@ "Modify": "Ændre", "Month": "Måned", "My communication preferences": "Mine kommunikationspræferencer", - "My credit cards": "Mine kreditkort", "My membership cards": "Mine medlemskort", "My pages": "Mine sider", + "My payment cards": "Mine betalingskort", "My wishes": "Mine ønsker", "New password": "Nyt kodeord", "Next": "Næste", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 10a8e7fcf..b52bba60e 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -92,9 +92,9 @@ "Modify": "Ändern", "Month": "Monat", "My communication preferences": "Meine Kommunikationseinstellungen", - "My credit cards": "Meine Kreditkarten", "My membership cards": "Meine Mitgliedskarten", "My pages": "Meine Seiten", + "My payment cards": "Meine Zahlungskarten", "My wishes": "Meine Wünsche", "New password": "Neues Kennwort", "Next": "Nächste", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 8d6ce6f44..89a87b728 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -99,9 +99,9 @@ "Modify": "Modify", "Month": "Month", "My communication preferences": "My communication preferences", - "My credit cards": "My credit cards", "My membership cards": "My membership cards", "My pages": "My pages", + "My payment cards": "My payment cards", "My wishes": "My wishes", "New password": "New password", "Next": "Next", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 3c33b7848..92dc61768 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -94,9 +94,9 @@ "Modify": "Muokkaa", "Month": "Kuukausi", "My communication preferences": "Viestintämieltymykseni", - "My credit cards": "Luottokorttini", "My membership cards": "Jäsenkorttini", "My pages": "Omat sivut", + "My payment cards": "Minun maksukortit", "My wishes": "Toiveeni", "New password": "Uusi salasana", "Next": "Seuraava", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 7cebd9aac..ef8593a95 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -94,9 +94,9 @@ "Modify": "Endre", "Month": "Måned", "My communication preferences": "Mine kommunikasjonspreferanser", - "My credit cards": "Kredittkortene mine", "My membership cards": "Mine medlemskort", "My pages": "Mine sider", + "My payment cards": "Mine betalingskort", "My wishes": "Mine ønsker", "New password": "Nytt passord", "Next": "Neste", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index e08e9afe3..1103fa4c3 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -96,9 +96,9 @@ "Modify": "Ändra", "Month": "Månad", "My communication preferences": "Mina kommunikationspreferenser", - "My credit cards": "Mina kreditkort", "My membership cards": "Mina medlemskort", "My pages": "Mina sidor", + "My payment cards": "Mina betalningskort", "My wishes": "Mina önskningar", "New password": "Nytt lösenord", "Next": "Nästa", @@ -183,7 +183,7 @@ "Type of room": "Rumstyp", "uppercase letter": "stor bokstav", "Use bonus cheque": "Use bonus cheque", - "User information": "Användar information", + "User information": "Användarinformation", "View your booking": "Visa din bokning", "Visiting address": "Besöksadress", "We could not add a card right now, please try again later.": "Vi kunde inte lägga till ett kort just nu, vänligen försök igen senare.", diff --git a/package-lock.json b/package-lock.json index dd2ab62da..14db5a10c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-slot": "^1.0.2", "@react-aria/ssr": "^3.9.5", - "@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.8", + "@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.9", "@t3-oss/env-nextjs": "^0.9.2", "@tanstack/react-query": "^5.28.6", "@trpc/client": "^11.0.0-rc.467", diff --git a/package.json b/package.json index 948db7f1a..bc4b33000 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-slot": "^1.0.2", "@react-aria/ssr": "^3.9.5", - "@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.8", + "@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.9", "@t3-oss/env-nextjs": "^0.9.2", "@tanstack/react-query": "^5.28.6", "@trpc/client": "^11.0.0-rc.467", diff --git a/types/components/myPages/header.ts b/types/components/myPages/header.ts index af1ed6a7e..619dc805b 100644 --- a/types/components/myPages/header.ts +++ b/types/components/myPages/header.ts @@ -5,7 +5,7 @@ export type HeaderProps = { href: string text: string } - subtitle: string | null + preamble: string | null textTransform?: HeadingProps["textTransform"] title: string | null topTitle?: boolean From 70297bec91f5b345b523aea0b28c57efe4a6bcd7 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 2 Sep 2024 15:21:54 +0200 Subject: [PATCH 089/319] feat(SW-330): layout and some styling fixes for hotel-pages --- app/globals.css | 4 +- .../IntroSection/introSection.module.css | 1 + .../HotelPage/PreviewImages/index.tsx | 1 - .../PreviewImages/previewImages.module.css | 32 ++++++++------- .../HotelPage/Rooms/RoomCard/index.tsx | 14 +++---- .../ContentType/HotelPage/Rooms/index.tsx | 2 +- .../TabNavigation/tabNavigation.module.css | 13 ++++-- .../HotelPage/hotelPage.module.css | 41 ++++++++++++++----- components/ContentType/HotelPage/index.tsx | 10 +++-- components/MaxWidth/maxWidth.module.css | 2 +- 10 files changed, 75 insertions(+), 45 deletions(-) diff --git a/app/globals.css b/app/globals.css index 6180a72f6..e3ad605ff 100644 --- a/app/globals.css +++ b/app/globals.css @@ -97,7 +97,9 @@ } :root { - --max-width: 113.5rem; + --current-max-width: 113.5rem; + + --max-width: 94.5rem; --max-width-content: 74.75rem; --max-width-text-block: 49.5rem; --mobile-site-header-height: 70.047px; diff --git a/components/ContentType/HotelPage/IntroSection/introSection.module.css b/components/ContentType/HotelPage/IntroSection/introSection.module.css index 4332cadbc..1697b7ee8 100644 --- a/components/ContentType/HotelPage/IntroSection/introSection.module.css +++ b/components/ContentType/HotelPage/IntroSection/introSection.module.css @@ -2,6 +2,7 @@ display: grid; gap: var(--Spacing-x2); position: relative; + max-width: var(--max-width-text-block); } .mainContent { diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index 3b17f0dc4..3ee992bce 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -20,7 +20,6 @@ export default async function PreviewImages({ images }: PreviewImagesProps) { title={image.title} width={index === 0 ? 752 : 292} height={index === 0 ? 540 : 266} - objectFit="cover" className={styles.image} /> ))} diff --git a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css index 70323fe20..7a1818557 100644 --- a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css +++ b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css @@ -1,26 +1,17 @@ .imageWrapper { display: grid; - grid-template-areas: - "main" - "main" - "main"; gap: 8px; position: relative; width: 100%; - padding-top: var(--Spacing-x2); - background-color: var(--Base-Surface-Subtle-Normal); + padding: var(--Spacing-x2) var(--Spacing-x2) 0; } .image { object-fit: cover; border-radius: var(--Corner-radius-Medium); - display: block; width: 100%; height: 100%; -} - -.imageWrapper > :first-child { - grid-area: main; + max-height: 30vh; } .imageWrapper > :nth-child(2), @@ -33,20 +24,29 @@ bottom: var(--Spacing-x2); right: var(--Spacing-x4); z-index: 1; - display: block; } @media screen and (min-width: 1367px) { .imageWrapper { - grid-template-columns: 72fr 28fr; + grid-template-columns: 70% 30%; + grid-template-rows: repeat(2, 16.625rem); grid-template-areas: "main side1" "main side2"; + padding: var(--Spacing-x2) var(--Spacing-x5) 0; + } + + .image { + max-height: none; + } + + .imageWrapper > :first-child { + grid-area: main; } .imageWrapper > :nth-child(2), .imageWrapper > :nth-child(3) { - display: block; + display: initial; } .imageWrapper > :nth-child(2) { @@ -56,4 +56,8 @@ .desktopGrid > :nth-child(3) { grid-area: side2; } + + .seeAllButton { + right: var(--Spacing-x7); + } } diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx index c9d299b6b..e0678cd5e 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx +++ b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx @@ -2,9 +2,9 @@ import { useIntl } from "react-intl" -import { ImageIcon } from "@/components/Icons" +import { ChevronRightIcon, ImageIcon } from "@/components/Icons" import Image from "@/components/Image" -import Link from "@/components/TempDesignSystem/Link" +import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Title from "@/components/TempDesignSystem/Text/Title" @@ -62,14 +62,10 @@ export function RoomCard({ {subtitle}
- +
) diff --git a/components/ContentType/HotelPage/Rooms/index.tsx b/components/ContentType/HotelPage/Rooms/index.tsx index 08e8f07ce..b6fa4f7dc 100644 --- a/components/ContentType/HotelPage/Rooms/index.tsx +++ b/components/ContentType/HotelPage/Rooms/index.tsx @@ -80,7 +80,7 @@ export function Rooms({ rooms }: RoomsProps) {
+ ) : ( + + {title} + + ) +} diff --git a/components/Header/MainMenu/MenuItem/menuItem.module.css b/components/Header/MainMenu/MenuItem/menuItem.module.css new file mode 100644 index 000000000..4b8afc4de --- /dev/null +++ b/components/Header/MainMenu/MenuItem/menuItem.module.css @@ -0,0 +1,21 @@ +.navigationButton { + display: flex; + gap: var(--Spacing-x1); + align-items: center; + background-color: transparent; + color: var(--Base-Text-High-contrast); + border-width: 0; + padding: 0; + cursor: pointer; + font-family: var(--typography-Body-Bold-fontFamily); + font-weight: var(--typography-Body-Bold-fontWeight); + font-size: var(--typography-Body-Bold-fontSize); +} + +.chevron { + transition: transform 0.2s; +} + +.chevron.isExpanded { + transform: rotate(180deg); +} diff --git a/components/Header/MainMenu/MenuItem/menuItem.ts b/components/Header/MainMenu/MenuItem/menuItem.ts new file mode 100644 index 000000000..b4c761110 --- /dev/null +++ b/components/Header/MainMenu/MenuItem/menuItem.ts @@ -0,0 +1,5 @@ +import { MenuItem } from ".." + +export interface MenuItemProps { + item: MenuItem +} diff --git a/components/Header/MainMenu/User/Avatar/avatar.module.css b/components/Header/MainMenu/User/Avatar/avatar.module.css new file mode 100644 index 000000000..0c6055350 --- /dev/null +++ b/components/Header/MainMenu/User/Avatar/avatar.module.css @@ -0,0 +1,16 @@ +.avatar { + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + border-radius: 50%; + width: 2rem; + height: 2rem; + background-color: var(--Main-Grey-40); +} + +.initials { + font-size: 0.75rem; + color: var(--Base-Text-Inverted); + background-color: var(--Scandic-Peach-70); +} diff --git a/components/Header/MainMenu/User/Avatar/avatar.ts b/components/Header/MainMenu/User/Avatar/avatar.ts new file mode 100644 index 000000000..6156e0bbe --- /dev/null +++ b/components/Header/MainMenu/User/Avatar/avatar.ts @@ -0,0 +1,6 @@ +import { ImageProps } from "next/image" + +export interface AvatarProps { + image?: ImageProps + initials?: string +} diff --git a/components/Header/MainMenu/User/Avatar/index.tsx b/components/Header/MainMenu/User/Avatar/index.tsx new file mode 100644 index 000000000..ceff75675 --- /dev/null +++ b/components/Header/MainMenu/User/Avatar/index.tsx @@ -0,0 +1,19 @@ +import { PersonIcon } from "@/components/Icons" +import Image from "@/components/Image" + +import { AvatarProps } from "./avatar" + +import styles from "./avatar.module.css" + +export default function Avatar({ image, initials }: AvatarProps) { + let classNames = [styles.avatar] + let element = + if (image) { + classNames.push(styles.image) + element = {image.alt} + } else if (initials) { + classNames.push(styles.initials) + element = {initials} + } + return {element} +} diff --git a/components/Header/MainMenu/User/index.tsx b/components/Header/MainMenu/User/index.tsx new file mode 100644 index 000000000..d26bc1173 --- /dev/null +++ b/components/Header/MainMenu/User/index.tsx @@ -0,0 +1,16 @@ +import Link from "next/link" + +import Avatar from "./Avatar" + +import styles from "./user.module.css" + +export default function User() { + return ( +
+ + + Log in/Join + +
+ ) +} diff --git a/components/Header/MainMenu/User/user.module.css b/components/Header/MainMenu/User/user.module.css new file mode 100644 index 000000000..c43fa17fe --- /dev/null +++ b/components/Header/MainMenu/User/user.module.css @@ -0,0 +1,12 @@ +.user { +} + +.link { + display: flex; + gap: var(--Spacing-x1); + align-items: center; + font-family: var(--typography-Body-Bold-fontFamily); + font-weight: 600; + color: var(--Base-Text-High-contrast); + text-decoration: none; +} diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx new file mode 100644 index 000000000..d0b85ceb4 --- /dev/null +++ b/components/Header/MainMenu/index.tsx @@ -0,0 +1,125 @@ +import Link from "next/link" + +import Image from "@/components/Image" + +import Menu from "./Menu" +import User from "./User" + +import styles from "./mainMenu.module.css" + +export interface MenuItem { + id: string + title: string + href: string + children?: { + groupTitle: string + children: { + id: string + title: string + href: string + }[] + }[] + seeAllLinkText?: string + infoCard?: { + scriptedTitle: string + title: string + description: string + ctaLink: string + } +} + +export default async function MainMenu() { + const menuItems: MenuItem[] = [ + { + id: "hotels", + title: "Hotels", + href: "/hotels", + children: [], + }, + { + id: "business", + title: "Business", + href: "/business", + children: [ + { + groupTitle: "Top conference venues", + children: [ + { + id: "stockholm", + title: "Stockholm", + href: "/stockholm", + }, + { + id: "bergen", + title: "Bergen", + href: "/bergen", + }, + { + id: "copenhagen", + title: "Copenhagen", + href: "/copenhagen", + }, + ], + }, + { + groupTitle: "Scandic for business", + children: [ + { + id: "book-a-venue", + title: "Book a venue", + href: "/book-a-venue", + }, + { + id: "conference-packages", + title: "Conference packages", + href: "/conference-packages", + }, + { + id: "co-working", + title: "Co-working", + href: "/co-working", + }, + ], + }, + ], + seeAllLinkText: "See all conference & meeting venues", + infoCard: { + scriptedTitle: "Stockholm", + title: "Meeting venues in Stockholm", + description: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed et felis metus. Sed et felis metus.", + ctaLink: "/stockholm", + }, + }, + { + id: "offers", + title: "Offers", + href: "/offers", + }, + { + id: "restaurants", + title: "Restaurants", + href: "/restaurants", + }, + ] + return ( +
+ +
+ ) +} diff --git a/components/Header/MainMenu/mainMenu.module.css b/components/Header/MainMenu/mainMenu.module.css new file mode 100644 index 000000000..e58ac423c --- /dev/null +++ b/components/Header/MainMenu/mainMenu.module.css @@ -0,0 +1,15 @@ +.mainMenu { + background-color: var(--Base-Surface-Primary-light-Normal); + padding: var(--Spacing-x2); + border-top: 1px solid var(--Base-Border-Subtle); + border-bottom: 1px solid var(--Base-Border-Subtle); +} + +.content { + max-width: 89.5rem; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--Spacing-x3); +} diff --git a/components/Header/TopMenu/Button/button.module.css b/components/Header/TopMenu/Button/button.module.css new file mode 100644 index 000000000..004e01d19 --- /dev/null +++ b/components/Header/TopMenu/Button/button.module.css @@ -0,0 +1,9 @@ +.button { + background-color: transparent; + color: var(--Base-Text-High-contrast); + border-width: 0; + cursor: pointer; + display: flex; + gap: var(--Spacing-x1); + align-items: center; +} diff --git a/components/Header/TopMenu/Button/button.ts b/components/Header/TopMenu/Button/button.ts new file mode 100644 index 000000000..9aa84a33f --- /dev/null +++ b/components/Header/TopMenu/Button/button.ts @@ -0,0 +1,2 @@ +export interface ButtonProps + extends React.ButtonHTMLAttributes {} diff --git a/components/Header/TopMenu/Button/index.tsx b/components/Header/TopMenu/Button/index.tsx new file mode 100644 index 000000000..437fe2d73 --- /dev/null +++ b/components/Header/TopMenu/Button/index.tsx @@ -0,0 +1,11 @@ +import { ButtonProps } from "./button" + +import styles from "./button.module.css" + +export default function Button({ children, ...props }: ButtonProps) { + return ( + + ) +} diff --git a/components/Header/TopMenu/LanguageSwitcher/index.tsx b/components/Header/TopMenu/LanguageSwitcher/index.tsx new file mode 100644 index 000000000..f13a0153a --- /dev/null +++ b/components/Header/TopMenu/LanguageSwitcher/index.tsx @@ -0,0 +1,30 @@ +"use client" + +import { useState } from "react" + +import { ChevronDownIcon, GlobeIcon } from "@/components/Icons" + +import Button from "../Button" + +import styles from "./languageSwitcher.module.css" + +export default function LanguageSwitcher() { + const [isExpanded, setIsExpanded] = useState(false) + + function handleButtonClick() { + setIsExpanded((prev) => !prev) + } + + return ( + + ) +} diff --git a/components/Header/TopMenu/LanguageSwitcher/languageSwitcher.module.css b/components/Header/TopMenu/LanguageSwitcher/languageSwitcher.module.css new file mode 100644 index 000000000..739d7d34e --- /dev/null +++ b/components/Header/TopMenu/LanguageSwitcher/languageSwitcher.module.css @@ -0,0 +1,17 @@ +.button { + background-color: transparent; + color: var(--Base-Text-High-contrast); + border-width: 0; + cursor: pointer; + display: flex; + gap: var(--Spacing-x1); + align-items: center; +} + +.chevron { + transition: transform 0.2s; +} + +.chevron.isExpanded { + transform: rotate(180deg); +} diff --git a/components/Header/TopMenu/Search/index.tsx b/components/Header/TopMenu/Search/index.tsx new file mode 100644 index 000000000..5668d49aa --- /dev/null +++ b/components/Header/TopMenu/Search/index.tsx @@ -0,0 +1,12 @@ +import { SearchIcon } from "@/components/Icons" + +import Button from "../Button" + +export default function Search() { + return ( + + ) +} diff --git a/components/Header/TopMenu/Search/search.module.css b/components/Header/TopMenu/Search/search.module.css new file mode 100644 index 000000000..004e01d19 --- /dev/null +++ b/components/Header/TopMenu/Search/search.module.css @@ -0,0 +1,9 @@ +.button { + background-color: transparent; + color: var(--Base-Text-High-contrast); + border-width: 0; + cursor: pointer; + display: flex; + gap: var(--Spacing-x1); + align-items: center; +} diff --git a/components/Header/TopMenu/index.tsx b/components/Header/TopMenu/index.tsx new file mode 100644 index 000000000..73261bc5c --- /dev/null +++ b/components/Header/TopMenu/index.tsx @@ -0,0 +1,24 @@ +import { GiftIcon } from "@/components/Icons" +import Link from "@/components/TempDesignSystem/Link" + +import LanguageSwitcher from "./LanguageSwitcher" +import Search from "./Search" + +import styles from "./topMenu.module.css" + +export default async function TopMenu() { + return ( +
+
+ + + Join Scandic Friends + +
+ + +
+
+
+ ) +} diff --git a/components/Header/TopMenu/topMenu.module.css b/components/Header/TopMenu/topMenu.module.css new file mode 100644 index 000000000..4d86f55d2 --- /dev/null +++ b/components/Header/TopMenu/topMenu.module.css @@ -0,0 +1,18 @@ +.topMenu { + background-color: var(--Base-Surface-Subtle-Normal); + padding: var(--Spacing-x2) var(--Spacing-x-one-and-half); +} + +.content { + max-width: 89.5rem; + margin: 0 auto; + display: flex; + justify-content: space-between; + gap: var(--Spacing-x3); +} + +.right { + display: flex; + gap: var(--Spacing-x2); + align-items: center; +} diff --git a/components/Header/header.module.css b/components/Header/header.module.css new file mode 100644 index 000000000..a3d728549 --- /dev/null +++ b/components/Header/header.module.css @@ -0,0 +1,4 @@ +.header { + font-family: var(--typography-Body-Regular-fontFamily); + color: var(--Base-Text-High-contrast); +} diff --git a/components/Header/index.tsx b/components/Header/index.tsx new file mode 100644 index 000000000..d6693a52b --- /dev/null +++ b/components/Header/index.tsx @@ -0,0 +1,13 @@ +import MainMenu from "./MainMenu" +import TopMenu from "./TopMenu" + +import styles from "./header.module.css" + +export default async function Header() { + return ( +
+ + +
+ ) +} diff --git a/components/Icons/Gift.tsx b/components/Icons/Gift.tsx new file mode 100644 index 000000000..b07015db5 --- /dev/null +++ b/components/Icons/Gift.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function GiftIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Person.tsx b/components/Icons/Person.tsx index 20c66c236..bc2452ac4 100644 --- a/components/Icons/Person.tsx +++ b/components/Icons/Person.tsx @@ -7,28 +7,28 @@ export default function PersonIcon({ className, color, ...props }: IconProps) { return ( - + - + diff --git a/components/Icons/Search.tsx b/components/Icons/Search.tsx new file mode 100644 index 000000000..3e91f1f5c --- /dev/null +++ b/components/Icons/Search.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function GiftIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index e10495d52..3c7a37d7e 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -25,6 +25,7 @@ import { ElectricBikeIcon, EmailIcon, FitnessIcon, + GiftIcon, GlobeIcon, HouseIcon, ImageIcon, @@ -39,6 +40,7 @@ import { PlusCircleIcon, RestaurantIcon, SaunaIcon, + SearchIcon, TshirtWashIcon, WarningTriangle, WifiIcon, @@ -92,6 +94,8 @@ export function getIconByIconName(icon?: IconName): FC | null { return FacebookIcon case IconName.Fitness: return FitnessIcon + case IconName.Gift: + return GiftIcon case IconName.Globe: return GlobeIcon case IconName.House: @@ -122,6 +126,8 @@ export function getIconByIconName(icon?: IconName): FC | null { return RestaurantIcon case IconName.Sauna: return SaunaIcon + case IconName.Search: + return SearchIcon case IconName.Tripadvisor: return TripAdvisorIcon case IconName.TshirtWash: diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 7f0259352..0dbb3b628 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -21,6 +21,7 @@ export { default as DoorOpenIcon } from "./DoorOpen" export { default as ElectricBikeIcon } from "./ElectricBike" export { default as EmailIcon } from "./Email" export { default as FitnessIcon } from "./Fitness" +export { default as GiftIcon } from "./Gift" export { default as GlobeIcon } from "./Globe" export { default as HouseIcon } from "./House" export { default as ImageIcon } from "./Image" @@ -37,6 +38,7 @@ export { default as PriceTagIcon } from "./PriceTag" export { default as RestaurantIcon } from "./Restaurant" export { default as SaunaIcon } from "./Sauna" export { default as ScandicLogoIcon } from "./ScandicLogo" +export { default as SearchIcon } from "./Search" export { default as TshirtWashIcon } from "./TshirtWash" export { default as WarningTriangle } from "./WarningTriangle" export { default as WifiIcon } from "./Wifi" diff --git a/types/components/icon.ts b/types/components/icon.ts index d965a4e78..ed0d3da3b 100644 --- a/types/components/icon.ts +++ b/types/components/icon.ts @@ -29,6 +29,7 @@ export enum IconName { Email = "Email", Facebook = "Facebook", Fitness = "Fitness", + Gift = "Gift", Globe = "Globe", House = "House", Image = "Image", @@ -44,6 +45,7 @@ export enum IconName { PlusCircle = "PlusCircle", Restaurant = "Restaurant", Sauna = "Sauna", + Search = "Search", Tripadvisor = "Tripadvisor", TshirtWash = "TshirtWash", Wifi = "Wifi", From 45095a27d4c4512134beb17966c5d5d0175445f3 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 20 Aug 2024 13:31:14 +0200 Subject: [PATCH 091/319] feat(SW-184): added menu buttons and my pages menu --- .../Header/MainMenu/MenuButton/index.tsx | 11 +++ .../MainMenu/MenuButton/menuButton.module.css | 13 +++ components/Header/MainMenu/MenuItem/index.tsx | 9 +- .../MainMenu/MenuItem/menuItem.module.css | 14 ---- .../Avatar/avatar.module.css | 0 .../{User => MyPages}/Avatar/avatar.ts | 2 +- .../{User => MyPages}/Avatar/index.tsx | 0 components/Header/MainMenu/MyPages/index.tsx | 53 ++++++++++++ .../MainMenu/MyPages/myPages.module.css | 11 +++ .../Header/MainMenu/MyPagesMenu/index.tsx | 83 +++++++++++++++++++ .../MyPagesMenu/myPagesMenu.module.css | 58 +++++++++++++ components/Header/MainMenu/User/index.tsx | 16 ---- .../Header/MainMenu/User/user.module.css | 12 --- components/Header/MainMenu/index.tsx | 11 ++- .../Header/MainMenu/mainMenu.module.css | 2 +- components/Header/TopMenu/topMenu.module.css | 1 + components/Header/header.module.css | 2 + .../TempDesignSystem/Link/link.module.css | 20 ++--- i18n/dictionaries/en.json | 2 + stores/main-menu.ts | 1 + 20 files changed, 255 insertions(+), 66 deletions(-) create mode 100644 components/Header/MainMenu/MenuButton/index.tsx create mode 100644 components/Header/MainMenu/MenuButton/menuButton.module.css rename components/Header/MainMenu/{User => MyPages}/Avatar/avatar.module.css (100%) rename components/Header/MainMenu/{User => MyPages}/Avatar/avatar.ts (77%) rename components/Header/MainMenu/{User => MyPages}/Avatar/index.tsx (100%) create mode 100644 components/Header/MainMenu/MyPages/index.tsx create mode 100644 components/Header/MainMenu/MyPages/myPages.module.css create mode 100644 components/Header/MainMenu/MyPagesMenu/index.tsx create mode 100644 components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css delete mode 100644 components/Header/MainMenu/User/index.tsx delete mode 100644 components/Header/MainMenu/User/user.module.css diff --git a/components/Header/MainMenu/MenuButton/index.tsx b/components/Header/MainMenu/MenuButton/index.tsx new file mode 100644 index 000000000..f0ee1f2f1 --- /dev/null +++ b/components/Header/MainMenu/MenuButton/index.tsx @@ -0,0 +1,11 @@ +import styles from "./menuButton.module.css" + +export default function MenuButton({ + className, + ...props +}: React.ButtonHTMLAttributes) { + const classNames = className + ? `${styles.menuButton} ${className}` + : styles.menuButton + return + ) : ( {title} diff --git a/components/Header/MainMenu/MenuItem/menuItem.module.css b/components/Header/MainMenu/MenuItem/menuItem.module.css index 4b8afc4de..c743f0649 100644 --- a/components/Header/MainMenu/MenuItem/menuItem.module.css +++ b/components/Header/MainMenu/MenuItem/menuItem.module.css @@ -1,17 +1,3 @@ -.navigationButton { - display: flex; - gap: var(--Spacing-x1); - align-items: center; - background-color: transparent; - color: var(--Base-Text-High-contrast); - border-width: 0; - padding: 0; - cursor: pointer; - font-family: var(--typography-Body-Bold-fontFamily); - font-weight: var(--typography-Body-Bold-fontWeight); - font-size: var(--typography-Body-Bold-fontSize); -} - .chevron { transition: transform 0.2s; } diff --git a/components/Header/MainMenu/User/Avatar/avatar.module.css b/components/Header/MainMenu/MyPages/Avatar/avatar.module.css similarity index 100% rename from components/Header/MainMenu/User/Avatar/avatar.module.css rename to components/Header/MainMenu/MyPages/Avatar/avatar.module.css diff --git a/components/Header/MainMenu/User/Avatar/avatar.ts b/components/Header/MainMenu/MyPages/Avatar/avatar.ts similarity index 77% rename from components/Header/MainMenu/User/Avatar/avatar.ts rename to components/Header/MainMenu/MyPages/Avatar/avatar.ts index 6156e0bbe..1cb269879 100644 --- a/components/Header/MainMenu/User/Avatar/avatar.ts +++ b/components/Header/MainMenu/MyPages/Avatar/avatar.ts @@ -2,5 +2,5 @@ import { ImageProps } from "next/image" export interface AvatarProps { image?: ImageProps - initials?: string + initials?: string | null } diff --git a/components/Header/MainMenu/User/Avatar/index.tsx b/components/Header/MainMenu/MyPages/Avatar/index.tsx similarity index 100% rename from components/Header/MainMenu/User/Avatar/index.tsx rename to components/Header/MainMenu/MyPages/Avatar/index.tsx diff --git a/components/Header/MainMenu/MyPages/index.tsx b/components/Header/MainMenu/MyPages/index.tsx new file mode 100644 index 000000000..580d9b1a3 --- /dev/null +++ b/components/Header/MainMenu/MyPages/index.tsx @@ -0,0 +1,53 @@ +"use client" + +import Link from "next/link" +import { useIntl } from "react-intl" + +import { myPages } from "@/constants/routes/myPages" +import { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query" +import useDropdownStore from "@/stores/main-menu" + +import { ChevronDownIcon } from "@/components/Icons" +import useLang from "@/hooks/useLang" +import { getInitials } from "@/utils/user" + +import MenuButton from "../MenuButton" +import MyPagesMenu from "../MyPagesMenu" +import Avatar from "./Avatar" + +import styles from "./myPages.module.css" + +import { User } from "@/types/user" + +export default function MyPages({ + myPagesNavigation, + user, +}: { + myPagesNavigation: Awaited> + user: Pick | null +}) { + const intl = useIntl() + const lang = useLang() + + const { toggleMyPagesMobileMenu, isMyPagesMobileMenuOpen } = + useDropdownStore() + + return user ? ( + <> + + + {intl.formatMessage({ id: "Hi" })} {user.firstName}! + + + + + ) : ( + + + {intl.formatMessage({ id: "Log in/Join" })} + + ) +} diff --git a/components/Header/MainMenu/MyPages/myPages.module.css b/components/Header/MainMenu/MyPages/myPages.module.css new file mode 100644 index 000000000..2a5bc7a33 --- /dev/null +++ b/components/Header/MainMenu/MyPages/myPages.module.css @@ -0,0 +1,11 @@ +.button { + font-weight: 600; +} + +.chevron { + transition: transform 0.2s; +} + +.chevron.isExpanded { + transform: rotate(180deg); +} diff --git a/components/Header/MainMenu/MyPagesMenu/index.tsx b/components/Header/MainMenu/MyPagesMenu/index.tsx new file mode 100644 index 000000000..3d4e1ebf4 --- /dev/null +++ b/components/Header/MainMenu/MyPagesMenu/index.tsx @@ -0,0 +1,83 @@ +"use client" +import { useIntl } from "react-intl" + +import { logout } from "@/constants/routes/handleAuth" +import { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query" +import useDropdownStore from "@/stores/main-menu" + +import { ArrowRightIcon } from "@/components/Icons" +import Link from "@/components/TempDesignSystem/Link" +import useLang from "@/hooks/useLang" + +import styles from "./myPagesMenu.module.css" + +type Navigation = Awaited> + +export default function MyPagesMenu({ + navigation, +}: { + navigation: Navigation +}) { + const { formatMessage } = useIntl() + const lang = useLang() + const { toggleMyPagesMobileMenu, isMyPagesMobileMenuOpen } = + useDropdownStore() + + if (!navigation) { + return null + } + + console.log(navigation) + + return ( + + ) +} diff --git a/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css new file mode 100644 index 000000000..94f096a6f --- /dev/null +++ b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css @@ -0,0 +1,58 @@ +.myPagesMenu { + position: absolute; + top: 46px; + right: 0; + background-color: #fff; + border-top: 1px solid #e3e0db; + display: none; + list-style: none; + overflow-y: visible; + margin: 0; + padding: var(--Spacing-x2) var(--Spacing-x4); + border-radius: var(--Corner-radius-Large); + box-shadow: 0px 0px 14px 6px #0000001a; +} + +.myPagesMenu.isExpanded { + display: block; +} + +.friendTypeWrapper { + padding: 0 var(--Spacing-x1) var(--Spacing-x2); + font-weight: 400; + color: var(--UI-Text-Medium-contrast); +} +.friendType { + font-family: var(--typography-Title-5-fontFamily); + letter-spacing: var(--typography-Title-5-letterSpacing); + font-size: var(--typography-Caption-Bold-fontSize); + text-transform: uppercase; +} + +.friendType::after { + content: " · "; + display: inline; + padding: 0 var(--Spacing-x-half); +} + +.groups, +.menuItems { + list-style: none; +} + +.group { + padding: var(--Spacing-x2) 0; + border-top: 1px solid var(--Base-Border-Subtle); +} + +.group:last-child { + padding-bottom: 0; +} + +.arrow { + opacity: 0; +} + +.link:hover .arrow { + opacity: 1; +} diff --git a/components/Header/MainMenu/User/index.tsx b/components/Header/MainMenu/User/index.tsx deleted file mode 100644 index d26bc1173..000000000 --- a/components/Header/MainMenu/User/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import Link from "next/link" - -import Avatar from "./Avatar" - -import styles from "./user.module.css" - -export default function User() { - return ( -
- - - Log in/Join - -
- ) -} diff --git a/components/Header/MainMenu/User/user.module.css b/components/Header/MainMenu/User/user.module.css deleted file mode 100644 index c43fa17fe..000000000 --- a/components/Header/MainMenu/User/user.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.user { -} - -.link { - display: flex; - gap: var(--Spacing-x1); - align-items: center; - font-family: var(--typography-Body-Bold-fontFamily); - font-weight: 600; - color: var(--Base-Text-High-contrast); - text-decoration: none; -} diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx index d0b85ceb4..d16f829f1 100644 --- a/components/Header/MainMenu/index.tsx +++ b/components/Header/MainMenu/index.tsx @@ -1,9 +1,11 @@ import Link from "next/link" +import { serverClient } from "@/lib/trpc/server" + import Image from "@/components/Image" import Menu from "./Menu" -import User from "./User" +import MyPages from "./MyPages" import styles from "./mainMenu.module.css" @@ -29,6 +31,11 @@ export interface MenuItem { } export default async function MainMenu() { + const myPagesNavigation = + await serverClient().contentstack.myPages.navigation.get() + + const user = await serverClient().user.name() + const menuItems: MenuItem[] = [ { id: "hotels", @@ -118,7 +125,7 @@ export default async function MainMenu() { /> - +
) diff --git a/components/Header/MainMenu/mainMenu.module.css b/components/Header/MainMenu/mainMenu.module.css index e58ac423c..99a9a67e7 100644 --- a/components/Header/MainMenu/mainMenu.module.css +++ b/components/Header/MainMenu/mainMenu.module.css @@ -1,11 +1,11 @@ .mainMenu { background-color: var(--Base-Surface-Primary-light-Normal); padding: var(--Spacing-x2); - border-top: 1px solid var(--Base-Border-Subtle); border-bottom: 1px solid var(--Base-Border-Subtle); } .content { + position: relative; max-width: 89.5rem; margin: 0 auto; display: flex; diff --git a/components/Header/TopMenu/topMenu.module.css b/components/Header/TopMenu/topMenu.module.css index 4d86f55d2..f23170779 100644 --- a/components/Header/TopMenu/topMenu.module.css +++ b/components/Header/TopMenu/topMenu.module.css @@ -1,6 +1,7 @@ .topMenu { background-color: var(--Base-Surface-Subtle-Normal); padding: var(--Spacing-x2) var(--Spacing-x-one-and-half); + border-bottom: 1px solid var(--Base-Border-Subtle); } .content { diff --git a/components/Header/header.module.css b/components/Header/header.module.css index a3d728549..54032e59b 100644 --- a/components/Header/header.module.css +++ b/components/Header/header.module.css @@ -1,4 +1,6 @@ .header { + position: relative; font-family: var(--typography-Body-Regular-fontFamily); color: var(--Base-Text-High-contrast); + z-index: 1; } diff --git a/components/TempDesignSystem/Link/link.module.css b/components/TempDesignSystem/Link/link.module.css index d7f157a30..3fc3b2582 100644 --- a/components/TempDesignSystem/Link/link.module.css +++ b/components/TempDesignSystem/Link/link.module.css @@ -41,26 +41,18 @@ font-family: var(--typography-Body-Regular-fontFamily); font-size: var(--typography-Body-Regular-fontSize); line-height: var(--typography-Body-Regular-lineHeight); - letter-spacing: 0.096px; - padding: var(--Spacing-x1) var(--Spacing-x1) var(--Spacing-x1) - var(--Spacing-x-one-and-half); - width: 100%; + letter-spacing: var(--typography-Body-Regular-letterSpacing); + padding: var(--Spacing-x1); border-radius: var(--Corner-radius-Medium); - gap: 4px; display: flex; + gap: var(--Spacing-x1); + justify-content: space-between; align-items: center; - gap: 4px; - align-self: stretch; } -.myPageMobileDropdown.active { - background-color: var(--Scandic-Brand-Pale-Peach); +.myPageMobileDropdown:hover { + background-color: var(--Base-Surface-Primary-light-Hover-alt); border-radius: var(--Corner-radius-Medium); - font-family: var(--typography-Body-Underline-fontFamily); - font-size: var(--typography-Body-Underline-fontSize); - font-weight: var(--typography-Body-Underline-fontWeight); - letter-spacing: var(--typography-Body-Underline-letterSpacing); - line-height: var(--typography-Body-Underline-lineHeight); } .shortcut { diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 89a87b728..6a8459041 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -84,8 +84,10 @@ "Level 5": "Level 5", "Level 6": "Level 6", "Level 7": "Level 7", + "Hi": "Hi", "Level up to unlock": "Level up to unlock", "Log in": "Log in", + "Log in/Join": "Log in/Join", "Log in here": "Log in here", "Log out": "Log out", "Manage preferences": "Manage preferences", diff --git a/stores/main-menu.ts b/stores/main-menu.ts index cbd6f1004..68d4d2ba1 100644 --- a/stores/main-menu.ts +++ b/stores/main-menu.ts @@ -21,6 +21,7 @@ const useDropdownStore = create((set) => ({ toggleMyPagesMobileMenu: () => set((state) => { // Close the other dropdown if it's open + console.log({ state }) if (!state.isMyPagesMobileMenuOpen && state.isHamburgerMenuOpen) { set({ isHamburgerMenuOpen: false }) } From b37cc7a34caedd639fa1726ab8f04eb328b94eb2 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 20 Aug 2024 14:51:47 +0200 Subject: [PATCH 092/319] feat(SW-184): added language switcher --- .../(live)/@header/[...paths]/layout.tsx | 1 + app/[lang]/(live)/@header/page.tsx | 15 +---- .../Header/MainMenu/MyPagesMenu/index.tsx | 2 - .../MyPagesMenu/myPagesMenu.module.css | 9 +-- .../Header/TopMenu/LanguageSwitcher/index.tsx | 61 ++++++++++++++---- .../languageSwitcher.module.css | 63 +++++++++++++++++++ .../LanguageSwitcher/languageSwitcher.ts | 5 ++ components/Header/TopMenu/index.tsx | 10 ++- components/Header/TopMenu/topMenu.module.css | 1 + components/Header/header.module.css | 2 +- stores/main-menu.ts | 1 - 11 files changed, 133 insertions(+), 37 deletions(-) create mode 100644 components/Header/TopMenu/LanguageSwitcher/languageSwitcher.ts diff --git a/app/[lang]/(live)/@header/[...paths]/layout.tsx b/app/[lang]/(live)/@header/[...paths]/layout.tsx index 17edf6a50..bbd5697c2 100644 --- a/app/[lang]/(live)/@header/[...paths]/layout.tsx +++ b/app/[lang]/(live)/@header/[...paths]/layout.tsx @@ -5,5 +5,6 @@ import type { LangParams, LayoutArgs } from "@/types/params" export default function HeaderLayout({ params }: LayoutArgs) { setLang(params.lang) + return
} diff --git a/app/[lang]/(live)/@header/page.tsx b/app/[lang]/(live)/@header/page.tsx index 09220fbb6..adccd9484 100644 --- a/app/[lang]/(live)/@header/page.tsx +++ b/app/[lang]/(live)/@header/page.tsx @@ -1,9 +1,4 @@ -import { baseUrls } from "@/constants/routes/baseUrls" -import { serverClient } from "@/lib/trpc/server" - -import Header from "@/components/Current/Header" -import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher" -import MyPagesMobileDropdown from "@/components/Current/Header/MyPagesMobileDropdown" +import Header from "@/components/Header" import { setLang } from "@/i18n/serverContext" import { LangParams, PageArgs } from "@/types/params" @@ -11,11 +6,5 @@ import { LangParams, PageArgs } from "@/types/params" export default async function HeaderPage({ params }: PageArgs) { setLang(params.lang) - const navigation = await serverClient().contentstack.myPages.navigation.get() - return ( -
} - languageSwitcher={} - /> - ) + return
} diff --git a/components/Header/MainMenu/MyPagesMenu/index.tsx b/components/Header/MainMenu/MyPagesMenu/index.tsx index 3d4e1ebf4..3f4eb4213 100644 --- a/components/Header/MainMenu/MyPagesMenu/index.tsx +++ b/components/Header/MainMenu/MyPagesMenu/index.tsx @@ -27,8 +27,6 @@ export default function MyPagesMenu({ return null } - console.log(navigation) - return ( - +
) : ( diff --git a/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css index d6cbc237d..459fb6d9f 100644 --- a/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css +++ b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css @@ -1,3 +1,7 @@ +.myPagesMenu { + position: relative; +} + .button { font-weight: 600; } @@ -10,7 +14,7 @@ transform: rotate(180deg); } -.myPagesMenu { +.dropdown { position: absolute; top: 46px; right: 0; @@ -18,11 +22,25 @@ padding: var(--Spacing-x2) var(--Spacing-x4); border-radius: var(--Corner-radius-Large); box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); + min-width: 20rem; z-index: 1; display: none; } -.myPagesMenu.isExpanded { +/* Triangle above dropdown */ +.dropdown::before { + content: ""; + position: absolute; + top: -1.25rem; + right: 2.4rem; + transform: rotate(180deg); + border-width: 0.75rem; + border-style: solid; + border-color: var(--Base-Surface-Primary-light-Normal) transparent transparent + transparent; +} + +.dropdown.isExpanded { display: block; } diff --git a/components/Header/MainMenu/mainMenu.module.css b/components/Header/MainMenu/mainMenu.module.css index 83a5defcb..ddcb33627 100644 --- a/components/Header/MainMenu/mainMenu.module.css +++ b/components/Header/MainMenu/mainMenu.module.css @@ -5,8 +5,7 @@ } .nav { - position: relative; - max-width: 89.5rem; + max-width: var(--max-width-navigation); margin: 0 auto; display: flex; justify-content: space-between; diff --git a/components/Header/TopMenu/LanguageSwitcher/index.tsx b/components/Header/TopMenu/LanguageSwitcher/index.tsx index 8416c25b6..638a58dba 100644 --- a/components/Header/TopMenu/LanguageSwitcher/index.tsx +++ b/components/Header/TopMenu/LanguageSwitcher/index.tsx @@ -1,9 +1,9 @@ "use client" import Link from "next/link" -import { useState } from "react" import { Lang, languages } from "@/constants/languages" +import useDropdownStore from "@/stores/main-menu" import { CheckIcon, ChevronDownIcon, GlobeIcon } from "@/components/Icons" import useLang from "@/hooks/useLang" @@ -16,28 +16,24 @@ import { LanguageSwitcherProps } from "@/types/components/current/languageSwitch export default function LanguageSwitcher({ urls }: LanguageSwitcherProps) { const currentLanguage = useLang() - const [isExpanded, setIsExpanded] = useState(false) - - function toggleExpand() { - setIsExpanded((prev) => !prev) - } + const { toggleLanguageSwitcher, isLanguageSwitcherOpen } = useDropdownStore() const urlKeys = Object.keys(urls) as Lang[] return (
- + {languages[currentLanguage]}
    {urlKeys.map((key) => { diff --git a/components/Header/TopMenu/topMenu.module.css b/components/Header/TopMenu/topMenu.module.css index fbdec4a8b..e78f57d92 100644 --- a/components/Header/TopMenu/topMenu.module.css +++ b/components/Header/TopMenu/topMenu.module.css @@ -5,8 +5,7 @@ } .content { - position: relative; - max-width: 89.5rem; + max-width: var(--max-width-navigation); margin: 0 auto; display: flex; justify-content: space-between; diff --git a/components/Header/header.module.css b/components/Header/header.module.css index 7b40be816..1c3360388 100644 --- a/components/Header/header.module.css +++ b/components/Header/header.module.css @@ -1,5 +1,4 @@ .header { - position: relative; font-family: var(--typography-Body-Regular-fontFamily); color: var(--Base-Text-High-contrast); z-index: 10; diff --git a/stores/main-menu.ts b/stores/main-menu.ts index cbd6f1004..fe9fc50f3 100644 --- a/stores/main-menu.ts +++ b/stores/main-menu.ts @@ -1,15 +1,23 @@ import { create } from "zustand" +// TODO: When MyPagesMobileMenu is removed, also remove the +// isMyPagesMobileMenuOpen state and toggleMyPagesMobileMenu function interface DropdownState { isHamburgerMenuOpen: boolean isMyPagesMobileMenuOpen: boolean + isMyPagesMenuOpen: boolean + isLanguageSwitcherOpen: boolean toggleHamburgerMenu: () => void toggleMyPagesMobileMenu: () => void + toggleMyPagesMenu: () => void + toggleLanguageSwitcher: () => void } const useDropdownStore = create((set) => ({ isHamburgerMenuOpen: false, isMyPagesMobileMenuOpen: false, + isMyPagesMenuOpen: false, + isLanguageSwitcherOpen: false, toggleHamburgerMenu: () => set((state) => { // Close the other dropdown if it's open @@ -26,6 +34,22 @@ const useDropdownStore = create((set) => ({ } return { isMyPagesMobileMenuOpen: !state.isMyPagesMobileMenuOpen } }), + toggleMyPagesMenu: () => + set(({ isLanguageSwitcherOpen, isMyPagesMenuOpen }) => { + // Close the other dropdown if it's open + if (!isMyPagesMenuOpen && isLanguageSwitcherOpen) { + set({ isLanguageSwitcherOpen: false }) + } + return { isMyPagesMenuOpen: !isMyPagesMenuOpen } + }), + toggleLanguageSwitcher: () => + set(({ isLanguageSwitcherOpen, isMyPagesMenuOpen }) => { + // Close the other dropdown if it's open + if (!isLanguageSwitcherOpen && isMyPagesMenuOpen) { + set({ isMyPagesMenuOpen: false }) + } + return { isLanguageSwitcherOpen: !isLanguageSwitcherOpen } + }), })) export default useDropdownStore From a2e2cf575e960196ae2b716d15c33d463fd2e066 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Wed, 21 Aug 2024 14:38:29 +0200 Subject: [PATCH 096/319] feat(SW-184): implementing mobile design --- app/globals.css | 1 + .../Header/HeaderLink/headerLink.module.css | 6 + components/Header/HeaderLink/index.tsx | 13 +++ .../MainMenuButton/menuButton.module.css | 1 + .../Header/MainMenu/MobileMenu/index.tsx | 65 +++++++++++ .../MainMenu/MobileMenu/mobileMenu.module.css | 109 ++++++++++++++++++ .../Header/MainMenu/MyPagesMenu/index.tsx | 14 ++- .../MyPagesMenu/myPagesMenu.module.css | 24 +++- .../NavigationMenuItem/index.tsx | 29 +++-- .../navigationMenuItem.module.css | 7 ++ .../NavigationMenuItem/variants.ts | 15 +++ .../Header/MainMenu/NavigationMenu/index.tsx | 12 +- .../NavigationMenu/navigationMenu.module.css | 20 +++- .../MainMenu/NavigationMenu/variants.ts | 15 +++ components/Header/MainMenu/index.tsx | 19 ++- .../Header/MainMenu/mainMenu.module.css | 33 +++++- components/Header/TopMenu/Search/index.tsx | 6 +- .../Header/TopMenu/Search/search.module.css | 11 ++ .../Header/TopMenu/TopMenuButton/index.tsx | 14 --- .../TopMenuButton/topMenuButton.module.css | 9 -- components/Header/TopMenu/index.tsx | 29 +++-- components/Header/TopMenu/topMenu.module.css | 7 ++ components/Header/index.tsx | 12 +- components/Icons/ChevronRight.tsx | 41 +++---- components/Icons/Search.tsx | 2 +- components/Icons/Service.tsx | 36 ++++++ components/Icons/get-icon-by-icon-name.ts | 3 + components/Icons/index.tsx | 1 + .../TopMenu => }/LanguageSwitcher/index.tsx | 11 +- .../languageSwitcher.module.css | 16 ++- i18n/dictionaries/da.json | 2 + i18n/dictionaries/de.json | 2 + i18n/dictionaries/en.json | 2 + i18n/dictionaries/fi.json | 2 + i18n/dictionaries/no.json | 2 + i18n/dictionaries/sv.json | 2 + stores/main-menu.ts | 8 +- types/components/header/headerLink.ts | 3 + types/components/header/mainMenu.ts | 5 + types/components/header/mobileMenu.ts | 8 ++ types/components/header/navigationMenu.ts | 7 +- types/components/header/navigationMenuItem.ts | 7 +- types/components/header/topMenu.ts | 5 + types/components/icon.ts | 1 + 44 files changed, 526 insertions(+), 111 deletions(-) create mode 100644 components/Header/HeaderLink/headerLink.module.css create mode 100644 components/Header/HeaderLink/index.tsx create mode 100644 components/Header/MainMenu/MobileMenu/index.tsx create mode 100644 components/Header/MainMenu/MobileMenu/mobileMenu.module.css create mode 100644 components/Header/MainMenu/NavigationMenu/NavigationMenuItem/variants.ts create mode 100644 components/Header/MainMenu/NavigationMenu/variants.ts delete mode 100644 components/Header/TopMenu/TopMenuButton/index.tsx delete mode 100644 components/Header/TopMenu/TopMenuButton/topMenuButton.module.css create mode 100644 components/Icons/Service.tsx rename components/{Header/TopMenu => }/LanguageSwitcher/index.tsx (93%) rename components/{Header/TopMenu => }/LanguageSwitcher/languageSwitcher.module.css (78%) create mode 100644 types/components/header/headerLink.ts create mode 100644 types/components/header/mainMenu.ts create mode 100644 types/components/header/mobileMenu.ts create mode 100644 types/components/header/topMenu.ts diff --git a/app/globals.css b/app/globals.css index 58a807453..0c84c08ec 100644 --- a/app/globals.css +++ b/app/globals.css @@ -104,6 +104,7 @@ --max-width-text-block: 49.5rem; --mobile-site-header-height: 70.047px; --max-width-navigation: 89.5rem; + --main-menu-mobile-height: 75px; } * { diff --git a/components/Header/HeaderLink/headerLink.module.css b/components/Header/HeaderLink/headerLink.module.css new file mode 100644 index 000000000..7049d4857 --- /dev/null +++ b/components/Header/HeaderLink/headerLink.module.css @@ -0,0 +1,6 @@ +.topLink { + display: flex; + align-items: center; + gap: var(--Spacing-x1); + font-size: var(--typography-Caption-Regular-fontSize); +} diff --git a/components/Header/HeaderLink/index.tsx b/components/Header/HeaderLink/index.tsx new file mode 100644 index 000000000..9b2195ebb --- /dev/null +++ b/components/Header/HeaderLink/index.tsx @@ -0,0 +1,13 @@ +import Link from "@/components/TempDesignSystem/Link" + +import styles from "./headerLink.module.css" + +import { HeaderLinkProps } from "@/types/components/header/headerLink" + +export default function HeaderLink({ children, ...props }: HeaderLinkProps) { + return ( + + {children} + + ) +} diff --git a/components/Header/MainMenu/MainMenuButton/menuButton.module.css b/components/Header/MainMenu/MainMenuButton/menuButton.module.css index 9746f1808..1d999b3ad 100644 --- a/components/Header/MainMenu/MainMenuButton/menuButton.module.css +++ b/components/Header/MainMenu/MainMenuButton/menuButton.module.css @@ -2,6 +2,7 @@ display: flex; gap: var(--Spacing-x1); align-items: center; + width: 100%; background-color: transparent; color: var(--Base-Text-High-contrast); border-width: 0; diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx new file mode 100644 index 000000000..616cd10bb --- /dev/null +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -0,0 +1,65 @@ +"use client" + +import { Dialog, Modal } from "react-aria-components" +import { useIntl } from "react-intl" + +import useDropdownStore from "@/stores/main-menu" + +import { GiftIcon, SearchIcon, ServiceIcon } from "@/components/Icons" +import LanguageSwitcher from "@/components/LanguageSwitcher" + +import HeaderLink from "../../HeaderLink" +import NavigationMenu from "../NavigationMenu" + +import styles from "./mobileMenu.module.css" + +import { MobileMenuProps } from "@/types/components/header/mobileMenu" + +export default function MobileMenu({ + mainNavigation, + languageUrls, +}: MobileMenuProps) { + const intl = useIntl() + const { isHamburgerMenuOpen, toggleHamburgerMenu } = useDropdownStore() + + return ( + <> + + + + +
    + + + {intl.formatMessage({ id: "Find booking" })} + + + + {intl.formatMessage({ id: "Join Scandic Friends" })} + + + + {intl.formatMessage({ id: "Customer service" })} + + +
    +
    +
    + + ) +} diff --git a/components/Header/MainMenu/MobileMenu/mobileMenu.module.css b/components/Header/MainMenu/MobileMenu/mobileMenu.module.css new file mode 100644 index 000000000..a36886e21 --- /dev/null +++ b/components/Header/MainMenu/MobileMenu/mobileMenu.module.css @@ -0,0 +1,109 @@ +@keyframes slide-in { + from { + right: -100vw; + } + + to { + right: 0; + } +} + +.hamburger { + 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.2s; + width: 32px; +} + +.bar::after, +.bar::before { + content: ""; + left: 0; + position: absolute; + transform-origin: 2.286px center; +} + +.bar::after { + top: -8px; +} + +.bar::before { + top: 8px; +} + +.isExpanded .bar { + background: transparent; +} + +.isExpanded .bar::after, +.isExpanded .bar::before { + top: 0; + transform-origin: 50% 50%; + width: 32px; +} + +.isExpanded .bar::after { + transform: rotate(-45deg); +} + +.isExpanded .bar::before { + transform: rotate(45deg); +} + +@media screen and (min-width: 768px) { + .hamburger { + display: none; + } +} + +.overlay { + position: absolute; + top: var(--main-menu-mobile-height); + bottom: 0; + left: 0; + right: 0; +} + +.modal { + position: fixed; + right: auto; + top: var(--main-menu-mobile-height); + bottom: 0; + width: 100%; + background-color: var(--Base-Surface-Primary-light-Normal); +} + +.modal[data-entering] { + animation: slide-in 0.3s; +} +.modal[data-exiting] { + animation: slide-in 0.3s reverse; +} + +.dialog { + height: 100%; + overflow-y: auto; + display: grid; + align-content: space-between; +} + +.footer { + background-color: var(--Base-Surface-Subtle-Normal); + padding: var(--Spacing-x4) var(--Spacing-x2); + display: grid; + gap: var(--Spacing-x2); +} diff --git a/components/Header/MainMenu/MyPagesMenu/index.tsx b/components/Header/MainMenu/MyPagesMenu/index.tsx index 6407cf382..42cd370ab 100644 --- a/components/Header/MainMenu/MyPagesMenu/index.tsx +++ b/components/Header/MainMenu/MyPagesMenu/index.tsx @@ -35,7 +35,9 @@ export default function MyPagesMenu({ navigation, user }: MyPagesMenuProps) {
    - {intl.formatMessage({ id: "Hi" })} {user.firstName}! + + {intl.formatMessage({ id: "Hi" })} {user.firstName}! +
    ) : ( - + - {intl.formatMessage({ id: "Log in/Join" })} + + {intl.formatMessage({ id: "Log in/Join" })} + ) } diff --git a/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css index 459fb6d9f..8d6b2fe1c 100644 --- a/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css +++ b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css @@ -2,11 +2,8 @@ position: relative; } -.button { - font-weight: 600; -} - .chevron { + display: none; transition: transform 0.2s; } @@ -14,6 +11,12 @@ transform: rotate(180deg); } +.userName { + display: none; + font-weight: 600; + color: var(--Base-Text-High-contrast); +} + .dropdown { position: absolute; top: 46px; @@ -80,6 +83,15 @@ opacity: 0; } -.link:hover .arrow { - opacity: 1; +.loginLink { + display: flex; + align-items: center; + gap: var(--Spacing-x1); +} + +@media screen and (min-width: 768px) { + .userName, + .chevron { + display: initial; + } } diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx index 6d00244ff..4efc44086 100644 --- a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx @@ -2,33 +2,46 @@ import { useState } from "react" -import { ChevronDownIcon } from "@/components/Icons" +import { ChevronDownIcon, ChevronRightIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" import MainMenuButton from "../../MainMenuButton" +import { navigationMenuItemVariants } from "./variants" import styles from "./navigationMenuItem.module.css" import { NavigationMenuItemProps } from "@/types/components/header/navigationMenuItem" -export default function MenuItem({ item }: NavigationMenuItemProps) { +export default function MenuItem({ item, variant }: NavigationMenuItemProps) { const { children, title, href, seeAllLinkText, infoCard } = item const [isExpanded, setIsExpanded] = useState(false) + const isMobile = variant === "mobile" function handleButtonClick() { setIsExpanded((prev) => !prev) } return children?.length ? ( - + {title} - + {isMobile ? ( + + ) : ( + + )} ) : ( - + {title} ) diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/navigationMenuItem.module.css b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/navigationMenuItem.module.css index c743f0649..b509f4e73 100644 --- a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/navigationMenuItem.module.css +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/navigationMenuItem.module.css @@ -1,3 +1,10 @@ +.navigationMenuItem.mobile { + display: flex; + justify-content: space-between; + padding: var(--Spacing-x2) 0; + font-size: var(--typography-Subtitle-1-Mobile-fontSize); +} + .chevron { transition: transform 0.2s; } diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/variants.ts b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/variants.ts new file mode 100644 index 000000000..06eead455 --- /dev/null +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/variants.ts @@ -0,0 +1,15 @@ +import { cva } from "class-variance-authority" + +import styles from "./navigationMenuItem.module.css" + +export const navigationMenuItemVariants = cva(styles.navigationMenuItem, { + variants: { + variant: { + default: styles.default, + mobile: styles.mobile, + }, + }, + defaultVariants: { + variant: "default", + }, +}) diff --git a/components/Header/MainMenu/NavigationMenu/index.tsx b/components/Header/MainMenu/NavigationMenu/index.tsx index f5168ab21..08534f56e 100644 --- a/components/Header/MainMenu/NavigationMenu/index.tsx +++ b/components/Header/MainMenu/NavigationMenu/index.tsx @@ -1,15 +1,19 @@ import NavigationMenuItem from "./NavigationMenuItem" +import { navigationMenuVariants } from "./variants" import styles from "./navigationMenu.module.css" import { NavigationMenuProps } from "@/types/components/header/navigationMenu" -export default function NavigationMenu({ items }: NavigationMenuProps) { +export default function NavigationMenu({ + items, + variant, +}: NavigationMenuProps) { return ( -
      +
        {items.map((item) => ( -
      • - +
      • +
      • ))}
      diff --git a/components/Header/MainMenu/NavigationMenu/navigationMenu.module.css b/components/Header/MainMenu/NavigationMenu/navigationMenu.module.css index 066aca610..6c554acde 100644 --- a/components/Header/MainMenu/NavigationMenu/navigationMenu.module.css +++ b/components/Header/MainMenu/NavigationMenu/navigationMenu.module.css @@ -1,8 +1,26 @@ .navigationMenu { list-style: none; margin: 0; - display: flex; justify-content: space-between; align-items: center; gap: var(--Spacing-x4); + display: none; +} + +.navigationMenu.mobile { + display: grid; + width: 100%; + gap: 0; + justify-content: stretch; + padding: var(--Spacing-x-one-and-half) var(--Spacing-x2) var(--Spacing-x2); +} + +.navigationMenu.mobile .item { + border-bottom: 1px solid var(--Base-Border-Subtle); +} + +@media screen and (min-width: 768px) { + .navigationMenu.default { + display: flex; + } } diff --git a/components/Header/MainMenu/NavigationMenu/variants.ts b/components/Header/MainMenu/NavigationMenu/variants.ts new file mode 100644 index 000000000..b1c37d49b --- /dev/null +++ b/components/Header/MainMenu/NavigationMenu/variants.ts @@ -0,0 +1,15 @@ +import { cva } from "class-variance-authority" + +import styles from "./navigationMenu.module.css" + +export const navigationMenuVariants = cva(styles.navigationMenu, { + variants: { + variant: { + default: styles.default, + mobile: styles.mobile, + }, + }, + defaultVariants: { + variant: "default", + }, +}) diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx index 288400a4b..db7d448d2 100644 --- a/components/Header/MainMenu/index.tsx +++ b/components/Header/MainMenu/index.tsx @@ -6,12 +6,15 @@ import Image from "@/components/Image" import { getIntl } from "@/i18n" import { navigationMenuItems } from "../tempHeaderData" +import MobileMenu from "./MobileMenu" import MyPagesMenu from "./MyPagesMenu" import NavigationMenu from "./NavigationMenu" import styles from "./mainMenu.module.css" -export default async function MainMenu() { +import { MainMenuProps } from "@/types/components/header/mainMenu" + +export default async function MainMenu({ languageUrls }: MainMenuProps) { const intl = await getIntl() const myPagesNavigation = await serverClient().contentstack.myPages.navigation.get() @@ -28,13 +31,19 @@ export default async function MainMenu() { data-js="scandiclogoimg" data-nosvgsrc="/_static/img/scandic-logotype.png" itemProp="logo" - height={24} + height={22} src="/_static/img/scandic-logotype.svg" - width={113} + width={103} /> - - +
      + + + +
) diff --git a/components/Header/MainMenu/mainMenu.module.css b/components/Header/MainMenu/mainMenu.module.css index ddcb33627..26c13c8e3 100644 --- a/components/Header/MainMenu/mainMenu.module.css +++ b/components/Header/MainMenu/mainMenu.module.css @@ -7,8 +7,35 @@ .nav { max-width: var(--max-width-navigation); margin: 0 auto; - display: flex; - justify-content: space-between; + display: grid; + grid-template-columns: max-content 1fr; align-items: center; - gap: var(--Spacing-x3); + gap: var(--Spacing-x2); +} + +.menus { + display: flex; + justify-self: end; + align-items: center; + gap: var(--Spacing-x2); +} + +.logoLink { + display: inline-flex; + width: auto; +} + +.logo { + width: 6.4375rem; +} + +@media screen and (min-width: 768px) { + .nav { + display: flex; + justify-content: space-between; + gap: var(--Spacing-x3); + } + .menus { + display: contents; + } } diff --git a/components/Header/TopMenu/Search/index.tsx b/components/Header/TopMenu/Search/index.tsx index f0b3316a7..abd840bc0 100644 --- a/components/Header/TopMenu/Search/index.tsx +++ b/components/Header/TopMenu/Search/index.tsx @@ -4,15 +4,15 @@ import { useIntl } from "react-intl" import { SearchIcon } from "@/components/Icons" -import TopMenuButton from "../TopMenuButton" +import styles from "./search.module.css" export default function Search() { const intl = useIntl() return ( - + ) } diff --git a/components/Header/TopMenu/Search/search.module.css b/components/Header/TopMenu/Search/search.module.css index 004e01d19..45d5bb1b1 100644 --- a/components/Header/TopMenu/Search/search.module.css +++ b/components/Header/TopMenu/Search/search.module.css @@ -1,9 +1,20 @@ .button { background-color: transparent; color: var(--Base-Text-High-contrast); + font-family: var(--typography-Caption-Regular-fontFamily); + font-size: var(--typography-Caption-Regular-fontSize); border-width: 0; + padding: 0; cursor: pointer; display: flex; gap: var(--Spacing-x1); align-items: center; + width: 100%; +} + +@media screen and (min-width: 768px) { + .button { + font-size: var(--typography-Body-Bold-fontSize); + font-family: var(--typography-Body-Bold-fontFamily); + } } diff --git a/components/Header/TopMenu/TopMenuButton/index.tsx b/components/Header/TopMenu/TopMenuButton/index.tsx deleted file mode 100644 index 4e42b05e0..000000000 --- a/components/Header/TopMenu/TopMenuButton/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import styles from "./topMenuButton.module.css" - -import { TopMenuButtonProps } from "@/types/components/header/topMenuButton" - -export default function TopMenuButton({ - children, - ...props -}: TopMenuButtonProps) { - return ( - - ) -} diff --git a/components/Header/TopMenu/TopMenuButton/topMenuButton.module.css b/components/Header/TopMenu/TopMenuButton/topMenuButton.module.css deleted file mode 100644 index 004e01d19..000000000 --- a/components/Header/TopMenu/TopMenuButton/topMenuButton.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.button { - background-color: transparent; - color: var(--Base-Text-High-contrast); - border-width: 0; - cursor: pointer; - display: flex; - gap: var(--Spacing-x1); - align-items: center; -} diff --git a/components/Header/TopMenu/index.tsx b/components/Header/TopMenu/index.tsx index f1555a5bf..b47a6d36d 100644 --- a/components/Header/TopMenu/index.tsx +++ b/components/Header/TopMenu/index.tsx @@ -1,32 +1,29 @@ -import { serverClient } from "@/lib/trpc/server" - -import { GiftIcon } from "@/components/Icons" -import Link from "@/components/TempDesignSystem/Link" +import { GiftIcon, SearchIcon } from "@/components/Icons" +import LanguageSwitcher from "@/components/LanguageSwitcher" import { getIntl } from "@/i18n" -import LanguageSwitcher from "./LanguageSwitcher" -import Search from "./Search" +import HeaderLink from "../HeaderLink" import styles from "./topMenu.module.css" -export default async function TopMenu() { - const intl = await getIntl() - const languages = await serverClient().contentstack.languageSwitcher.get() +import { TopMenuProps } from "@/types/components/header/topMenu" - if (!languages) { - return null - } +export default async function TopMenu({ languageUrls }: TopMenuProps) { + const intl = await getIntl() return (
- + {intl.formatMessage({ id: "Join Scandic Friends" })} - +
- - + + + + {intl.formatMessage({ id: "Find booking" })} +
diff --git a/components/Header/TopMenu/topMenu.module.css b/components/Header/TopMenu/topMenu.module.css index e78f57d92..17a739989 100644 --- a/components/Header/TopMenu/topMenu.module.css +++ b/components/Header/TopMenu/topMenu.module.css @@ -1,4 +1,5 @@ .topMenu { + display: none; background-color: var(--Base-Surface-Subtle-Normal); padding: var(--Spacing-x2) var(--Spacing-x-one-and-half); border-bottom: 1px solid var(--Base-Border-Subtle); @@ -17,3 +18,9 @@ gap: var(--Spacing-x2); align-items: center; } + +@media screen and (min-width: 768px) { + .topMenu { + display: block; + } +} diff --git a/components/Header/index.tsx b/components/Header/index.tsx index d6693a52b..d4e01de0f 100644 --- a/components/Header/index.tsx +++ b/components/Header/index.tsx @@ -1,13 +1,21 @@ +import { serverClient } from "@/lib/trpc/server" + import MainMenu from "./MainMenu" import TopMenu from "./TopMenu" import styles from "./header.module.css" export default async function Header() { + const languages = await serverClient().contentstack.languageSwitcher.get() + + if (!languages) { + return null + } + return (
- - + +
) } diff --git a/components/Icons/ChevronRight.tsx b/components/Icons/ChevronRight.tsx index 1b66b90a3..9930ac095 100644 --- a/components/Icons/ChevronRight.tsx +++ b/components/Icons/ChevronRight.tsx @@ -11,32 +11,29 @@ export default function ChevronRightIcon({ return ( - - - - - - - + + + + + ) diff --git a/components/Icons/Search.tsx b/components/Icons/Search.tsx index 3e91f1f5c..aa9f15e52 100644 --- a/components/Icons/Search.tsx +++ b/components/Icons/Search.tsx @@ -2,7 +2,7 @@ import { iconVariants } from "./variants" import type { IconProps } from "@/types/components/icon" -export default function GiftIcon({ className, color, ...props }: IconProps) { +export default function SearchIcon({ className, color, ...props }: IconProps) { const classNames = iconVariants({ className, color }) return ( + + + + + + + + ) +} diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 3c7a37d7e..79feb4b0c 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -41,6 +41,7 @@ import { RestaurantIcon, SaunaIcon, SearchIcon, + ServiceIcon, TshirtWashIcon, WarningTriangle, WifiIcon, @@ -128,6 +129,8 @@ export function getIconByIconName(icon?: IconName): FC | null { return SaunaIcon case IconName.Search: return SearchIcon + case IconName.Service: + return ServiceIcon case IconName.Tripadvisor: return TripAdvisorIcon case IconName.TshirtWash: diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 0dbb3b628..31abcd5ca 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -39,6 +39,7 @@ export { default as RestaurantIcon } from "./Restaurant" export { default as SaunaIcon } from "./Sauna" export { default as ScandicLogoIcon } from "./ScandicLogo" export { default as SearchIcon } from "./Search" +export { default as ServiceIcon } from "./Service" export { default as TshirtWashIcon } from "./TshirtWash" export { default as WarningTriangle } from "./WarningTriangle" export { default as WifiIcon } from "./Wifi" diff --git a/components/Header/TopMenu/LanguageSwitcher/index.tsx b/components/LanguageSwitcher/index.tsx similarity index 93% rename from components/Header/TopMenu/LanguageSwitcher/index.tsx rename to components/LanguageSwitcher/index.tsx index 638a58dba..e5ab88c5e 100644 --- a/components/Header/TopMenu/LanguageSwitcher/index.tsx +++ b/components/LanguageSwitcher/index.tsx @@ -8,8 +8,6 @@ import useDropdownStore from "@/stores/main-menu" import { CheckIcon, ChevronDownIcon, GlobeIcon } from "@/components/Icons" import useLang from "@/hooks/useLang" -import TopMenuButton from "../TopMenuButton" - import styles from "./languageSwitcher.module.css" import { LanguageSwitcherProps } from "@/types/components/current/languageSwitcher" @@ -22,7 +20,11 @@ export default function LanguageSwitcher({ urls }: LanguageSwitcherProps) { return (
- + +
diff --git a/components/Header/TopMenu/LanguageSwitcher/languageSwitcher.module.css b/components/LanguageSwitcher/languageSwitcher.module.css similarity index 78% rename from components/Header/TopMenu/LanguageSwitcher/languageSwitcher.module.css rename to components/LanguageSwitcher/languageSwitcher.module.css index ff2301963..1eeeab77b 100644 --- a/components/Header/TopMenu/LanguageSwitcher/languageSwitcher.module.css +++ b/components/LanguageSwitcher/languageSwitcher.module.css @@ -5,14 +5,20 @@ .button { background-color: transparent; color: var(--Base-Text-High-contrast); + font-family: var(--typography-Caption-Regular-fontFamily); + font-size: var(--typography-Caption-Regular-fontSize); border-width: 0; + padding: 0; cursor: pointer; - display: flex; + display: grid; + grid-template-columns: repeat(2, max-content) 1fr; gap: var(--Spacing-x1); align-items: center; + width: 100%; } .chevron { + justify-self: end; transition: transform 0.2s; } @@ -78,3 +84,11 @@ .link:hover { font-weight: 600; } + +@media screen and (min-width: 768px) { + .button { + grid-template-columns: repeat(3, max-content); + font-size: var(--typography-Body-Bold-fontSize); + font-family: var(--typography-Body-Bold-fontFamily); + } +} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index f4553b9ce..43b927104 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -44,6 +44,7 @@ "Country code": "Landekode", "Credit card deleted successfully": "Kreditkort blev slettet", "Current password": "Nuværende kodeord", + "Customer service": "Kundeservice", "Date of Birth": "Fødselsdato", "Day": "Dag", "Description": "Beskrivelse", @@ -94,6 +95,7 @@ "Members": "Medlemmer", "Membership cards": "Medlemskort", "Membership ID": "Medlems-id", + "Menu": "Menu", "Modify": "Ændre", "Month": "Måned", "My communication preferences": "Mine kommunikationspræferencer", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index b80e56960..8814ff721 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -43,6 +43,7 @@ "Country code": "Landesvorwahl", "Credit card deleted successfully": "Kreditkarte erfolgreich gelöscht", "Current password": "Aktuelles Passwort", + "Customer service": "Kundendienst", "Date of Birth": "Geburtsdatum", "Day": "Tag", "Description": "Beschreibung", @@ -92,6 +93,7 @@ "Members": "Mitglieder", "Membership cards": "Mitgliedskarten", "Membership ID": "Mitglieds-ID", + "Menu": "Menu", "Modify": "Ändern", "Month": "Monat", "My communication preferences": "Meine Kommunikationseinstellungen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index f86bab933..ea205dd05 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -44,6 +44,7 @@ "Country code": "Country code", "Credit card deleted successfully": "Credit card deleted successfully", "Current password": "Current password", + "Customer service": "Customer service", "Date of Birth": "Date of Birth", "Day": "Day", "Description": "Description", @@ -99,6 +100,7 @@ "Members": "Members", "Membership cards": "Membership cards", "Membership ID": "Membership ID", + "Menu": "Menu", "Modify": "Modify", "Month": "Month", "My communication preferences": "My communication preferences", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index a3dd9cff8..5781dea73 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -44,6 +44,7 @@ "Country code": "Maatunnus", "Credit card deleted successfully": "Luottokortti poistettu onnistuneesti", "Current password": "Nykyinen salasana", + "Customer service": "Asiakaspalvelu", "Date of Birth": "Syntymäaika", "Day": "Päivä", "Description": "Kuvaus", @@ -93,6 +94,7 @@ "Members": "Jäsenet", "Membership cards": "Jäsenkortit", "Membership ID": "Jäsentunnus", + "Menu": "Menu", "Modify": "Muokkaa", "Month": "Kuukausi", "My communication preferences": "Viestintämieltymykseni", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 8dbf2cb2c..0f38a89b0 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -42,6 +42,7 @@ "Could not find requested resource": "Kunne ikke finne den forespurte ressursen", "Country": "Land", "Country code": "Landskode", + "Customer service": "Kundeservice", "Credit card deleted successfully": "Kredittkort slettet", "Current password": "Nåværende passord", "Date of Birth": "Fødselsdato", @@ -94,6 +95,7 @@ "Members": "Medlemmer", "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", + "Menu": "Menu", "Modify": "Endre", "Month": "Måned", "My communication preferences": "Mine kommunikasjonspreferanser", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 25eb5acf5..53f38241e 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -44,6 +44,7 @@ "Country code": "Landskod", "Credit card deleted successfully": "Kreditkort har tagits bort", "Current password": "Nuvarande lösenord", + "Customer service": "Kundservice", "Date of Birth": "Födelsedatum", "Day": "Dag", "Description": "Beskrivning", @@ -96,6 +97,7 @@ "Members": "Medlemmar", "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", + "Menu": "Meny", "Modify": "Ändra", "Month": "Månad", "My communication preferences": "Mina kommunikationspreferenser", diff --git a/stores/main-menu.ts b/stores/main-menu.ts index fe9fc50f3..008cce9b7 100644 --- a/stores/main-menu.ts +++ b/stores/main-menu.ts @@ -19,13 +19,7 @@ const useDropdownStore = create((set) => ({ isMyPagesMenuOpen: false, isLanguageSwitcherOpen: false, toggleHamburgerMenu: () => - set((state) => { - // Close the other dropdown if it's open - if (!state.isHamburgerMenuOpen && state.isMyPagesMobileMenuOpen) { - set({ isMyPagesMobileMenuOpen: false }) - } - return { isHamburgerMenuOpen: !state.isHamburgerMenuOpen } - }), + set((state) => ({ isHamburgerMenuOpen: !state.isHamburgerMenuOpen })), toggleMyPagesMobileMenu: () => set((state) => { // Close the other dropdown if it's open diff --git a/types/components/header/headerLink.ts b/types/components/header/headerLink.ts new file mode 100644 index 000000000..671ab6d0e --- /dev/null +++ b/types/components/header/headerLink.ts @@ -0,0 +1,3 @@ +import { LinkProps } from "@/components/TempDesignSystem/Link/link" + +export interface HeaderLinkProps extends React.PropsWithChildren {} diff --git a/types/components/header/mainMenu.ts b/types/components/header/mainMenu.ts new file mode 100644 index 000000000..a867be31b --- /dev/null +++ b/types/components/header/mainMenu.ts @@ -0,0 +1,5 @@ +import { LanguageSwitcherData } from "@/types/requests/languageSwitcher" + +export interface MainMenuProps { + languageUrls: LanguageSwitcherData +} diff --git a/types/components/header/mobileMenu.ts b/types/components/header/mobileMenu.ts new file mode 100644 index 000000000..79f11a563 --- /dev/null +++ b/types/components/header/mobileMenu.ts @@ -0,0 +1,8 @@ +import { MainNavigationItem } from "./mainNavigationItem" + +import { LanguageSwitcherData } from "@/types/requests/languageSwitcher" + +export interface MobileMenuProps { + languageUrls: LanguageSwitcherData + mainNavigation: MainNavigationItem[] +} diff --git a/types/components/header/navigationMenu.ts b/types/components/header/navigationMenu.ts index f429d557d..11642b11f 100644 --- a/types/components/header/navigationMenu.ts +++ b/types/components/header/navigationMenu.ts @@ -1,5 +1,10 @@ +import { VariantProps } from "class-variance-authority" + +import { navigationMenuVariants } from "@/components/Header/MainMenu/NavigationMenu/variants" + import { MainNavigationItem } from "@/types/components/header/mainNavigationItem" -export interface NavigationMenuProps { +export interface NavigationMenuProps + extends VariantProps { items: MainNavigationItem[] } diff --git a/types/components/header/navigationMenuItem.ts b/types/components/header/navigationMenuItem.ts index d53c2313a..942b706c8 100644 --- a/types/components/header/navigationMenuItem.ts +++ b/types/components/header/navigationMenuItem.ts @@ -1,5 +1,10 @@ +import { VariantProps } from "class-variance-authority" + +import { navigationMenuItemVariants } from "@/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/variants" + import { MainNavigationItem } from "@/types/components/header/mainNavigationItem" -export interface NavigationMenuItemProps { +export interface NavigationMenuItemProps + extends VariantProps { item: MainNavigationItem } diff --git a/types/components/header/topMenu.ts b/types/components/header/topMenu.ts new file mode 100644 index 000000000..4e20a6e7f --- /dev/null +++ b/types/components/header/topMenu.ts @@ -0,0 +1,5 @@ +import { LanguageSwitcherData } from "@/types/requests/languageSwitcher" + +export interface TopMenuProps { + languageUrls: LanguageSwitcherData +} diff --git a/types/components/icon.ts b/types/components/icon.ts index ed0d3da3b..0e705b7e6 100644 --- a/types/components/icon.ts +++ b/types/components/icon.ts @@ -46,6 +46,7 @@ export enum IconName { Restaurant = "Restaurant", Sauna = "Sauna", Search = "Search", + Service = "Service", Tripadvisor = "Tripadvisor", TshirtWash = "TshirtWash", Wifi = "Wifi", From 7ef7b4a54431181bdc28a1b2ad6e1974c169d2aa Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 22 Aug 2024 14:21:24 +0200 Subject: [PATCH 097/319] feat(SW-184): language switcher mobile/desktop functionality --- .../Header/MainMenu/MobileMenu/index.tsx | 13 +- .../MainMenu/MobileMenu/mobileMenu.module.css | 3 +- components/Icons/Check.tsx | 22 ++-- components/Icons/ChevronLeft.tsx | 40 ++++++ components/Icons/get-icon-by-icon-name.ts | 3 + components/Icons/index.tsx | 1 + components/LanguageSwitcher/index.tsx | 88 +++++++++---- .../languageSwitcher.module.css | 121 ++++++++++++++---- hooks/useHandleKeyPress.ts | 12 +- hooks/useHandleKeyUp.ts | 12 ++ hooks/useTrapFocus.ts | 82 ++++++++++++ stores/main-menu.ts | 27 +++- types/components/current/languageSwitcher.ts | 3 +- types/components/icon.ts | 1 + utils/tabbable.ts | 62 +++++++++ 15 files changed, 407 insertions(+), 83 deletions(-) create mode 100644 components/Icons/ChevronLeft.tsx create mode 100644 hooks/useHandleKeyUp.ts create mode 100644 hooks/useTrapFocus.ts create mode 100644 utils/tabbable.ts diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index 616cd10bb..d9e63fc5b 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -7,6 +7,7 @@ import useDropdownStore from "@/stores/main-menu" import { GiftIcon, SearchIcon, ServiceIcon } from "@/components/Icons" import LanguageSwitcher from "@/components/LanguageSwitcher" +import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import HeaderLink from "../../HeaderLink" import NavigationMenu from "../NavigationMenu" @@ -22,6 +23,12 @@ export default function MobileMenu({ const intl = useIntl() const { isHamburgerMenuOpen, toggleHamburgerMenu } = useDropdownStore() + useHandleKeyUp((event: KeyboardEvent) => { + if (event.key === "Escape" && isHamburgerMenuOpen) { + toggleHamburgerMenu() + } + }) + return ( <> - + - + - + diff --git a/components/Icons/ChevronLeft.tsx b/components/Icons/ChevronLeft.tsx new file mode 100644 index 000000000..eb14d07dd --- /dev/null +++ b/components/Icons/ChevronLeft.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ChevronLeftIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 79feb4b0c..d7508b83a 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -15,6 +15,7 @@ import { CheckCircleIcon, CheckIcon, ChevronDownIcon, + ChevronLeftIcon, ChevronRightIcon, CloseIcon, CloseLarge, @@ -75,6 +76,8 @@ export function getIconByIconName(icon?: IconName): FC | null { return CheckCircleIcon case IconName.ChevronDown: return ChevronDownIcon + case IconName.ChevronLeft: + return ChevronLeftIcon case IconName.ChevronRight: return ChevronRightIcon case IconName.Close: diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 31abcd5ca..9ed34a098 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -9,6 +9,7 @@ export { default as CellphoneIcon } from "./Cellphone" export { default as CheckIcon } from "./Check" export { default as CheckCircleIcon } from "./CheckCircle" export { default as ChevronDownIcon } from "./ChevronDown" +export { default as ChevronLeftIcon } from "./ChevronLeft" export { default as ChevronRightIcon } from "./ChevronRight" export { default as CloseIcon } from "./Close" export { default as CloseLarge } from "./CloseLarge" diff --git a/components/LanguageSwitcher/index.tsx b/components/LanguageSwitcher/index.tsx index e5ab88c5e..229920f01 100644 --- a/components/LanguageSwitcher/index.tsx +++ b/components/LanguageSwitcher/index.tsx @@ -1,28 +1,54 @@ "use client" import Link from "next/link" +import { useIntl } from "react-intl" import { Lang, languages } from "@/constants/languages" import useDropdownStore from "@/stores/main-menu" -import { CheckIcon, ChevronDownIcon, GlobeIcon } from "@/components/Icons" +import { + CheckIcon, + ChevronDownIcon, + ChevronLeftIcon, + GlobeIcon, +} from "@/components/Icons" +import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import useLang from "@/hooks/useLang" +import { useTrapFocus } from "@/hooks/useTrapFocus" + +import Subtitle from "../TempDesignSystem/Text/Subtitle" import styles from "./languageSwitcher.module.css" import { LanguageSwitcherProps } from "@/types/components/current/languageSwitcher" -export default function LanguageSwitcher({ urls }: LanguageSwitcherProps) { +export default function LanguageSwitcher({ + urls, + location = "header", +}: LanguageSwitcherProps) { + const intl = useIntl() + const languageSwitcherRef = useTrapFocus() const currentLanguage = useLang() const { toggleLanguageSwitcher, isLanguageSwitcherOpen } = useDropdownStore() const urlKeys = Object.keys(urls) as Lang[] + useHandleKeyUp((event: KeyboardEvent) => { + if (event.key === "Escape" && isLanguageSwitcherOpen) { + toggleLanguageSwitcher() + } + }) + return ( -
+
+
+ +
+ + {intl.formatMessage({ id: "Select your language" })} + +
    + {urlKeys.map((key) => { + const url = urls[key]?.url + const isActive = currentLanguage === key + if (url) { + return ( +
  • + + {languages[key]} + {isActive ? : null} + +
  • + ) + } + })} +
+
) diff --git a/components/LanguageSwitcher/languageSwitcher.module.css b/components/LanguageSwitcher/languageSwitcher.module.css index 1eeeab77b..9bdf4f188 100644 --- a/components/LanguageSwitcher/languageSwitcher.module.css +++ b/components/LanguageSwitcher/languageSwitcher.module.css @@ -1,5 +1,11 @@ -.languageSwitcher { - position: relative; +@keyframes slide-in { + from { + right: -100vw; + } + + to { + right: 0; + } } .button { @@ -27,33 +33,50 @@ } .dropdown { - position: absolute; - top: 2.25rem; - right: 0; + position: fixed; + top: var(--main-menu-mobile-height); + right: -100vw; + bottom: 0; + width: 100%; background-color: var(--Base-Surface-Primary-light-Normal); - padding: var(--Spacing-x2) var(--Spacing-x3); - border-radius: var(--Corner-radius-Large); - box-shadow: 0px 0px 14px 6px #0000001a; - display: none; - min-width: 12.5rem; - z-index: 1; -} - -/* Triangle above dropdown */ -.dropdown::before { - content: ""; - position: absolute; - top: -1.25rem; - right: 2.4rem; - transform: rotate(180deg); - border-width: 0.75rem; - border-style: solid; - border-color: var(--Base-Surface-Primary-light-Normal) transparent transparent - transparent; + transition: right 0.3s; } .dropdown.isExpanded { display: block; + right: 0; +} + +.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); + padding: var(--Spacing-x3) var(--Spacing-x2); +} + +.subtitle { + font-family: var(--typography-Subtitle-2-fontFamily); + font-size: var(--typography-Subtitle-2-Mobile-fontSize); + font-weight: var(--typography-Subtitle-2-fontWeight); + color: var(--Base-Text-High-contrast, #4d001b); } .list { @@ -73,22 +96,64 @@ justify-content: space-between; align-items: center; text-decoration: none; -} - -.link:hover { - background-color: var(--Base-Surface-Primary-light-Hover-alt); border-radius: var(--Corner-radius-Medium); } .link.active, .link:hover { + background-color: var(--Base-Surface-Primary-light-Hover-alt); font-weight: 600; } @media screen and (min-width: 768px) { + .languageSwitcher { + position: relative; + } + + .backWrapper { + display: none; + } + + .languageWrapper { + padding: var(--Spacing-x2) var(--Spacing-x3); + } + + .subtitle { + display: none; + } + + .dropdown { + position: absolute; + top: 2.25rem; + background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Large); + box-shadow: 0px 0px 14px 6px #0000001a; + display: none; + min-width: 12.5rem; + z-index: 1; + bottom: auto; + } + + /* Triangle above dropdown */ + .dropdown::before { + content: ""; + position: absolute; + top: -1.25rem; + right: 2.4rem; + transform: rotate(180deg); + border-width: 0.75rem; + border-style: solid; + border-color: var(--Base-Surface-Primary-light-Normal) transparent + transparent transparent; + } + .button { grid-template-columns: repeat(3, max-content); font-size: var(--typography-Body-Bold-fontSize); font-family: var(--typography-Body-Bold-fontFamily); } + + .link.active:not(:hover) { + background-color: transparent; + } } diff --git a/hooks/useHandleKeyPress.ts b/hooks/useHandleKeyPress.ts index d9fdc0ea2..b240650d1 100644 --- a/hooks/useHandleKeyPress.ts +++ b/hooks/useHandleKeyPress.ts @@ -1,11 +1,11 @@ "use client" -import { useEffect } from 'react'; +import { useEffect } from "react" export function useHandleKeyPress(callback: (event: KeyboardEvent) => void) { useEffect(() => { - window.addEventListener('keydown', callback); + window.addEventListener("keydown", callback) return () => { - window.removeEventListener('keydown', callback); - }; - }, [callback]); -} \ No newline at end of file + window.removeEventListener("keydown", callback) + } + }, [callback]) +} diff --git a/hooks/useHandleKeyUp.ts b/hooks/useHandleKeyUp.ts new file mode 100644 index 000000000..b44f82d5a --- /dev/null +++ b/hooks/useHandleKeyUp.ts @@ -0,0 +1,12 @@ +"use client" + +import { useEffect } from "react" + +export function useHandleKeyUp(callback: (event: KeyboardEvent) => void) { + useEffect(() => { + window.addEventListener("keyup", callback) + return () => { + window.removeEventListener("keyup", callback) + } + }, [callback]) +} diff --git a/hooks/useTrapFocus.ts b/hooks/useTrapFocus.ts new file mode 100644 index 000000000..7d2e214c7 --- /dev/null +++ b/hooks/useTrapFocus.ts @@ -0,0 +1,82 @@ +"use client" + +import { useCallback, useEffect, useRef, useState } from "react" + +import { useHandleKeyPress } from "@/hooks/useHandleKeyPress" +import findTabbableDescendants from "@/utils/tabbable" + +const TAB_KEY = "Tab" +const optionsDefault = { focusOnRender: true, returnFocus: true } +type OptionsType = { + focusOnRender?: boolean + returnFocus?: boolean +} +export function useTrapFocus(opts?: OptionsType) { + const options = opts ? { ...optionsDefault, ...opts } : optionsDefault + const ref = useRef(null) + const previouseFocusedElement = useRef( + document.activeElement as HTMLElement + ) + const [tabbableElements, setTabbableElements] = useState([]) + // Handle initial focus of the referenced element, and return focus to previously focused element on cleanup + // and find all the tabbable elements in the referenced element + + useEffect(() => { + const { current } = ref + if (current) { + const focusableChildNodes = findTabbableDescendants(current) + if (options.focusOnRender) { + current.focus() + } + + setTabbableElements(focusableChildNodes) + } + return () => { + const { current } = previouseFocusedElement + if (current instanceof HTMLElement && options.returnFocus) { + current.focus() + } + } + }, [options.focusOnRender, options.returnFocus, ref, setTabbableElements]) + + const handleUserKeyPress = useCallback( + (event: KeyboardEvent) => { + const { code, shiftKey } = event + const first = tabbableElements[0] + const last = tabbableElements[tabbableElements.length - 1] + const currentActiveElement = document.activeElement + // Scope current tabs to current root element + if (isWithinCurrentElementScope([...tabbableElements, ref.current])) { + if (code === TAB_KEY) { + if ( + currentActiveElement === first || + currentActiveElement === ref.current + ) { + // move focus to last element if shift+tab while currently focusing the first tabbable element + if (shiftKey) { + event.preventDefault() + last.focus() + } + } + if (currentActiveElement === last) { + // move focus back to first if tabbing while currently focusing the last tabbable element + if (!shiftKey) { + event.preventDefault() + first.focus() + } + } + } + } + }, + [ref, tabbableElements] + ) + useHandleKeyPress(handleUserKeyPress) + + return ref +} +function isWithinCurrentElementScope( + elementList: (HTMLInputElement | Element | null)[] +) { + const currentActiveElement = document.activeElement + return elementList.includes(currentActiveElement) +} diff --git a/stores/main-menu.ts b/stores/main-menu.ts index 008cce9b7..1cfcd3039 100644 --- a/stores/main-menu.ts +++ b/stores/main-menu.ts @@ -19,7 +19,15 @@ const useDropdownStore = create((set) => ({ isMyPagesMenuOpen: false, isLanguageSwitcherOpen: false, toggleHamburgerMenu: () => - set((state) => ({ isHamburgerMenuOpen: !state.isHamburgerMenuOpen })), + set(({ isHamburgerMenuOpen, isMyPagesMenuOpen }) => { + // Close the other dropdown if it's open + if (!isHamburgerMenuOpen && isMyPagesMenuOpen) { + set({ isMyPagesMenuOpen: false }) + } + return { isHamburgerMenuOpen: !isHamburgerMenuOpen } + }), + // toggleHamburgerMenu: () => + // set((state) => ({ isHamburgerMenuOpen: !state.isHamburgerMenuOpen })), toggleMyPagesMobileMenu: () => set((state) => { // Close the other dropdown if it's open @@ -29,13 +37,18 @@ const useDropdownStore = create((set) => ({ return { isMyPagesMobileMenuOpen: !state.isMyPagesMobileMenuOpen } }), toggleMyPagesMenu: () => - set(({ isLanguageSwitcherOpen, isMyPagesMenuOpen }) => { - // Close the other dropdown if it's open - if (!isMyPagesMenuOpen && isLanguageSwitcherOpen) { - set({ isLanguageSwitcherOpen: false }) + set( + ({ isHamburgerMenuOpen, isLanguageSwitcherOpen, isMyPagesMenuOpen }) => { + // Close the other dropdown if it's open + if (!isMyPagesMenuOpen && isLanguageSwitcherOpen) { + set({ isLanguageSwitcherOpen: false }) + } + if (!isMyPagesMenuOpen && isHamburgerMenuOpen) { + set({ isHamburgerMenuOpen: false }) + } + return { isMyPagesMenuOpen: !isMyPagesMenuOpen } } - return { isMyPagesMenuOpen: !isMyPagesMenuOpen } - }), + ), toggleLanguageSwitcher: () => set(({ isLanguageSwitcherOpen, isMyPagesMenuOpen }) => { // Close the other dropdown if it's open diff --git a/types/components/current/languageSwitcher.ts b/types/components/current/languageSwitcher.ts index 0f409d36e..dd39494a9 100644 --- a/types/components/current/languageSwitcher.ts +++ b/types/components/current/languageSwitcher.ts @@ -1,5 +1,3 @@ -import { Lang } from "@/constants/languages" - import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" export type LanguageSwitcherLink = { @@ -9,4 +7,5 @@ export type LanguageSwitcherLink = { export type LanguageSwitcherProps = { urls: LanguageSwitcherData + location?: "header" | "footer" } diff --git a/types/components/icon.ts b/types/components/icon.ts index 0e705b7e6..b0f6359d8 100644 --- a/types/components/icon.ts +++ b/types/components/icon.ts @@ -19,6 +19,7 @@ export enum IconName { CrossCircle = "CrossCircle", CheckCircle = "CheckCircle", ChevronDown = "ChevronDown", + ChevronLeft = "ChevronLeft", ChevronRight = "ChevronRight", Close = "Close", CloseLarge = "CloseLarge", diff --git a/utils/tabbable.ts b/utils/tabbable.ts new file mode 100644 index 000000000..b38352c99 --- /dev/null +++ b/utils/tabbable.ts @@ -0,0 +1,62 @@ +/*! + * Adapted from jQuery UI core + * + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/category/ui-core/ + */ + +const tabbableNode = /input|select|textarea|button|object/ + +function hidesContents(element: HTMLElement) { + const zeroSize = element.offsetWidth <= 0 && element.offsetHeight <= 0 + + // If the node is empty, this is good enough + if (zeroSize && !element.innerHTML) return true + + // Otherwise we need to check some styles + const style = window.getComputedStyle(element) + return zeroSize + ? style.getPropertyValue("overflow") !== "visible" + : style.getPropertyValue("display") === "none" +} + +function visible(element: any) { + let parentElement = element + while (parentElement) { + if (parentElement === document.body) break + if (hidesContents(parentElement)) return false + parentElement = parentElement.parentNode + } + return true +} + +export function focusable(element: HTMLElement, isTabIndexNotNaN: boolean) { + const nodeName = element.nodeName.toLowerCase() + const res = + //@ts-ignore + (tabbableNode.test(nodeName) && !element.disabled) || + //@ts-ignore + (nodeName === "a" ? element.href || isTabIndexNotNaN : isTabIndexNotNaN) + return res && visible(element) +} + +export function tabbable(element: HTMLElement) { + let tabIndex = element.getAttribute("tabindex") + //@ts-ignore + if (tabIndex === null) tabIndex = undefined + //@ts-ignore + const isTabIndexNaN = isNaN(tabIndex) + //@ts-ignore + return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN) +} + +export default function findTabbableDescendants( + element: HTMLElement +): HTMLElement[] { + return [].slice.call(element.querySelectorAll("*"), 0).filter(tabbable) +} From bdec054ecd90a809242f58fd00aae284d3116417 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Fri, 23 Aug 2024 13:25:41 +0200 Subject: [PATCH 098/319] feat(SW-184): my pages menu mobile/desktop functionality --- .../Avatar/avatar.module.css | 0 .../{MyPagesMenu => }/Avatar/index.tsx | 0 .../MainMenu/MobileMenu/mobileMenu.module.css | 20 +-- .../Header/MainMenu/MyPagesMenu/index.tsx | 99 +++----------- .../MyPagesMenu/myPagesMenu.module.css | 128 ++++++------------ .../MainMenu/MyPagesMenuContent/index.tsx | 81 +++++++++++ .../myPagesMenuContent.module.css | 74 ++++++++++ .../MainMenu/MyPagesMobileMenu/index.tsx | 55 ++++++++ .../myPagesMobileMenu.module.css | 38 ++++++ components/Header/MainMenu/index.tsx | 30 +++- .../Header/MainMenu/mainMenu.module.css | 6 + components/LanguageSwitcher/index.tsx | 25 ++-- .../languageSwitcher.module.css | 3 +- hooks/useTrapFocus.ts | 1 + i18n/dictionaries/da.json | 4 + i18n/dictionaries/de.json | 4 + i18n/dictionaries/en.json | 4 + i18n/dictionaries/fi.json | 4 + i18n/dictionaries/no.json | 4 + i18n/dictionaries/sv.json | 4 + stores/main-menu.ts | 95 +++++++++---- types/components/header/myPagesMenu.ts | 6 +- 22 files changed, 459 insertions(+), 226 deletions(-) rename components/Header/MainMenu/{MyPagesMenu => }/Avatar/avatar.module.css (100%) rename components/Header/MainMenu/{MyPagesMenu => }/Avatar/index.tsx (100%) create mode 100644 components/Header/MainMenu/MyPagesMenuContent/index.tsx create mode 100644 components/Header/MainMenu/MyPagesMenuContent/myPagesMenuContent.module.css create mode 100644 components/Header/MainMenu/MyPagesMobileMenu/index.tsx create mode 100644 components/Header/MainMenu/MyPagesMobileMenu/myPagesMobileMenu.module.css diff --git a/components/Header/MainMenu/MyPagesMenu/Avatar/avatar.module.css b/components/Header/MainMenu/Avatar/avatar.module.css similarity index 100% rename from components/Header/MainMenu/MyPagesMenu/Avatar/avatar.module.css rename to components/Header/MainMenu/Avatar/avatar.module.css diff --git a/components/Header/MainMenu/MyPagesMenu/Avatar/index.tsx b/components/Header/MainMenu/Avatar/index.tsx similarity index 100% rename from components/Header/MainMenu/MyPagesMenu/Avatar/index.tsx rename to components/Header/MainMenu/Avatar/index.tsx diff --git a/components/Header/MainMenu/MobileMenu/mobileMenu.module.css b/components/Header/MainMenu/MobileMenu/mobileMenu.module.css index 1e8fad253..da62ccfff 100644 --- a/components/Header/MainMenu/MobileMenu/mobileMenu.module.css +++ b/components/Header/MainMenu/MobileMenu/mobileMenu.module.css @@ -64,20 +64,6 @@ transform: rotate(45deg); } -@media screen and (min-width: 768px) { - .hamburger { - display: none; - } -} - -.overlay { - position: absolute; - top: var(--main-menu-mobile-height); - bottom: 0; - left: 0; - right: 0; -} - .modal { position: fixed; top: var(--main-menu-mobile-height); @@ -108,3 +94,9 @@ display: grid; gap: var(--Spacing-x2); } + +@media screen and (min-width: 768px) { + .hamburger { + display: none; + } +} diff --git a/components/Header/MainMenu/MyPagesMenu/index.tsx b/components/Header/MainMenu/MyPagesMenu/index.tsx index 42cd370ab..02d30cad7 100644 --- a/components/Header/MainMenu/MyPagesMenu/index.tsx +++ b/components/Header/MainMenu/MyPagesMenu/index.tsx @@ -2,113 +2,54 @@ import { useIntl } from "react-intl" -import { logout } from "@/constants/routes/handleAuth" -import { myPages } from "@/constants/routes/myPages" import useDropdownStore from "@/stores/main-menu" -import { ArrowRightIcon, ChevronDownIcon } from "@/components/Icons" -import Link from "@/components/TempDesignSystem/Link" +import { ChevronDownIcon } from "@/components/Icons" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import useLang from "@/hooks/useLang" import { getInitials } from "@/utils/user" +import Avatar from "../Avatar" import MainMenuButton from "../MainMenuButton" -import Avatar from "./Avatar" +import MyPagesMenuContent from "../MyPagesMenuContent" import styles from "./myPagesMenu.module.css" import { MyPagesMenuProps } from "@/types/components/header/myPagesMenu" -// This component is mostly the same as MyPagesMobileDropdown, but with a -// different name and some different styles. Should probably be refactored in -// a later stage to fit the design from Figma better. - export default function MyPagesMenu({ navigation, user }: MyPagesMenuProps) { const intl = useIntl() const lang = useLang() const { toggleMyPagesMenu, isMyPagesMenuOpen } = useDropdownStore() - if (!navigation) { - return null - } + useHandleKeyUp((event: KeyboardEvent) => { + if (event.key === "Escape" && isMyPagesMenuOpen) { + toggleMyPagesMenu() + } + }) - return user ? ( + return (
- + {intl.formatMessage({ id: "Hi" })} {user.firstName}! - + - + +
- ) : ( - - - - {intl.formatMessage({ id: "Log in/Join" })} - - ) } diff --git a/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css index 8d6b2fe1c..d322955e3 100644 --- a/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css +++ b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css @@ -1,97 +1,47 @@ .myPagesMenu { - position: relative; -} - -.chevron { display: none; - transition: transform 0.2s; -} - -.chevron.isExpanded { - transform: rotate(180deg); -} - -.userName { - display: none; - font-weight: 600; - color: var(--Base-Text-High-contrast); -} - -.dropdown { - position: absolute; - top: 46px; - right: 0; - background-color: var(--Base-Surface-Primary-light-Normal); - padding: var(--Spacing-x2) var(--Spacing-x4); - border-radius: var(--Corner-radius-Large); - box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); - min-width: 20rem; - z-index: 1; - display: none; -} - -/* Triangle above dropdown */ -.dropdown::before { - content: ""; - position: absolute; - top: -1.25rem; - right: 2.4rem; - transform: rotate(180deg); - border-width: 0.75rem; - border-style: solid; - border-color: var(--Base-Surface-Primary-light-Normal) transparent transparent - transparent; -} - -.dropdown.isExpanded { - display: block; -} - -.friendTypeWrapper { - padding: 0 var(--Spacing-x1) var(--Spacing-x2); - font-weight: 400; - color: var(--UI-Text-Medium-contrast); -} -.friendType { - font-family: var(--typography-Title-5-fontFamily); - letter-spacing: var(--typography-Title-5-letterSpacing); - font-size: var(--typography-Caption-Bold-fontSize); - text-transform: uppercase; -} - -.friendType::after { - content: " · "; - display: inline; - padding: 0 var(--Spacing-x-half); -} - -.groups, -.menuItems { - list-style: none; -} - -.group { - padding: var(--Spacing-x2) 0; - border-top: 1px solid var(--Base-Border-Subtle); -} - -.group:last-child { - padding-bottom: 0; -} - -.arrow { - opacity: 0; -} - -.loginLink { - display: flex; - align-items: center; - gap: var(--Spacing-x1); } @media screen and (min-width: 768px) { - .userName, + .myPagesMenu { + display: block; + position: relative; + } + .chevron { - display: initial; + transition: transform 0.2s; + } + + .chevron.isExpanded { + transform: rotate(180deg); + } + + .dropdown { + position: absolute; + top: 46px; + right: 0; + background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Large); + box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); + min-width: 20rem; + z-index: 1; + display: none; + } + + /* Triangle above dropdown */ + .dropdown::before { + content: ""; + position: absolute; + top: -1.25rem; + right: 2.4rem; + transform: rotate(180deg); + border-width: 0.75rem; + border-style: solid; + border-color: var(--Base-Surface-Primary-light-Normal) transparent + transparent transparent; + } + + .dropdown.isExpanded { + display: block; } } diff --git a/components/Header/MainMenu/MyPagesMenuContent/index.tsx b/components/Header/MainMenu/MyPagesMenuContent/index.tsx new file mode 100644 index 000000000..e87c25a94 --- /dev/null +++ b/components/Header/MainMenu/MyPagesMenuContent/index.tsx @@ -0,0 +1,81 @@ +"use client" + +import Link from "next/link" +import { useIntl } from "react-intl" + +import { logout } from "@/constants/routes/handleAuth" + +import { ArrowRightIcon } from "@/components/Icons" +import Divider from "@/components/TempDesignSystem/Divider" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import useLang from "@/hooks/useLang" +import { useTrapFocus } from "@/hooks/useTrapFocus" + +import styles from "./myPagesMenuContent.module.css" + +import { MyPagesMenuContentProps } from "@/types/components/header/myPagesMenu" + +export default function MyPagesMenuContent({ + navigation, + toggleOpenStateFn, + user, +}: MyPagesMenuContentProps) { + const intl = useIntl() + const lang = useLang() + const myPagesMenuContentRef = useTrapFocus() + + if (!navigation) { + return null + } + + return ( + + ) +} diff --git a/components/Header/MainMenu/MyPagesMenuContent/myPagesMenuContent.module.css b/components/Header/MainMenu/MyPagesMenuContent/myPagesMenuContent.module.css new file mode 100644 index 000000000..800f646b5 --- /dev/null +++ b/components/Header/MainMenu/MyPagesMenuContent/myPagesMenuContent.module.css @@ -0,0 +1,74 @@ +.myPagesMenuContent { + padding: var(--Spacing-x3) var(--Spacing-x2); +} + +.intro { + padding: 0 var(--Spacing-x1); +} + +.myPagesMenuContent .friendTypeWrapper { + color: var(--UI-Text-Medium-contrast); +} + +.divider { + margin: var(--Spacing-x2) 0; +} + +.friendType { + font-family: var(--typography-Title-5-fontFamily); + letter-spacing: var(--typography-Title-5-letterSpacing); + font-size: var(--typography-Caption-Bold-fontSize); + text-transform: uppercase; +} + +.friendType::after { + content: " · "; + display: inline; + padding: 0 var(--Spacing-x-half); +} + +.groups, +.menuItems { + list-style: none; +} + +.link { + display: flex; + align-items: center; + justify-content: space-between; + text-decoration: none; + padding: var(--Spacing-x1); + gap: var(--Spacing-x-one-and-half); + color: var(--Scandic-Brand-Burgundy); + font-family: var(--typography-Body-Bold-fontFamily); + font-size: var(--typography-Body-Bold-fontSize); + font-weight: 500; + line-height: var(--typography-Body-Bold-lineHeight); + letter-spacing: var(--typography-Body-Bold-letterSpacing); + border-radius: var(--Corner-radius-Medium); +} + +.link:hover { + background-color: var(--Base-Surface-Primary-light-Hover-alt); +} + +.link.smallLink { + font-family: var(--typography-Body-Regular-fontFamily); + font-size: var(--typography-Body-Regular-fontSize); + font-weight: var(--typography-Body-Regular-fontWeight); + line-height: var(--typography-Body-Regular-lineHeight); + letter-spacing: var(--typography-Body-Regular-letterSpacing); +} + +.link:not(:hover) .arrow { + opacity: 0; +} + +@media screen and (min-width: 768px) { + .myPagesMenuContent { + padding: var(--Spacing-x2) var(--Spacing-x4); + } + .userName { + display: none; + } +} diff --git a/components/Header/MainMenu/MyPagesMobileMenu/index.tsx b/components/Header/MainMenu/MyPagesMobileMenu/index.tsx new file mode 100644 index 000000000..2a9f5dac7 --- /dev/null +++ b/components/Header/MainMenu/MyPagesMobileMenu/index.tsx @@ -0,0 +1,55 @@ +"use client" + +import { Dialog, Modal } from "react-aria-components" +import { useIntl } from "react-intl" + +import useDropdownStore from "@/stores/main-menu" + +import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" +import { getInitials } from "@/utils/user" + +import Avatar from "../Avatar" +import MainMenuButton from "../MainMenuButton" +import MyPagesMenuContent from "../MyPagesMenuContent" + +import styles from "./myPagesMobileMenu.module.css" + +import { MyPagesMenuProps } from "@/types/components/header/myPagesMenu" + +export default function MyPagesMobileMenu({ + navigation, + user, +}: MyPagesMenuProps) { + const intl = useIntl() + const { toggleMyPagesMobileMenu, isMyPagesMobileMenuOpen } = + useDropdownStore() + + useHandleKeyUp((event: KeyboardEvent) => { + if (event.key === "Escape" && isMyPagesMobileMenuOpen) { + toggleMyPagesMobileMenu() + } + }) + + return ( +
+ + + + + + + + +
+ ) +} diff --git a/components/Header/MainMenu/MyPagesMobileMenu/myPagesMobileMenu.module.css b/components/Header/MainMenu/MyPagesMobileMenu/myPagesMobileMenu.module.css new file mode 100644 index 000000000..ba3a687c4 --- /dev/null +++ b/components/Header/MainMenu/MyPagesMobileMenu/myPagesMobileMenu.module.css @@ -0,0 +1,38 @@ +@keyframes slide-in { + from { + right: -100vw; + } + + to { + right: 0; + } +} + +.modal { + position: fixed; + top: var(--main-menu-mobile-height); + right: auto; + bottom: 0; + width: 100%; + background-color: var(--Base-Surface-Primary-light-Normal); + transition: right 0.3s; +} + +.modal[data-entering] { + animation: slide-in 0.3s; +} +.modal[data-exiting] { + animation: slide-in 0.3s reverse; +} + +.dialog { + height: 100%; + overflow-y: auto; +} + +@media screen and (min-width: 767px) { + .myPagesMobileMenu, + .modal { + display: none; + } +} diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx index db7d448d2..3ce22c53a 100644 --- a/components/Header/MainMenu/index.tsx +++ b/components/Header/MainMenu/index.tsx @@ -1,13 +1,18 @@ -import Link from "next/link" +import NextLink from "next/link" +import { myPages } from "@/constants/routes/myPages" import { serverClient } from "@/lib/trpc/server" import Image from "@/components/Image" +import Link from "@/components/TempDesignSystem/Link" import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" import { navigationMenuItems } from "../tempHeaderData" +import Avatar from "./Avatar" import MobileMenu from "./MobileMenu" import MyPagesMenu from "./MyPagesMenu" +import MyPagesMobileMenu from "./MyPagesMobileMenu" import NavigationMenu from "./NavigationMenu" import styles from "./mainMenu.module.css" @@ -16,6 +21,7 @@ import { MainMenuProps } from "@/types/components/header/mainMenu" export default async function MainMenu({ languageUrls }: MainMenuProps) { const intl = await getIntl() + const lang = getLang() const myPagesNavigation = await serverClient().contentstack.myPages.navigation.get() @@ -24,7 +30,7 @@ export default async function MainMenu({ languageUrls }: MainMenuProps) { return (
) diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 3db576744..e07444eb0 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -77,6 +77,7 @@ "image header" "image hotel" "image prices"; + grid-template-columns: 518px; overflow: hidden; width: 1050px; padding: 0; diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 77d038105..f79681015 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -1,3 +1,5 @@ +import { serverClient } from "@/lib/trpc/server" + import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { ChevronRightIcon, @@ -13,15 +15,28 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" import styles from "./hotelCard.module.css" import { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps" -export default async function HotelCard({ hotel }: HotelCardProps) { +export default async function HotelCard({ + checkInDate, + checkOutDate, + hotelId, + price, +}: HotelCardProps) { const intl = await getIntl() - const sortedAmenities = hotel.detailedFacilities + const hotelData = await serverClient().hotel.get({ + hotelId: hotelId.toString(), + language: getLang(), + }) + + if (!hotelData) return null + + const sortedAmenities = hotelData.hotel.detailedFacilities .sort((a, b) => b.sortOrder - a.sortOrder) .slice(0, 5) @@ -29,8 +44,8 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
{hotel.hotelContent.images.metaData.altText} - {hotel.ratings?.tripAdvisor.rating} + {hotelData.hotel.ratings?.tripAdvisor.rating}
- {hotel.name} + {hotelData.hotel.name} - {`${hotel.address.streetAddress}, ${hotel.address.city}`} + {`${hotelData.hotel.address.streetAddress}, ${hotelData.hotel.address.city}`} - {`${hotel.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} + {`${hotelData.hotel.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
- {sortedAmenities.map((facility) => { + {sortedAmenities?.map((facility) => { const IconComponent = mapFacilityToIcon(facility.name) return (
@@ -77,7 +92,9 @@ export default async function HotelCard({ hotel }: HotelCardProps) { {intl.formatMessage({ id: "Public price from" })} - 2820 SEK / night + + {price?.regularAmount} SEK / night + approx 280 eur
@@ -85,7 +102,9 @@ export default async function HotelCard({ hotel }: HotelCardProps) { {intl.formatMessage({ id: "Member price from" })} - 2820 SEK / night + + {price?.memberAmount} SEK / night + approx 280 eur
- {filterAvailability?.length ? ( + {filterAvailability.length ? ( filterAvailability.map((hotel) => ( b.sortOrder - a.sortOrder) .slice(0, 5) @@ -44,8 +46,8 @@ export default async function HotelCard({
{hotelData.hotel.hotelContent.images.metaData.altText} - {hotelData.hotel.ratings?.tripAdvisor.rating} + {hotel.ratings?.tripAdvisor.rating}
- {hotelData.hotel.name} + {hotel.name} - {`${hotelData.hotel.address.streetAddress}, ${hotelData.hotel.address.city}`} + {`${hotel.address.streetAddress}, ${hotel.address.city}`} - {`${hotelData.hotel.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} + {`${hotel.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
From fa6108d718cb95faf4409b82a61ab1ec11c7ffda Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 30 Aug 2024 11:50:37 +0200 Subject: [PATCH 141/319] feat(SW-176): use temp data for hotel --- components/HotelReservation/HotelCard/index.tsx | 11 +++-------- server/routers/hotels/query.ts | 3 ++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 8139b774c..10aa2b9dc 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -1,4 +1,5 @@ import { serverClient } from "@/lib/trpc/server" +import tempHotelData from "@/server/routers/hotels/tempHotelData.json" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { @@ -29,14 +30,8 @@ export default async function HotelCard({ }: HotelCardProps) { const intl = await getIntl() - const hotelResponse = await serverClient().hotel.get({ - hotelId: hotelId.toString(), - language: getLang(), - }) - - if (!hotelResponse) return null - - const { hotel } = hotelResponse + // TODO: Use real endpoint. + const hotel = tempHotelData.data.attributes const sortedAmenities = hotel.detailedFacilities .sort((a, b) => b.sortOrder - a.sortOrder) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 289d0c96d..fade6fcde 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -8,12 +8,13 @@ import { notFound, serverErrorByStatus, } from "@/server/errors/trpc" +import { extractHotelImages } from "@/server/routers/utils/hotels" import { contentStackUidWithServiceProcedure, publicProcedure, router, + serviceProcedure, } from "@/server/trpc" -import { extractHotelImages } from "@/server/routers/utils/hotels" import { toApiLang } from "@/server/utils" import { From 493ceb5d727d7abbdb789a89602f2e96a7608489 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 30 Aug 2024 11:59:34 +0200 Subject: [PATCH 142/319] feat(SW-176): remove null check --- components/HotelReservation/HotelCard/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 10aa2b9dc..b118ca656 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -68,7 +68,7 @@ export default async function HotelCard({
- {sortedAmenities?.map((facility) => { + {sortedAmenities.map((facility) => { const IconComponent = mapFacilityToIcon(facility.name) return (
From e26a2aec85331ec91a0f2c2ee6a84d3a006c0adf Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 30 Aug 2024 13:53:12 +0200 Subject: [PATCH 143/319] feat(SW-176): add enum --- .../(public)/hotelreservation/[section]/page.tsx | 3 ++- .../(public)/hotelreservation/select-hotel/page.tsx | 11 +++++++---- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 2 +- i18n/dictionaries/en.json | 1 + i18n/dictionaries/sv.json | 2 +- server/routers/hotels/output.ts | 2 +- .../hotelReservation/selectHotel/selectHotel.ts | 4 ++++ 8 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 types/components/hotelReservation/selectHotel/selectHotel.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx index ecd62644b..45ca7d308 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx @@ -107,7 +107,8 @@ export default async function SectionsPage({ return (
-
Hotel Card TBI
+ {/* TODO: Add Hotel Listing Card */} +
Hotel Listing Card TBI
diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx index 56302d36b..24b53a6b9 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx @@ -11,6 +11,7 @@ import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" +import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { LangParams, PageArgs } from "@/types/params" export default async function SelectHotelPage({ @@ -35,10 +36,12 @@ export default async function SelectHotelPage({ const { availability } = availabilityResponse - const filterAvailability = availability.data - .filter((hotels) => hotels.attributes.status === "Available") + const availableHotels = availability.data + .filter((hotels) => hotels.attributes.status === AvailabilityEnum.Available) .flatMap((hotels) => hotels.attributes) + console.log(availableHotels) + return (
@@ -56,8 +59,8 @@ export default async function SelectHotelPage({
- {filterAvailability.length ? ( - filterAvailability.map((hotel) => ( + {availableHotels.length ? ( + availableHotels.map((hotel) => ( Date: Fri, 30 Aug 2024 13:59:03 +0200 Subject: [PATCH 144/319] feat(SW-176): add comment --- .../(live)/(public)/hotelreservation/select-hotel/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx index 24b53a6b9..ebd516ce7 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx @@ -70,6 +70,7 @@ export default async function SelectHotelPage({ /> )) ) : ( + // TODO: handle no hotels found No hotels found )}
From 87de0435983c58349e25fac17a16e53e70b7e859 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 2 Sep 2024 10:39:07 +0200 Subject: [PATCH 145/319] feat(SW-176): add optional inputs --- server/routers/hotels/query.ts | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index fade6fcde..05d72c5db 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -223,17 +223,35 @@ export const hotelQueryRouter = router({ get: serviceProcedure .input(getAvailabilityInputSchema) .query(async ({ input, ctx }) => { - const { cityId, roomStayStartDate, roomStayEndDate, adults } = input - const params: Record = { + const { + cityId, roomStayStartDate, roomStayEndDate, adults, + children, + promotionCode, + reservationProfileType, + attachedProfileId, + } = input + // TODO: remove undefined type from params + const params: Record = { + roomStayStartDate, + roomStayEndDate, + adults, + children, + promotionCode, + reservationProfileType, + attachedProfileId, } availabilityCounter.add(1, { cityId, roomStayStartDate, roomStayEndDate, adults, + children, + promotionCode, + reservationProfileType, + attachedProfileId, }) console.info( "api.hotels.availability start", @@ -256,6 +274,10 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, + children, + promotionCode, + reservationProfileType, + attachedProfileId, error_type: "http_error", error: JSON.stringify({ status: apiResponse.status, @@ -285,6 +307,10 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, + children, + promotionCode, + reservationProfileType, + attachedProfileId, error_type: "validation_error", error: JSON.stringify(validateAvailabilityData.error), }) @@ -302,6 +328,10 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, + children, + promotionCode, + reservationProfileType, + attachedProfileId, }) console.info( JSON.stringify({ From a3e540baa697f73382bb0711e3d3cee3160067a6 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Tue, 3 Sep 2024 15:18:38 +0200 Subject: [PATCH 146/319] feat(SW-176): filter in route --- .../hotelreservation/select-hotel/page.tsx | 15 +++------ .../HotelReservation/HotelCard/index.tsx | 6 ++-- server/routers/hotels/output.ts | 4 +-- server/routers/hotels/query.ts | 33 ++++++------------- 4 files changed, 18 insertions(+), 40 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx index ebd516ce7..5144cc9f5 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx @@ -11,7 +11,6 @@ import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" -import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { LangParams, PageArgs } from "@/types/params" export default async function SelectHotelPage({ @@ -25,20 +24,14 @@ export default async function SelectHotelPage({ hotelId: "879", }) - const availabilityResponse = await serverClient().hotel.availability.get({ + const availableHotels = await serverClient().hotel.availability.get({ cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54", roomStayStartDate: "2024-11-02", roomStayEndDate: "2024-11-03", adults: 1, }) - if (!availabilityResponse) return null - - const { availability } = availabilityResponse - - const availableHotels = availability.data - .filter((hotels) => hotels.attributes.status === AvailabilityEnum.Available) - .flatMap((hotels) => hotels.attributes) + if (!availableHotels) return null console.log(availableHotels) @@ -59,8 +52,8 @@ export default async function SelectHotelPage({
- {availableHotels.length ? ( - availableHotels.map((hotel) => ( + {availableHotels.availability.length ? ( + availableHotels.availability.map((hotel) => ( - {price?.regularAmount} SEK / night + {price?.regularAmount} SEK / {intl.formatMessage({ id: "night" })} approx 280 eur @@ -100,7 +98,7 @@ export default async function HotelCard({ {intl.formatMessage({ id: "Member price from" })} - {price?.memberAmount} SEK / night + {price?.memberAmount} SEK / {intl.formatMessage({ id: "night" })} approx 280 eur diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 7f059aefa..a652db503 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -470,7 +470,7 @@ export const getHotelDataSchema = z.object({ const occupancySchema = z.object({ adults: z.number(), - children: z.number(), + children: z.number().optional(), }) const bestPricePerStaySchema = z.object({ @@ -512,7 +512,7 @@ const availabilitySchema = z.object({ attributes: z.object({ checkInDate: z.string(), checkOutDate: z.string(), - occupancy: occupancySchema.optional(), + occupancy: occupancySchema, status: z.string(), hotelId: z.number(), ratePlanSet: z.string().optional(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 05d72c5db..f6bd0e098 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -37,6 +37,8 @@ import { import tempFilterData from "./tempFilterData.json" import tempRatesData from "./tempRatesData.json" +import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" + const meter = metrics.getMeter("trpc.hotels") const getHotelCounter = meter.createCounter("trpc.hotel.get") const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success") @@ -234,24 +236,16 @@ export const hotelQueryRouter = router({ attachedProfileId, } = input // TODO: remove undefined type from params - const params: Record = { + const params: Record = { roomStayStartDate, roomStayEndDate, adults, - children, - promotionCode, - reservationProfileType, - attachedProfileId, } availabilityCounter.add(1, { cityId, roomStayStartDate, roomStayEndDate, adults, - children, - promotionCode, - reservationProfileType, - attachedProfileId, }) console.info( "api.hotels.availability start", @@ -260,7 +254,6 @@ export const hotelQueryRouter = router({ const apiResponse = await api.get( `${api.endpoints.v0.availability}/${cityId}`, { - cache: "no-store", headers: { Authorization: `Bearer ${ctx.serviceToken}`, }, @@ -274,10 +267,6 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, - children, - promotionCode, - reservationProfileType, - attachedProfileId, error_type: "http_error", error: JSON.stringify({ status: apiResponse.status, @@ -307,10 +296,6 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, - children, - promotionCode, - reservationProfileType, - attachedProfileId, error_type: "validation_error", error: JSON.stringify(validateAvailabilityData.error), }) @@ -328,18 +313,20 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, - children, - promotionCode, - reservationProfileType, - attachedProfileId, }) console.info( + "api.hotels.availability success", JSON.stringify({ query: { cityId, params: params }, }) ) return { - availability: validateAvailabilityData.data, + availability: validateAvailabilityData.data.data + .filter( + (hotels) => + hotels.attributes.status === AvailabilityEnum.Available + ) + .flatMap((hotels) => hotels.attributes), } }), }), From 70460e5aba09ec02a99040c347cab0e3610f9c91 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Tue, 3 Sep 2024 17:12:45 +0200 Subject: [PATCH 147/319] feat(SW-176): add default values --- server/routers/hotels/input.ts | 8 ++++---- server/routers/hotels/output.ts | 4 ++-- server/routers/hotels/query.ts | 19 ++++++++++++++++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 2e35d0f1d..562a0e608 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -11,10 +11,10 @@ export const getAvailabilityInputSchema = z.object({ roomStayStartDate: z.string(), roomStayEndDate: z.string(), adults: z.number(), - children: z.number().optional(), - promotionCode: z.string().optional(), - reservationProfileType: z.string().optional(), - attachedProfileId: z.string().optional(), + children: z.number().optional().default(0), + promotionCode: z.string().optional().default(""), + reservationProfileType: z.string().optional().default(""), + attachedProfileId: z.string().optional().default(""), }) export const getRatesInputSchema = z.object({ diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index a652db503..7f059aefa 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -470,7 +470,7 @@ export const getHotelDataSchema = z.object({ const occupancySchema = z.object({ adults: z.number(), - children: z.number().optional(), + children: z.number(), }) const bestPricePerStaySchema = z.object({ @@ -512,7 +512,7 @@ const availabilitySchema = z.object({ attributes: z.object({ checkInDate: z.string(), checkOutDate: z.string(), - occupancy: occupancySchema, + occupancy: occupancySchema.optional(), status: z.string(), hotelId: z.number(), ratePlanSet: z.string().optional(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index f6bd0e098..731b08306 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -235,17 +235,25 @@ export const hotelQueryRouter = router({ reservationProfileType, attachedProfileId, } = input - // TODO: remove undefined type from params + const params: Record = { roomStayStartDate, roomStayEndDate, adults, + children, + promotionCode, + reservationProfileType, + attachedProfileId, } + availabilityCounter.add(1, { cityId, roomStayStartDate, roomStayEndDate, adults, + children, + promotionCode, + reservationProfileType, }) console.info( "api.hotels.availability start", @@ -267,6 +275,9 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, + children, + promotionCode, + reservationProfileType, error_type: "http_error", error: JSON.stringify({ status: apiResponse.status, @@ -296,6 +307,9 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, + children, + promotionCode, + reservationProfileType, error_type: "validation_error", error: JSON.stringify(validateAvailabilityData.error), }) @@ -313,6 +327,9 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, + children, + promotionCode, + reservationProfileType, }) console.info( "api.hotels.availability success", From 2bab73c3b9a69739ae1b1e3283abd4f94fa3b4b9 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Tue, 3 Sep 2024 17:15:51 +0200 Subject: [PATCH 148/319] feat(SW-176): remove clg --- .../(live)/(public)/hotelreservation/select-hotel/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx index 5144cc9f5..6b505b06a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx @@ -33,8 +33,6 @@ export default async function SelectHotelPage({ if (!availableHotels) return null - console.log(availableHotels) - return (
From 60de4d16b988a3e083562a487cded21e25095391 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 4 Sep 2024 09:09:46 +0200 Subject: [PATCH 149/319] feat(SW-176): remove fixed width on grid column --- components/HotelReservation/HotelCard/hotelCard.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index e07444eb0..3db576744 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -77,7 +77,6 @@ "image header" "image hotel" "image prices"; - grid-template-columns: 518px; overflow: hidden; width: 1050px; padding: 0; From 3966cf59e77c950556e692f97977c8c3bbda9e50 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 4 Sep 2024 09:37:47 +0200 Subject: [PATCH 150/319] feat(SW-176): use currency input --- components/HotelReservation/HotelCard/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index f5ac8b8f1..43cf56a03 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -88,7 +88,8 @@ export default async function HotelCard({ {intl.formatMessage({ id: "Public price from" })} - {price?.regularAmount} SEK / {intl.formatMessage({ id: "night" })} + {price?.regularAmount} {price?.currency} / + {intl.formatMessage({ id: "night" })} approx 280 eur @@ -98,7 +99,8 @@ export default async function HotelCard({ {intl.formatMessage({ id: "Member price from" })} - {price?.memberAmount} SEK / {intl.formatMessage({ id: "night" })} + {price?.memberAmount} {price?.currency} / + {intl.formatMessage({ id: "night" })} approx 280 eur From 412f27acaaffa9782f1ce5307ca3845f0243625e Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 4 Sep 2024 09:50:25 +0200 Subject: [PATCH 151/319] feat(SW-176): sort translations --- i18n/dictionaries/da.json | 3 +-- i18n/dictionaries/de.json | 4 ++-- i18n/dictionaries/en.json | 5 ++--- i18n/dictionaries/fi.json | 4 ++-- i18n/dictionaries/no.json | 4 ++-- i18n/dictionaries/sv.json | 4 ++-- 6 files changed, 11 insertions(+), 13 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 8814f14f4..9767775e9 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -92,12 +92,12 @@ "Log out": "Log ud", "Manage preferences": "Administrer præferencer", "Meetings & Conferences": "Møder & Konferencer", - "Membership ID copied to clipboard": "Medlems-ID kopieret til udklipsholder", "Member price": "Medlemspris", "Member price from": "Medlemspris fra", "Members": "Medlemmer", "Membership cards": "Medlemskort", "Membership ID": "Medlems-id", + "Membership ID copied to clipboard": "Medlems-ID kopieret til udklipsholder", "Menu": "Menu", "Modify": "Ændre", "Month": "Måned", @@ -138,7 +138,6 @@ "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", "points": "Point", "Points": "Point", - "points": "point", "Points being calculated": "Point udregnes", "Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021", "Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index ed1606748..a969473ec 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -90,12 +90,12 @@ "Log in/Join": "Log in/Anmelden", "Log out": "Ausloggen", "Manage preferences": "Verwalten von Voreinstellungen", - "Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert", "Member price": "Mitgliederpreis", "Member price from": "Mitgliederpreis ab", "Members": "Mitglieder", "Membership cards": "Mitgliedskarten", "Membership ID": "Mitglieds-ID", + "Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert", "Menu": "Menu", "Modify": "Ändern", "Month": "Monat", @@ -133,8 +133,8 @@ "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", - "Points": "Punkte", "points": "Punkte", + "Points": "Punkte", "Points being calculated": "Punkte werden berechnet", "Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021", "Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index afb41aba3..685c2989c 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -97,12 +97,12 @@ "Log out": "Log out", "Manage preferences": "Manage preferences", "Meetings & Conferences": "Meetings & Conferences", - "Membership ID copied to clipboard": "Membership ID copied to clipboard", "Member price": "Member price", "Member price from": "Member price from", "Members": "Members", "Membership cards": "Membership cards", "Membership ID": "Membership ID", + "Membership ID copied to clipboard": "Membership ID copied to clipboard", "Menu": "Menu", "Modify": "Modify", "Month": "Month", @@ -141,9 +141,8 @@ "Phone is required": "Phone is required", "Phone number": "Phone number", "Please enter a valid phone number": "Please enter a valid phone number", - "points": "Points", "Points": "Points", - "points": "points", + "points": "Points", "Points being calculated": "Points being calculated", "Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021", "Points may take up to 10 days to be displayed.": "Points may take up to 10 days to be displayed.", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index b54da3963..64fb63f7b 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -91,12 +91,12 @@ "Log out": "Kirjaudu ulos", "Manage preferences": "Asetusten hallinta", "Meetings & Conferences": "Kokoukset & Konferenssit", - "Membership ID copied to clipboard": "Jäsenyystunnus kopioitu leikepöydälle", "Member price": "Jäsenhinta", "Member price from": "Jäsenhinta alkaen", "Members": "Jäsenet", "Membership cards": "Jäsenkortit", "Membership ID": "Jäsentunnus", + "Membership ID copied to clipboard": "Jäsenyystunnus kopioitu leikepöydälle", "Menu": "Valikko", "Modify": "Muokkaa", "Month": "Kuukausi", @@ -135,8 +135,8 @@ "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", - "Points": "Pisteet", "points": "pistettä", + "Points": "Pisteet", "Points being calculated": "Pisteitä lasketaan", "Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021", "Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 57ea705a0..09f967a84 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -92,12 +92,12 @@ "Log out": "Logg ut", "Manage preferences": "Administrer preferanser", "Meetings & Conferences": "Møter & Konferanser", - "Membership ID copied to clipboard": "Medlems-ID kopiert til utklippstavlen", "Member price": "Medlemspris", "Member price from": "Medlemspris fra", "Members": "Medlemmer", "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", + "Membership ID copied to clipboard": "Medlems-ID kopiert til utklippstavlen", "Menu": "Menu", "Modify": "Endre", "Month": "Måned", @@ -136,8 +136,8 @@ "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", - "points": "poeng", "Points": "Poeng", + "points": "poeng", "Points being calculated": "Poeng beregnes", "Points earned prior to May 1, 2021": "Opptjente poeng før 1. mai 2021", "Points may take up to 10 days to be displayed.": "Det kan ta opptil 10 dager før poeng vises.", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 41257a282..f6c23d5a2 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -94,12 +94,12 @@ "Log out": "Logga ut", "Manage preferences": "Hantera inställningar", "Meetings & Conferences": "Möten & Konferenser", - "Membership ID copied to clipboard": "Medlems-ID kopierat till urklipp", "Member price": "Medlemspris", "Member price from": "Medlemspris från", "Members": "Medlemmar", "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", + "Membership ID copied to clipboard": "Medlems-ID kopierat till urklipp", "Menu": "Meny", "Modify": "Ändra", "Month": "Månad", @@ -138,8 +138,8 @@ "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", - "Points": "Poäng", "points": "poäng", + "Points": "Poäng", "Points being calculated": "Poäng beräknas", "Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021", "Points may take up to 10 days to be displayed.": "Det kan ta upp till 10 dagar innan poäng visas.", From 957cd77d59e3a4e8063d74c63478fe92aacf9f5f Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 4 Sep 2024 14:04:07 +0200 Subject: [PATCH 152/319] fix: update package-lock to reflect new commit of design system --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 14db5a10c..1362c73c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5516,7 +5516,7 @@ }, "node_modules/@scandic-hotels/design-system": { "version": "0.1.0", - "resolved": "git+https://x-token-auth:ATCTT3xFfGN0gu4BSBWR71ifMM-_iAT2ip_jnjF0OjTkYhEB3sn71fPCGuMUA7O3BxJ2oHptZVGAlVvMUoeo3Wfute7RYido9HlvrVjemqns9hR3WSf6eNHhsSy5bLtxQ6VK7mnSSAGHaCqTejxirs_PmOB_jPIi1Ft4OEDehtnMxCteg8rO-IE%3D27DF8E0B@bitbucket.org/scandic-swap/design-system.git#a24a425525c021cebb7d1ff6126400aa21ca749f", + "resolved": "git+https://x-token-auth:ATCTT3xFfGN0gu4BSBWR71ifMM-_iAT2ip_jnjF0OjTkYhEB3sn71fPCGuMUA7O3BxJ2oHptZVGAlVvMUoeo3Wfute7RYido9HlvrVjemqns9hR3WSf6eNHhsSy5bLtxQ6VK7mnSSAGHaCqTejxirs_PmOB_jPIi1Ft4OEDehtnMxCteg8rO-IE%3D27DF8E0B@bitbucket.org/scandic-swap/design-system.git#3279c13e03b53a9c49fc770313e289a92a6481b5", "peerDependencies": { "react": "^18.2.0", "react-aria-components": "^1.0.1", From a3cb24dfd476229220d8158230f9a49595859ea7 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 4 Sep 2024 14:07:57 +0200 Subject: [PATCH 153/319] feat(SW-187): App badges localization --- .../Footer/Navigation/SecondaryNav/index.tsx | 10 +++++-- components/Footer/Navigation/index.tsx | 2 ++ components/Footer/index.tsx | 1 - .../Fragments/Footer/Refs/MainLinks.graphql | 4 +++ .../Footer/Refs/SecondaryLinks.graphql | 4 +++ .../Fragments/Footer/SecondaryLinks.graphql | 16 +++++++--- public/_static/img/app-store-badge-de.svg | 19 ++++++++++++ public/_static/img/app-store-badge-dk.svg | 17 +++++++++++ ...store-badge.svg => app-store-badge-en.svg} | 0 public/_static/img/app-store-badge-fi.svg | 17 +++++++++++ public/_static/img/app-store-badge-no.svg | 22 ++++++++++++++ public/_static/img/app-store-badge-se.svg | 18 +++++++++++ public/_static/img/google-play-badge-de.svg | 24 +++++++++++++++ public/_static/img/google-play-badge-dk.svg | 22 ++++++++++++++ ...lay-badge.svg => google-play-badge-en.svg} | 0 public/_static/img/google-play-badge-fi.svg | 22 ++++++++++++++ public/_static/img/google-play-badge-no.svg | 27 +++++++++++++++++ public/_static/img/google-play-badge-se.svg | 23 ++++++++++++++ server/routers/contentstack/base/query.ts | 30 +++++++++++++------ server/routers/contentstack/base/utils.ts | 26 ++++++++++++---- types/components/footer/navigation.ts | 14 +++++++-- 21 files changed, 293 insertions(+), 25 deletions(-) create mode 100644 public/_static/img/app-store-badge-de.svg create mode 100644 public/_static/img/app-store-badge-dk.svg rename public/_static/img/{app-store-badge.svg => app-store-badge-en.svg} (100%) create mode 100644 public/_static/img/app-store-badge-fi.svg create mode 100644 public/_static/img/app-store-badge-no.svg create mode 100644 public/_static/img/app-store-badge-se.svg create mode 100644 public/_static/img/google-play-badge-de.svg create mode 100644 public/_static/img/google-play-badge-dk.svg rename public/_static/img/{google-play-badge.svg => google-play-badge-en.svg} (100%) create mode 100644 public/_static/img/google-play-badge-fi.svg create mode 100644 public/_static/img/google-play-badge-no.svg create mode 100644 public/_static/img/google-play-badge-se.svg diff --git a/components/Footer/Navigation/SecondaryNav/index.tsx b/components/Footer/Navigation/SecondaryNav/index.tsx index c3497ef21..2588a24fd 100644 --- a/components/Footer/Navigation/SecondaryNav/index.tsx +++ b/components/Footer/Navigation/SecondaryNav/index.tsx @@ -1,6 +1,7 @@ import Image from "@/components/Image" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" +import { getLang } from "@/i18n/serverContext" import styles from "./secondarynav.module.css" @@ -13,7 +14,8 @@ export default function FooterSecondaryNav({ secondaryLinks, appDownloads, }: FooterSecondaryNavProps) { - console.log("secondaryLinks", secondaryLinks[0].links) + const lang = getLang() + console.log("hej", JSON.stringify(secondaryLinks, null, 4)) return (
- { - // This will be changed to the new LangueSwitcher that is done in the header branch, when implementing contentstack - }
diff --git a/components/Footer/Navigation/SecondaryNav/index.tsx b/components/Footer/Navigation/SecondaryNav/index.tsx index 2588a24fd..1524ffbb6 100644 --- a/components/Footer/Navigation/SecondaryNav/index.tsx +++ b/components/Footer/Navigation/SecondaryNav/index.tsx @@ -5,46 +5,49 @@ import { getLang } from "@/i18n/serverContext" import styles from "./secondarynav.module.css" -import { - AppDownLoadLinks, - type FooterSecondaryNavProps, -} from "@/types/components/footer/navigation" +import { AppDownLoadLinks } from "@/types/components/footer/appDownloadIcons" +import { type FooterSecondaryNavProps } from "@/types/components/footer/navigation" export default function FooterSecondaryNav({ secondaryLinks, appDownloads, }: FooterSecondaryNavProps) { const lang = getLang() - console.log("hej", JSON.stringify(secondaryLinks, null, 4)) + return (
- + {appDownloads && ( + + )} {secondaryLinks.map((link) => ( - + {languageUrls && ( + + )}
diff --git a/components/Footer/Navigation/MainNav/mainnav.module.css b/components/Footer/Navigation/MainNav/mainnav.module.css index 496fa9618..933c9e774 100644 --- a/components/Footer/Navigation/MainNav/mainnav.module.css +++ b/components/Footer/Navigation/MainNav/mainnav.module.css @@ -22,7 +22,7 @@ justify-content: space-between; } -@media screen and (min-width: 767px) { +@media screen and (min-width: 1367px) { .mainNavigation { max-width: 360px; } diff --git a/components/Footer/Navigation/SecondaryNav/secondarynav.module.css b/components/Footer/Navigation/SecondaryNav/secondarynav.module.css index 057c1daa5..bedaf577b 100644 --- a/components/Footer/Navigation/SecondaryNav/secondarynav.module.css +++ b/components/Footer/Navigation/SecondaryNav/secondarynav.module.css @@ -27,8 +27,14 @@ @media screen and (min-width: 767px) { .secondaryNavigation { - margin-top: 0; - gap: 80px; + margin-top: var(--Spacing-x4); + gap: 120px; flex-direction: row; } } +@media screen and (min-width: 1367px) { + .secondaryNavigation { + margin-top: 0; + gap: 80px; + } +} diff --git a/components/Footer/Navigation/navigation.module.css b/components/Footer/Navigation/navigation.module.css index d78408ec4..db086dcb7 100644 --- a/components/Footer/Navigation/navigation.module.css +++ b/components/Footer/Navigation/navigation.module.css @@ -13,10 +13,7 @@ @media screen and (min-width: 767px) { .section { - padding: var(--Spacing-x9) var(--Spacing-x6); - } - .maxWidth { - flex-direction: row; + padding: var(--Spacing-x9) var(--Spacing-x5); } } @@ -24,4 +21,7 @@ .section { padding: var(--Spacing-x9) 0; } + .maxWidth { + flex-direction: row; + } } diff --git a/components/Footer/index.tsx b/components/Footer/index.tsx index a6f85bb80..83c5aa7d7 100644 --- a/components/Footer/index.tsx +++ b/components/Footer/index.tsx @@ -9,7 +9,9 @@ export default async function Footer() { const footerData = await serverClient().contentstack.base.footer({ lang: getLang(), }) - if (!footerData) { + const languages = await serverClient().contentstack.languageSwitcher.get() + + if (!footerData || !languages) { return } return ( @@ -22,6 +24,7 @@ export default async function Footer() { ) diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index 025d7dc98..7aa9e7785 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -24,7 +24,7 @@ export default function MobileMenu({ const { isHamburgerMenuOpen, isMyPagesMobileMenuOpen, - isLanguageSwitcherOpen, + isHeaderLanguageSwitcherOpen, toggleHamburgerMenu, toggleMyPagesMobileMenu, toggleLanguageSwitcher, @@ -40,8 +40,8 @@ export default function MobileMenu({ if (isMyPagesMobileMenuOpen) { toggleMyPagesMobileMenu() } else { - if (isLanguageSwitcherOpen) { - toggleLanguageSwitcher() + if (isHeaderLanguageSwitcherOpen) { + toggleLanguageSwitcher("header") } toggleHamburgerMenu() diff --git a/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx b/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx index 7a20e3da1..4406c4fd5 100644 --- a/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx +++ b/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx @@ -24,6 +24,7 @@ export default function LanguageSwitcherContent({ const { toggleLanguageSwitcher } = useDropdownStore() const languageSwitcherRef = useTrapFocus() const urlKeys = Object.keys(urls) as Lang[] + const position = type === "footer" ? "footer" : "header" return (
@@ -32,7 +33,7 @@ export default function LanguageSwitcherContent({ diff --git a/components/LanguageSwitcher/languageSwitcher.module.css b/components/LanguageSwitcher/languageSwitcher.module.css index 89ce55b56..6c85a22aa 100644 --- a/components/LanguageSwitcher/languageSwitcher.module.css +++ b/components/LanguageSwitcher/languageSwitcher.module.css @@ -1,6 +1,5 @@ .button { background-color: transparent; - color: var(--Base-Text-High-contrast); font-family: var(--typography-Caption-Regular-fontFamily); font-size: var(--typography-Caption-Regular-fontSize); border-width: 0; @@ -13,6 +12,14 @@ width: 100%; } +.burgundy .button { + color: var(--Base-Text-High-contrast); +} + +.pale .button { + color: var(--Primary-Dark-On-Surface-Text); +} + .chevron { justify-self: end; transition: transform 0.3s; @@ -45,29 +52,43 @@ .dropdown { position: absolute; - top: 2.25rem; background-color: var(--Base-Surface-Primary-light-Normal); border-radius: var(--Corner-radius-Large); box-shadow: 0px 0px 14px 6px #0000001a; display: none; min-width: 12.5rem; z-index: 1; + } + .top .dropdown { + top: 2.25rem; bottom: auto; } - /* Triangle above dropdown */ + .top .dropdown::before { + top: -1.25rem; + transform: rotate(180deg); + } + + /* Triangle dropdown */ .dropdown::before { content: ""; position: absolute; - top: -1.25rem; right: 2.4rem; - transform: rotate(180deg); border-width: 0.75rem; border-style: solid; border-color: var(--Base-Surface-Primary-light-Normal) transparent transparent transparent; } + .bottom .dropdown { + top: auto; + bottom: 2.25rem; + } + + .bottom .dropdown::before { + top: 100%; + } + .button { grid-template-columns: repeat(3, max-content); font-size: var(--typography-Body-Bold-fontSize); diff --git a/components/LanguageSwitcher/variants.ts b/components/LanguageSwitcher/variants.ts new file mode 100644 index 000000000..0fac0e5da --- /dev/null +++ b/components/LanguageSwitcher/variants.ts @@ -0,0 +1,20 @@ +import { cva } from "class-variance-authority" + +import styles from "./languageSwitcher.module.css" + +export const languageSwitcherVariants = cva(styles.languageSwitcher, { + variants: { + color: { + burgundy: styles.burgundy, + pale: styles.pale, + }, + position: { + header: styles.top, + footer: styles.bottom, + }, + defaultVariants: { + color: "burgundy", + position: "top", + }, + }, +}) diff --git a/stores/main-menu.ts b/stores/main-menu.ts index 6818d4cdb..454477f08 100644 --- a/stores/main-menu.ts +++ b/stores/main-menu.ts @@ -6,18 +6,20 @@ interface DropdownState { isHamburgerMenuOpen: boolean isMyPagesMobileMenuOpen: boolean isMyPagesMenuOpen: boolean - isLanguageSwitcherOpen: boolean + isHeaderLanguageSwitcherOpen: boolean + isFooterLanguageSwitcherOpen: boolean toggleHamburgerMenu: () => void toggleMyPagesMobileMenu: () => void toggleMyPagesMenu: () => void - toggleLanguageSwitcher: () => void + toggleLanguageSwitcher: (location: "header" | "footer") => void } const useDropdownStore = create((set) => ({ isHamburgerMenuOpen: false, isMyPagesMobileMenuOpen: false, isMyPagesMenuOpen: false, - isLanguageSwitcherOpen: false, + isHeaderLanguageSwitcherOpen: false, + isFooterLanguageSwitcherOpen: false, toggleHamburgerMenu: () => set( ({ isHamburgerMenuOpen, isMyPagesMenuOpen, isMyPagesMobileMenuOpen }) => { @@ -39,7 +41,8 @@ const useDropdownStore = create((set) => ({ isMyPagesMenuOpen, isMyPagesMobileMenuOpen, isHamburgerMenuOpen, - isLanguageSwitcherOpen, + isHeaderLanguageSwitcherOpen, + isFooterLanguageSwitcherOpen, }) => { // Close the other dropdowns if they're open if (!isMyPagesMobileMenuOpen) { @@ -49,8 +52,11 @@ const useDropdownStore = create((set) => ({ if (isHamburgerMenuOpen) { set({ isHamburgerMenuOpen: false }) } - if (isLanguageSwitcherOpen) { - set({ isLanguageSwitcherOpen: false }) + if (isHeaderLanguageSwitcherOpen) { + set({ isHeaderLanguageSwitcherOpen: false }) + } + if (isFooterLanguageSwitcherOpen) { + set({ isFooterLanguageSwitcherOpen: false }) } } return { isMyPagesMobileMenuOpen: !isMyPagesMobileMenuOpen } @@ -60,7 +66,8 @@ const useDropdownStore = create((set) => ({ set( ({ isHamburgerMenuOpen, - isLanguageSwitcherOpen, + isHeaderLanguageSwitcherOpen, + isFooterLanguageSwitcherOpen, isMyPagesMenuOpen, isMyPagesMobileMenuOpen, }) => { @@ -72,32 +79,40 @@ const useDropdownStore = create((set) => ({ if (isMyPagesMobileMenuOpen) { set({ isMyPagesMobileMenuOpen: false }) } - if (isLanguageSwitcherOpen) { - set({ isLanguageSwitcherOpen: false }) + if (isHeaderLanguageSwitcherOpen) { + set({ isHeaderLanguageSwitcherOpen: false }) + } + if (isFooterLanguageSwitcherOpen) { + set({ isFooterLanguageSwitcherOpen: false }) } } return { isMyPagesMenuOpen: !isMyPagesMenuOpen } } ), - toggleLanguageSwitcher: () => - set( - ({ - isLanguageSwitcherOpen, - isMyPagesMenuOpen, - isMyPagesMobileMenuOpen, - }) => { - // Close the other dropdowns if they're open - if (!isLanguageSwitcherOpen) { - if (isMyPagesMenuOpen) { - set({ isMyPagesMenuOpen: false }) - } - if (isMyPagesMobileMenuOpen) { - set({ isMyPagesMobileMenuOpen: false }) - } + toggleLanguageSwitcher: (location: "header" | "footer") => + set((state) => { + const isCurrentlyOpen = + location === "header" + ? state.isHeaderLanguageSwitcherOpen + : state.isFooterLanguageSwitcherOpen + + if (!isCurrentlyOpen) { + return { + isHeaderLanguageSwitcherOpen: location === "header", + isFooterLanguageSwitcherOpen: location === "footer", + isMyPagesMenuOpen: false, + isMyPagesMobileMenuOpen: false, + isHamburgerMenuOpen: false, } - return { isLanguageSwitcherOpen: !isLanguageSwitcherOpen } } - ), + + return { + isHeaderLanguageSwitcherOpen: + location === "header" ? false : state.isHeaderLanguageSwitcherOpen, + isFooterLanguageSwitcherOpen: + location === "footer" ? false : state.isFooterLanguageSwitcherOpen, + } + }), })) export default useDropdownStore diff --git a/types/components/footer/navigation.ts b/types/components/footer/navigation.ts index 4da0f479d..e10b43408 100644 --- a/types/components/footer/navigation.ts +++ b/types/components/footer/navigation.ts @@ -1,3 +1,5 @@ +import { LanguageSwitcherData } from "@/types/requests/languageSwitcher" + export type FooterLink = { isExternal: boolean openInNewTab: boolean @@ -39,6 +41,7 @@ export type FooterSecondaryNavProps = { export type FooterDetailsProps = { socialMedia?: FooterSocialMedia tertiaryLinks?: FooterLink[] + languageUrls?: LanguageSwitcherData } export type FooterNavigationProps = FooterMainNavProps & FooterSecondaryNavProps diff --git a/types/components/languageSwitcher/languageSwitcher.ts b/types/components/languageSwitcher/languageSwitcher.ts index fbadfab5d..d938edc87 100644 --- a/types/components/languageSwitcher/languageSwitcher.ts +++ b/types/components/languageSwitcher/languageSwitcher.ts @@ -1,6 +1,6 @@ import { LanguageSwitcherData } from "@/types/requests/languageSwitcher" export interface LanguageSwitcherProps { - type: "mobileHeader" | "mobileFooter" | "desktopHeader" | "desktopFooter" + type: "mobileHeader" | "desktopHeader" | "footer" urls: LanguageSwitcherData } From 208d8e6034b265e40a4457ce5508d6d3957eee25 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 5 Sep 2024 14:31:06 +0200 Subject: [PATCH 160/319] feat(SW-187): Removed mocked data --- components/Footer/mockedData.ts | 191 -------------------------------- 1 file changed, 191 deletions(-) delete mode 100644 components/Footer/mockedData.ts diff --git a/components/Footer/mockedData.ts b/components/Footer/mockedData.ts deleted file mode 100644 index 2a2a40398..000000000 --- a/components/Footer/mockedData.ts +++ /dev/null @@ -1,191 +0,0 @@ -export const footer = { - mainLinks: [ - { - title: "Travel guides", - href: "/travel-guides", - id: "travel-guides", - openInNewTab: false, - isExternal: false, - }, - { - title: "New hotels", - href: "/new-hotels", - id: "new-hotels", - openInNewTab: false, - isExternal: false, - }, - { - title: "Accessibililty", - href: "/accessibility", - id: "accessibility", - openInNewTab: false, - isExternal: false, - }, - { - title: "Sustanability", - href: "/sustainability", - id: "sustainability", - openInNewTab: false, - isExternal: false, - }, - ], - appDownloads: { - title: "Scandic App", - links: [ - { - title: "App Store", - href: "https://apps.apple.com/se/app/scandic-hotels/id1020208712", - id: "apple", - }, - { - title: "Google Play", - href: "https://play.google.com/store/apps/details?id=com.scandichotels.scandichotels", - id: "google", - }, - ], - }, - - secondaryLinks: [ - { - title: "Customer service", - links: [ - { - title: "Contact us", - href: "/contact-us", - id: "contact-us", - openInNewTab: false, - isExternal: false, - }, - { - title: "Frequntly asked questions", - href: "/frequently-asked-questions", - id: "frequently-asked-questions", - openInNewTab: false, - isExternal: false, - }, - { - title: "Rates and policys", - href: "/rates-and-policies", - id: "rates-and-policies", - openInNewTab: false, - isExternal: false, - }, - { - title: "Terms and conditions", - href: "/terms-and-conditions", - id: "terms-and-conditions", - openInNewTab: false, - isExternal: false, - }, - ], - }, - { - title: "About Scandic Hotels", - links: [ - { - title: "Scandic Group", - href: "/scandic-group", - id: "scandic-group", - openInNewTab: false, - isExternal: false, - }, - { - title: "Investors", - href: "/investors", - id: "investors", - openInNewTab: false, - isExternal: false, - }, - { - title: "Press", - href: "/press", - id: "press", - openInNewTab: false, - isExternal: false, - }, - { - title: "Sponsors", - href: "/sponsors", - id: "sponsors", - openInNewTab: false, - isExternal: false, - }, - { - title: "Partners", - href: "/partners", - id: "partners", - openInNewTab: false, - isExternal: false, - }, - { - title: "Career", - href: "/career", - id: "career", - openInNewTab: false, - isExternal: false, - }, - ], - }, - ], - tertiaryLinks: [ - { - title: "Cookies", - href: "/cookies", - id: "cookies", - }, - { - title: "Privacy policy", - href: "/privacy", - id: "privacy", - }, - ], - languageSwitcher: { - urls: { - da: { - url: "https://www.scandichotels.com/da", - isExternal: true, - }, - de: { - url: "https://www.scandichotels.com/de", - isExternal: true, - }, - en: { - url: "https://www.scandichotels.com/en", - isExternal: true, - }, - fi: { - url: "https://www.scandichotels.com/fi", - isExternal: true, - }, - no: { - url: "https://www.scandichotels.com/no", - isExternal: true, - }, - sv: { - url: "https://www.scandichotels.com/sv", - isExternal: true, - }, - }, - }, - copyrightCompany: "Scandic AB", - copyrightInfo: "All rights reserved.", - socialMedia: { - links: [ - { - title: "Facebook", - href: "https://www.facebook.com/scandichotels/", - id: "facebook", - }, - { - title: "Instagram", - href: "https://www.instagram.com/scandichotels/", - id: "instagram", - }, - { - title: "Tripadvisor", - href: "https://www.tripadvisor.com/Hotel_Review-g297628-d1020208712-Reviews-Scandic_Hotels-Stockholm_Sweden.html", - id: "tripadvisor", - }, - ], - }, -} From 205157bb364e69645a64948b0afd16bd3b681074 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 5 Sep 2024 14:36:54 +0200 Subject: [PATCH 161/319] feat(SW-187): removed log --- server/routers/contentstack/base/query.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/routers/contentstack/base/query.ts b/server/routers/contentstack/base/query.ts index 6ac685d00..b25656e96 100644 --- a/server/routers/contentstack/base/query.ts +++ b/server/routers/contentstack/base/query.ts @@ -431,8 +431,6 @@ export const baseQueryRouter = router({ }) ) - console.log("secondaryLinks", JSON.stringify(secondaryLinks)) - const tertiaryLinks = transformPageConnectionLinks( validatedFooterData.tertiary_links ) From 0ceded70492cd58939b4a669a4346474fa6f2d20 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 5 Sep 2024 14:46:45 +0200 Subject: [PATCH 162/319] feat(SW-187): Import type --- types/components/footer/navigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/components/footer/navigation.ts b/types/components/footer/navigation.ts index e10b43408..503c8ddb9 100644 --- a/types/components/footer/navigation.ts +++ b/types/components/footer/navigation.ts @@ -1,4 +1,4 @@ -import { LanguageSwitcherData } from "@/types/requests/languageSwitcher" +import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" export type FooterLink = { isExternal: boolean From b806824fde141938851b4e1a05969c5a28f2c2b2 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Thu, 29 Aug 2024 11:16:41 +0200 Subject: [PATCH 163/319] fix(SW-285): fix validation --- .../routers/contentstack/contentPage/query.ts | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index 73de1479e..24099a24e 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -9,6 +9,7 @@ import { makeImageVaultImage } from "@/utils/imageVault" import { validateContentPageSchema } from "./output" +import { ImageVaultAsset } from "@/types/components/imageVault" import { TrackingChannelEnum, TrackingSDKPageData, @@ -27,45 +28,45 @@ export const contentPageQueryRouter = router({ const response = await request( GetContentPage, - { - locale: lang, - uid, - }, - { - cache: "force-cache", - next: { - tags: [generateTag(lang, uid)], - }, - } + { locale: lang, uid }, + { cache: "force-cache", next: { tags: [generateTag(lang, uid)] } } ) - if (!response.data) { + const { content_page } = response.data + if (!content_page) { throw notFound(response) } - const validatedContentPage = validateContentPageSchema.safeParse( - response.data - ) + const validatedContentPage = validateContentPageSchema.safeParse({ + content_page: { + ...content_page, + hero_image: makeImageVaultImage(content_page.hero_image), + }, + }) if (!validatedContentPage.success) { console.error( `Failed to validate Contentpage Data - (lang: ${lang}, uid: ${uid})` ) - console.error(validatedContentPage.error) + console.error(validatedContentPage.error?.format()) return null } - const contentPageData = validatedContentPage.data.content_page + // Destructure hero_image and rename it to heroImage + const { hero_image: heroImage, ...restContentPage } = + validatedContentPage.data.content_page + + // Construct the contentPage object with the correct structure const contentPage: ContentPage = { - ...contentPageData, - heroImage: makeImageVaultImage(contentPageData.hero_image), + ...restContentPage, + heroImage: heroImage as ImageVaultAsset | undefined, } const tracking: TrackingSDKPageData = { - pageId: contentPageData.system.uid, - lang: contentPageData.system.locale as Lang, - publishedDate: contentPageData.system.updated_at, - createdDate: contentPageData.system.created_at, + pageId: contentPage.system.uid, + lang: contentPage.system.locale as Lang, + publishedDate: contentPage.system.updated_at, + createdDate: contentPage.system.created_at, channel: TrackingChannelEnum["static-content-page"], pageType: "staticcontentpage", } From 9a51cc6cb554edf645a36e2d069a3b27aab869e8 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 30 Aug 2024 08:10:57 +0200 Subject: [PATCH 164/319] feat(SW-285): Ship support for ContentPageBlocksContent --- components/Content/Blocks/index.tsx | 26 +++++++ components/ContentType/ContentPage/index.tsx | 2 + .../Fragments/PageLink/HotelPageLink.graphql | 8 ++ lib/graphql/Query/ContentPage.graphql | 77 +++++++++++++++++++ .../contentstack/contentPage/output.ts | 46 +++++++++++ .../routers/contentstack/contentPage/query.ts | 38 ++++++--- types/components/content/blocks.ts | 5 ++ types/components/content/enums.ts | 3 + .../trpc/routers/contentstack/contentPage.ts | 6 +- 9 files changed, 198 insertions(+), 13 deletions(-) create mode 100644 components/Content/Blocks/index.tsx create mode 100644 lib/graphql/Fragments/PageLink/HotelPageLink.graphql create mode 100644 types/components/content/blocks.ts create mode 100644 types/components/content/enums.ts diff --git a/components/Content/Blocks/index.tsx b/components/Content/Blocks/index.tsx new file mode 100644 index 000000000..8268aad0a --- /dev/null +++ b/components/Content/Blocks/index.tsx @@ -0,0 +1,26 @@ +import JsonToHtml from "@/components/JsonToHtml" + +// import DynamicContentBlock from "@/components/Loyalty/Blocks/DynamicContent" +// import Shortcuts from "@/components/MyPages/Blocks/Shortcuts" +// import CardsGrid from "./CardsGrid" +import type { BlocksProps } from "@/types/components/content/blocks" +import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" + +export function Blocks({ blocks }: BlocksProps) { + return blocks.map((block, idx) => { + const firstItem = idx === 0 + switch (block.__typename) { + case ContentBlocksTypenameEnum.ContentPageBlocksContent: + return ( +
+ +
+ ) + default: + return null + } + }) +} diff --git a/components/ContentType/ContentPage/index.tsx b/components/ContentType/ContentPage/index.tsx index 5f9f27dc9..3a3525766 100644 --- a/components/ContentType/ContentPage/index.tsx +++ b/components/ContentType/ContentPage/index.tsx @@ -1,5 +1,6 @@ import { serverClient } from "@/lib/trpc/server" +import { Blocks } from "@/components/Content/Blocks" import Hero from "@/components/Hero" import Intro from "@/components/Intro" import Preamble from "@/components/TempDesignSystem/Text/Preamble" @@ -36,6 +37,7 @@ export default async function ContentPage() { src={heroImage.url} /> ) : null} + {contentPage.blocks ? : null}
diff --git a/lib/graphql/Fragments/PageLink/HotelPageLink.graphql b/lib/graphql/Fragments/PageLink/HotelPageLink.graphql new file mode 100644 index 000000000..619b374d7 --- /dev/null +++ b/lib/graphql/Fragments/PageLink/HotelPageLink.graphql @@ -0,0 +1,8 @@ +fragment HotelPageLink on HotelPage { + system { + locale + uid + } + title + url +} diff --git a/lib/graphql/Query/ContentPage.graphql b/lib/graphql/Query/ContentPage.graphql index dee4d9610..5fc625fa3 100644 --- a/lib/graphql/Query/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage.graphql @@ -1,5 +1,40 @@ +#import "../Fragments/PageLink/AccountPageLink.graphql" +#import "../Fragments/PageLink/ContentPageLink.graphql" +#import "../Fragments/PageLink/LoyaltyPageLink.graphql" + query GetContentPage($locale: String!, $uid: String!) { content_page(uid: $uid, locale: $locale) { + blocks { + ... on ContentPageBlocksContent { + __typename + content { + content { + embedded_itemsConnection { + edges { + node { + __typename + ...LoyaltyPageLink + ...ContentPageLink + ...AccountPageLink + # TODO: Link HotelPage + # ...Image + # ... on ImageContainer { + # title + # image_left + # image_right + # system { + # uid + # } + # } + } + } + totalCount + } + json + } + } + } + } title header { heading @@ -15,6 +50,48 @@ query GetContentPage($locale: String!, $uid: String!) { } } +query GetContentPageRefs($locale: String!, $uid: String!) { + content_page(locale: $locale, uid: $uid) { + blocks { + ... on ContentPageBlocksContent { + __typename + content { + content { + embedded_itemsConnection { + edges { + node { + # No fragments used since we want to include __typename for each type to avoid fetching SystemAsset + ... on ContentPage { + __typename + system { + ...System + } + } + ... on LoyaltyPage { + __typename + system { + ...System + } + } + ... on AccountPage { + __typename + system { + ...System + } + } + } + } + } + } + } + } + } + system { + ...System + } + } +} + query GetDaDeEnUrlsContentPage($uid: String!) { de: all_content_page(where: { uid: $uid }, locale: "de") { items { diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 134a179f1..6965b3187 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -4,6 +4,43 @@ import { Lang } from "@/constants/languages" import { imageVaultAssetSchema } from "../schemas/imageVault" +import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" +import { ImageVaultAsset } from "@/types/components/imageVault" +import { Embeds } from "@/types/requests/embeds" +import { EdgesWithTotalCount } from "@/types/requests/utils/edges" +import { RTEDocument } from "@/types/rte/node" + +// Block Schema and types +const contentPageBlockTextContent = z.object({ + __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksContent), + content: z.object({ + content: z.object({ + embedded_itemsConnection: z.object({ + edges: z.array(z.any()), + totalCount: z.number(), + }), + json: z.any(), + }), + }), +}) + +const contentPageBlockItem = z.discriminatedUnion("__typename", [ + contentPageBlockTextContent, +]) + +type BlockContentRaw = z.infer +export interface RteBlockContent extends BlockContentRaw { + content: { + content: { + json: RTEDocument + embedded_itemsConnection: EdgesWithTotalCount + } + } +} + +export type Block = RteBlockContent + +// Content Page Schema and types export const validateContentPageSchema = z.object({ content_page: z.object({ title: z.string(), @@ -12,6 +49,7 @@ export const validateContentPageSchema = z.object({ preamble: z.string(), }), hero_image: imageVaultAssetSchema.nullable().optional(), + blocks: z.array(contentPageBlockItem).nullable(), system: z.object({ uid: z.string(), locale: z.nativeEnum(Lang), @@ -20,3 +58,11 @@ export const validateContentPageSchema = z.object({ }), }), }) + +export type ContentPageDataRaw = z.infer +type ContentPageRaw = ContentPageDataRaw["content_page"] + +export type ContentPage = Omit & { + heroImage?: ImageVaultAsset + blocks: Block[] +} diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index 24099a24e..35d864fd5 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -7,17 +7,19 @@ import { contentstackExtendedProcedureUID, router } from "@/server/trpc" import { generateTag } from "@/utils/generateTag" import { makeImageVaultImage } from "@/utils/imageVault" -import { validateContentPageSchema } from "./output" +import { removeEmptyObjects } from "../../utils" +import { + Block, + ContentPage, + ContentPageDataRaw, + validateContentPageSchema, +} from "./output" -import { ImageVaultAsset } from "@/types/components/imageVault" +import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" import { TrackingChannelEnum, TrackingSDKPageData, } from "@/types/components/tracking" -import { - ContentPage, - ContentPageDataRaw, -} from "@/types/trpc/routers/contentstack/contentPage" export const contentPageQueryRouter = router({ get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { @@ -32,15 +34,28 @@ export const contentPageQueryRouter = router({ { cache: "force-cache", next: { tags: [generateTag(lang, uid)] } } ) - const { content_page } = response.data + const { content_page } = removeEmptyObjects(response.data) if (!content_page) { throw notFound(response) } + const processedBlocks = content_page.blocks + ? content_page.blocks.map((block: any) => { + switch (block.__typename) { + case ContentBlocksTypenameEnum.ContentPageBlocksContent: + return block + default: + return block + } + }) + : null + + const heroImage = makeImageVaultImage(content_page.hero_image) const validatedContentPage = validateContentPageSchema.safeParse({ content_page: { ...content_page, - hero_image: makeImageVaultImage(content_page.hero_image), + blocks: processedBlocks, + hero_image: heroImage, }, }) @@ -52,14 +67,13 @@ export const contentPageQueryRouter = router({ return null } - // Destructure hero_image and rename it to heroImage - const { hero_image: heroImage, ...restContentPage } = + const { hero_image, blocks, ...restContentPage } = validatedContentPage.data.content_page - // Construct the contentPage object with the correct structure const contentPage: ContentPage = { ...restContentPage, - heroImage: heroImage as ImageVaultAsset | undefined, + heroImage, + blocks: blocks as Block[], } const tracking: TrackingSDKPageData = { diff --git a/types/components/content/blocks.ts b/types/components/content/blocks.ts new file mode 100644 index 000000000..af4815f69 --- /dev/null +++ b/types/components/content/blocks.ts @@ -0,0 +1,5 @@ +import { Block } from "@/server/routers/contentstack/contentPage/output" + +export type BlocksProps = { + blocks: Block[] +} diff --git a/types/components/content/enums.ts b/types/components/content/enums.ts new file mode 100644 index 000000000..3fa911d6b --- /dev/null +++ b/types/components/content/enums.ts @@ -0,0 +1,3 @@ +export enum ContentBlocksTypenameEnum { + ContentPageBlocksContent = "ContentPageBlocksContent", +} diff --git a/types/trpc/routers/contentstack/contentPage.ts b/types/trpc/routers/contentstack/contentPage.ts index d5a8cc322..49d55e3fa 100644 --- a/types/trpc/routers/contentstack/contentPage.ts +++ b/types/trpc/routers/contentstack/contentPage.ts @@ -1,6 +1,9 @@ import { z } from "zod" -import { validateContentPageSchema } from "@/server/routers/contentstack/contentPage/output" +import { + Block, + validateContentPageSchema, +} from "@/server/routers/contentstack/contentPage/output" import { ImageVaultAsset } from "@/types/components/imageVault" @@ -10,4 +13,5 @@ type ContentPageRaw = ContentPageDataRaw["content_page"] export type ContentPage = Omit & { heroImage?: ImageVaultAsset + blocks?: Block[] } From 57cd8c72da35e1a329b5bd3b216dbc7f10903f91 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 30 Aug 2024 11:16:36 +0200 Subject: [PATCH 165/319] feat(SW-285): add support for shortcuts in content pages --- components/Content/Blocks/index.tsx | 14 +++++++++++-- lib/graphql/Query/ContentPage.graphql | 21 +++++++++++++++++++ .../contentstack/contentPage/output.ts | 20 +++++++++++++++++- .../routers/contentstack/contentPage/query.ts | 18 ++++++++++++++++ types/components/content/enums.ts | 1 + 5 files changed, 71 insertions(+), 3 deletions(-) diff --git a/components/Content/Blocks/index.tsx b/components/Content/Blocks/index.tsx index 8268aad0a..3638d7af6 100644 --- a/components/Content/Blocks/index.tsx +++ b/components/Content/Blocks/index.tsx @@ -1,7 +1,7 @@ import JsonToHtml from "@/components/JsonToHtml" - // import DynamicContentBlock from "@/components/Loyalty/Blocks/DynamicContent" -// import Shortcuts from "@/components/MyPages/Blocks/Shortcuts" +import Shortcuts from "@/components/MyPages/Blocks/Shortcuts" + // import CardsGrid from "./CardsGrid" import type { BlocksProps } from "@/types/components/content/blocks" import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" @@ -19,6 +19,16 @@ export function Blocks({ blocks }: BlocksProps) { /> ) + case ContentBlocksTypenameEnum.ContentPageBlocksShortcuts: + return ( + + ) default: return null } diff --git a/lib/graphql/Query/ContentPage.graphql b/lib/graphql/Query/ContentPage.graphql index 5fc625fa3..47db78954 100644 --- a/lib/graphql/Query/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage.graphql @@ -34,6 +34,27 @@ query GetContentPage($locale: String!, $uid: String!) { } } } + ... on ContentPageBlocksShortcuts { + __typename + shortcuts { + title + preamble + shortcuts { + open_in_new_tab + text + linkConnection { + totalCount + edges { + node { + ...LoyaltyPageLink + ...ContentPageLink + ...AccountPageLink + } + } + } + } + } + } } title header { diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 6965b3187..8b3f6a0ea 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -24,8 +24,25 @@ const contentPageBlockTextContent = z.object({ }), }) +const contentPageShortcuts = z.object({ + __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksShortcuts), + shortcuts: z.object({ + title: z.string().nullable(), + preamble: z.string().nullable(), + shortcuts: z.array( + z.object({ + text: z.string().optional(), + openInNewTab: z.boolean(), + url: z.string(), + title: z.string(), + }) + ), + }), +}) + const contentPageBlockItem = z.discriminatedUnion("__typename", [ contentPageBlockTextContent, + contentPageShortcuts, ]) type BlockContentRaw = z.infer @@ -38,7 +55,8 @@ export interface RteBlockContent extends BlockContentRaw { } } -export type Block = RteBlockContent +export type Shortcuts = z.infer +export type Block = RteBlockContent | Shortcuts // Content Page Schema and types export const validateContentPageSchema = z.object({ diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index 35d864fd5..e1215d38b 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -6,6 +6,7 @@ import { contentstackExtendedProcedureUID, router } from "@/server/trpc" import { generateTag } from "@/utils/generateTag" import { makeImageVaultImage } from "@/utils/imageVault" +import { removeMultipleSlashes } from "@/utils/url" import { removeEmptyObjects } from "../../utils" import { @@ -44,6 +45,23 @@ export const contentPageQueryRouter = router({ switch (block.__typename) { case ContentBlocksTypenameEnum.ContentPageBlocksContent: return block + case ContentBlocksTypenameEnum.ContentPageBlocksShortcuts: + return { + ...block, + shortcuts: { + ...block.shortcuts, + shortcuts: block.shortcuts.shortcuts.map((shortcut: any) => ({ + text: shortcut.text, + openInNewTab: shortcut.open_in_new_tab, + ...shortcut.linkConnection.edges[0].node, + url: + shortcut.linkConnection.edges[0].node.web?.original_url || + removeMultipleSlashes( + `/${shortcut.linkConnection.edges[0].node.system.locale}/${shortcut.linkConnection.edges[0].node.url}` + ), + })), + }, + } default: return block } diff --git a/types/components/content/enums.ts b/types/components/content/enums.ts index 3fa911d6b..77fad4d68 100644 --- a/types/components/content/enums.ts +++ b/types/components/content/enums.ts @@ -1,3 +1,4 @@ export enum ContentBlocksTypenameEnum { ContentPageBlocksContent = "ContentPageBlocksContent", + ContentPageBlocksShortcuts = "ContentPageBlocksShortcuts", } From dd336ca4ab6b9b3ae7258cf8dcc82de4bee4b8c8 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 30 Aug 2024 14:31:27 +0200 Subject: [PATCH 166/319] feat(SW-285): add support for tags --- lib/graphql/Query/ContentPage.graphql | 22 +++ .../contentstack/contentPage/output.ts | 54 +++++++ .../routers/contentstack/contentPage/query.ts | 31 +++- .../routers/contentstack/contentPage/utils.ts | 132 ++++++++++++++++++ 4 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 server/routers/contentstack/contentPage/utils.ts diff --git a/lib/graphql/Query/ContentPage.graphql b/lib/graphql/Query/ContentPage.graphql index 47db78954..8202e89e1 100644 --- a/lib/graphql/Query/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage.graphql @@ -2,6 +2,11 @@ #import "../Fragments/PageLink/ContentPageLink.graphql" #import "../Fragments/PageLink/LoyaltyPageLink.graphql" +#import "../Fragments/Refs/MyPages/AccountPage.graphql" +#import "../Fragments/Refs/ContentPage/ContentPage.graphql" +#import "../Fragments/Refs/LoyaltyPage/LoyaltyPage.graphql" +#import "../Fragments/Refs/System.graphql" + query GetContentPage($locale: String!, $uid: String!) { content_page(uid: $uid, locale: $locale) { blocks { @@ -106,6 +111,23 @@ query GetContentPageRefs($locale: String!, $uid: String!) { } } } + ... on ContentPageBlocksShortcuts { + __typename + shortcuts { + shortcuts { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...LoyaltyPageRef + } + } + } + } + } + } } system { ...System diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 8b3f6a0ea..b99a873ff 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -7,6 +7,7 @@ import { imageVaultAssetSchema } from "../schemas/imageVault" import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" import { ImageVaultAsset } from "@/types/components/imageVault" import { Embeds } from "@/types/requests/embeds" +import { RTEEmbedsEnum } from "@/types/requests/rte" import { EdgesWithTotalCount } from "@/types/requests/utils/edges" import { RTEDocument } from "@/types/rte/node" @@ -84,3 +85,56 @@ export type ContentPage = Omit & { heroImage?: ImageVaultAsset blocks: Block[] } + +const rteConnectionRefs = z.object({ + edges: z.array( + z.object({ + node: z.object({ + __typename: z.nativeEnum(RTEEmbedsEnum), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), + }), + }) + ), +}) + +const contentPageBlockTextContentRefs = z.object({ + __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksContent), + content: z.object({ + content: z.object({ + embedded_itemsConnection: rteConnectionRefs, + }), + }), +}) + +const contentPageShortcutsRefs = z.object({ + __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksShortcuts), + shortcuts: z.object({ + shortcuts: z.array( + z.object({ + linkConnection: rteConnectionRefs, + }) + ), + }), +}) + +const contentPageBlockRefsItem = z.discriminatedUnion("__typename", [ + contentPageBlockTextContentRefs, + contentPageShortcutsRefs, +]) + +export const validateContentPageRefsSchema = z.object({ + content_page: z.object({ + blocks: z.array(contentPageBlockRefsItem).nullable(), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), + }), +}) + +export type ContentPageRefsDataRaw = z.infer< + typeof validateContentPageRefsSchema +> diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index e1215d38b..b473bb16c 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -4,7 +4,6 @@ import { request } from "@/lib/graphql/request" import { notFound } from "@/server/errors/trpc" import { contentstackExtendedProcedureUID, router } from "@/server/trpc" -import { generateTag } from "@/utils/generateTag" import { makeImageVaultImage } from "@/utils/imageVault" import { removeMultipleSlashes } from "@/utils/url" @@ -15,6 +14,12 @@ import { ContentPageDataRaw, validateContentPageSchema, } from "./output" +import { + fetchContentPageRefs, + generatePageTags, + getContentPageCounter, + validateContentPageRefs, +} from "./utils" import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" import { @@ -26,13 +31,31 @@ export const contentPageQueryRouter = router({ get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { const { lang, uid } = ctx - // TODO: Refs request should be done when adding more data to this query - // which has references to other pages. + const cleanedRefsData = await fetchContentPageRefs(lang, uid) + const validatedRefsData = validateContentPageRefs( + cleanedRefsData, + lang, + uid + ) + const tags = generatePageTags(validatedRefsData, lang) + + getContentPageCounter.add(1, { lang, uid }) + console.info( + "contentstack.contentPage start", + JSON.stringify({ + query: { lang, uid }, + }) + ) const response = await request( GetContentPage, { locale: lang, uid }, - { cache: "force-cache", next: { tags: [generateTag(lang, uid)] } } + { + cache: "force-cache", + next: { + tags, + }, + } ) const { content_page } = removeEmptyObjects(response.data) diff --git a/server/routers/contentstack/contentPage/utils.ts b/server/routers/contentstack/contentPage/utils.ts new file mode 100644 index 000000000..50c49e5e2 --- /dev/null +++ b/server/routers/contentstack/contentPage/utils.ts @@ -0,0 +1,132 @@ +import { metrics } from "@opentelemetry/api" + +import { Lang } from "@/constants/languages" +import { GetContentPageRefs } from "@/lib/graphql/Query/ContentPage.graphql" +import { request } from "@/lib/graphql/request" +import { notFound } from "@/server/errors/trpc" + +import { generateTag, generateTags } from "@/utils/generateTag" + +import { removeEmptyObjects } from "../../utils" +import { ContentPageRefsDataRaw, validateContentPageRefsSchema } from "./output" + +import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" +import { Edges } from "@/types/requests/utils/edges" +import { NodeRefs } from "@/types/requests/utils/refs" + +const meter = metrics.getMeter("trpc.contentPage") +// OpenTelemetry metrics: ContentPage + +export const getContentPageCounter = meter.createCounter( + "trpc.contentstack.contentPage.get" +) + +const getContentPageRefsCounter = meter.createCounter( + "trpc.contentstack.contentPage.get" +) +const getContentPageRefsFailCounter = meter.createCounter( + "trpc.contentstack.contentPage.get-fail" +) +const getContentPageRefsSuccessCounter = meter.createCounter( + "trpc.contentstack.contentPage.get-success" +) + +export async function fetchContentPageRefs(lang: Lang, uid: string) { + getContentPageRefsCounter.add(1, { lang, uid }) + console.info( + "contentstack.contentPage.refs start", + JSON.stringify({ + query: { lang, uid }, + }) + ) + const refsResponse = await request( + GetContentPageRefs, + { locale: lang, uid }, + { cache: "force-cache", next: { tags: [generateTag(lang, uid)] } } + ) + if (!refsResponse.data) { + const notFoundError = notFound(refsResponse) + getContentPageRefsFailCounter.add(1, { + lang, + uid, + error_type: "http_error", + error: JSON.stringify({ + code: notFoundError.code, + }), + }) + console.error( + "contentstack.contentPage.refs not found error", + JSON.stringify({ + query: { + lang, + uid, + }, + error: { code: notFoundError.code }, + }) + ) + throw notFoundError + } + + return removeEmptyObjects(refsResponse.data) +} + +export function validateContentPageRefs(data: any, lang: Lang, uid: string) { + const validatedData = validateContentPageRefsSchema.safeParse(data) + if (!validatedData.success) { + getContentPageRefsFailCounter.add(1, { + lang, + uid, + error_type: "validation_error", + error: JSON.stringify(validatedData.error), + }) + console.error( + "contentstack.contentPage.refs validation error", + JSON.stringify({ + query: { lang, uid }, + error: validatedData.error, + }) + ) + return null + } + getContentPageRefsSuccessCounter.add(1, { lang, uid }) + console.info( + "contentstack.contentPage.refs success", + JSON.stringify({ + query: { lang, uid }, + }) + ) + return validatedData.data +} + +export function generatePageTags(validatedData: any, lang: Lang): string[] { + const connections = getConnections(validatedData) + return [ + generateTags(lang, connections), + generateTag(lang, validatedData.content_page.system.uid), + ].flat() +} + +export function getConnections(refs: ContentPageRefsDataRaw) { + const connections: Edges[] = [] + if (refs.content_page.blocks) { + refs.content_page.blocks.forEach((item) => { + switch (item.__typename) { + case ContentBlocksTypenameEnum.ContentPageBlocksContent: { + if (item.content.content.embedded_itemsConnection.edges.length) { + connections.push(item.content.content.embedded_itemsConnection) + } + break + } + case ContentBlocksTypenameEnum.ContentPageBlocksShortcuts: { + item.shortcuts.shortcuts.forEach((shortcut) => { + if (shortcut.linkConnection.edges.length) { + connections.push(shortcut.linkConnection) + } + }) + break + } + } + }) + } + return connections +} From e88e4d92bf11fd5094d85fc49f9396f140049b87 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Mon, 2 Sep 2024 10:23:21 +0200 Subject: [PATCH 167/319] feat(SW-285): Ship support for ContentPageBlocksCardsGrid --- components/Content/Blocks/CardsGrid/index.tsx | 52 +++++++ components/Content/Blocks/index.tsx | 11 +- lib/graphql/Query/ContentPage.graphql | 36 +++++ .../contentstack/contentPage/output.ts | 146 +++++++++++++++++- .../routers/contentstack/contentPage/query.ts | 38 ++++- .../routers/contentstack/contentPage/utils.ts | 27 ++++ types/components/content/blocks.ts | 9 +- types/components/content/enums.ts | 6 + 8 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 components/Content/Blocks/CardsGrid/index.tsx diff --git a/components/Content/Blocks/CardsGrid/index.tsx b/components/Content/Blocks/CardsGrid/index.tsx new file mode 100644 index 000000000..2eb4b9b4e --- /dev/null +++ b/components/Content/Blocks/CardsGrid/index.tsx @@ -0,0 +1,52 @@ +import SectionContainer from "@/components/Section/Container" +import SectionHeader from "@/components/Section/Header" +import Card from "@/components/TempDesignSystem/Card" +import Grids from "@/components/TempDesignSystem/Grids" +import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard" + +import { CardsGridProps } from "@/types/components/content/blocks" +import { CardsGridEnum } from "@/types/components/content/enums" + +export default function CardsGrid({ + cards_grid, + firstItem = false, +}: CardsGridProps) { + return ( + + + + {cards_grid.cards.map((card) => { + switch (card.__typename) { + case CardsGridEnum.Card: { + return ( + + ) + } + case CardsGridEnum.LoyaltyCard: + return ( + + ) + } + })} + + + ) +} diff --git a/components/Content/Blocks/index.tsx b/components/Content/Blocks/index.tsx index 3638d7af6..9c396ed11 100644 --- a/components/Content/Blocks/index.tsx +++ b/components/Content/Blocks/index.tsx @@ -2,7 +2,8 @@ import JsonToHtml from "@/components/JsonToHtml" // import DynamicContentBlock from "@/components/Loyalty/Blocks/DynamicContent" import Shortcuts from "@/components/MyPages/Blocks/Shortcuts" -// import CardsGrid from "./CardsGrid" +import CardsGrid from "./CardsGrid" + import type { BlocksProps } from "@/types/components/content/blocks" import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" @@ -29,6 +30,14 @@ export function Blocks({ blocks }: BlocksProps) { title={block.shortcuts.title} /> ) + case ContentBlocksTypenameEnum.ContentPageBlocksCardsGrid: + return ( + + ) default: return null } diff --git a/lib/graphql/Query/ContentPage.graphql b/lib/graphql/Query/ContentPage.graphql index 8202e89e1..6dedd3057 100644 --- a/lib/graphql/Query/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage.graphql @@ -1,3 +1,8 @@ +#import "../Fragments/Blocks/Card.graphql" +#import "../Fragments/Blocks/LoyaltyCard.graphql" +#import "../Fragments/Blocks/Refs/Card.graphql" +#import "../Fragments/Blocks/Refs/LoyaltyCard.graphql" + #import "../Fragments/PageLink/AccountPageLink.graphql" #import "../Fragments/PageLink/ContentPageLink.graphql" #import "../Fragments/PageLink/LoyaltyPageLink.graphql" @@ -60,6 +65,24 @@ query GetContentPage($locale: String!, $uid: String!) { } } } + ... on ContentPageBlocksCardsGrid { + __typename + cards_grid { + title + preamble + layout + theme + cardConnection(limit: 10) { + edges { + node { + __typename + ...CardBlock + ...LoyaltyCardBlock + } + } + } + } + } } title header { @@ -128,6 +151,19 @@ query GetContentPageRefs($locale: String!, $uid: String!) { } } } + ... on ContentPageBlocksCardsGrid { + __typename + cards_grid { + cardConnection(limit: 10) { + edges { + node { + ...CardBlockRef + ...LoyaltyCardBlockRef + } + } + } + } + } } system { ...System diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index b99a873ff..c15d18555 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -4,9 +4,13 @@ import { Lang } from "@/constants/languages" import { imageVaultAssetSchema } from "../schemas/imageVault" -import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" +import { + CardsGridEnum, + ContentBlocksTypenameEnum, +} from "@/types/components/content/enums" import { ImageVaultAsset } from "@/types/components/imageVault" import { Embeds } from "@/types/requests/embeds" +import { PageLinkEnum } from "@/types/requests/pageLinks" import { RTEEmbedsEnum } from "@/types/requests/rte" import { EdgesWithTotalCount } from "@/types/requests/utils/edges" import { RTEDocument } from "@/types/rte/node" @@ -41,9 +45,74 @@ const contentPageShortcuts = z.object({ }), }) +// TODO: this is a separate entity, should be in a separate file. +const cardBlock = z.object({ + __typename: z.literal(CardsGridEnum.Card), + heading: z.string().nullable(), + body_text: z.string().nullable(), + background_image: z.any(), + scripted_top_title: z.string().nullable(), + primaryButton: z + .object({ + openInNewTab: z.boolean(), + title: z.string(), + href: z.string(), + isExternal: z.boolean(), + }) + .optional(), + secondaryButton: z + .object({ + openInNewTab: z.boolean(), + title: z.string(), + href: z.string(), + isExternal: z.boolean(), + }) + .optional(), + system: z.object({ + locale: z.nativeEnum(Lang), + uid: z.string(), + }), +}) + +const loyaltyCardBlock = z.object({ + __typename: z.literal(CardsGridEnum.LoyaltyCard), + heading: z.string().nullable(), + body_text: z.string().nullable(), + image: z.any(), + link: z + .object({ + openInNewTab: z.boolean(), + title: z.string(), + href: z.string(), + isExternal: z.boolean(), + }) + .optional(), + system: z.object({ + locale: z.nativeEnum(Lang), + uid: z.string(), + }), +}) + +const contentPageCardsItems = z.discriminatedUnion("__typename", [ + loyaltyCardBlock, + cardBlock, +]) + +const contentPageCards = z.object({ + __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksCardsGrid), + cards_grid: z.object({ + title: z.string().nullable(), + preamble: z.string().nullable(), + layout: z.enum(["twoColumnGrid", "threeColumnGrid", "twoPlusOne"]), + theme: z.enum(["one", "two", "three"]).nullable(), + cards: z.array(contentPageCardsItems), + }), +}) + const contentPageBlockItem = z.discriminatedUnion("__typename", [ contentPageBlockTextContent, contentPageShortcuts, + contentPageCards, ]) type BlockContentRaw = z.infer @@ -57,7 +126,22 @@ export interface RteBlockContent extends BlockContentRaw { } export type Shortcuts = z.infer -export type Block = RteBlockContent | Shortcuts + +type LoyaltyCardRaw = z.infer +type LoyaltyCard = Omit & { + image?: ImageVaultAsset +} +type CardRaw = z.infer +type Card = Omit & { + backgroundImage?: ImageVaultAsset +} +type CardsGridRaw = z.infer +export type CardsGrid = Omit & { + cards: (LoyaltyCard | Card)[] +} +export type CardsRaw = CardsGrid["cards_grid"]["cards"][number] + +export type Block = RteBlockContent | Shortcuts | CardsGrid // Content Page Schema and types export const validateContentPageSchema = z.object({ @@ -86,6 +170,20 @@ export type ContentPage = Omit & { blocks: Block[] } +const pageConnectionRefs = z.object({ + edges: z.array( + z.object({ + node: z.object({ + __typename: z.nativeEnum(PageLinkEnum), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), + }), + }) + ), +}) + const rteConnectionRefs = z.object({ edges: z.array( z.object({ @@ -100,6 +198,42 @@ const rteConnectionRefs = z.object({ ), }) +const cardBlockRefs = z.object({ + __typename: z.literal(CardsGridEnum.Card), + primary_button: z + .object({ + linkConnection: pageConnectionRefs, + }) + .nullable(), + secondary_button: z + .object({ + linkConnection: pageConnectionRefs, + }) + .nullable(), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), +}) + +const loyaltyCardBlockRefs = z.object({ + __typename: z.literal(CardsGridEnum.LoyaltyCard), + link: z + .object({ + linkConnection: pageConnectionRefs, + }) + .nullable(), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), +}) + +const cardGridCardsRef = z.discriminatedUnion("__typename", [ + loyaltyCardBlockRefs, + cardBlockRefs, +]) + const contentPageBlockTextContentRefs = z.object({ __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksContent), content: z.object({ @@ -109,6 +243,13 @@ const contentPageBlockTextContentRefs = z.object({ }), }) +const contentPageCardsRefs = z.object({ + __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksCardsGrid), + cards_grid: z.object({ + cardConnection: cardGridCardsRef, + }), +}) + const contentPageShortcutsRefs = z.object({ __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksShortcuts), shortcuts: z.object({ @@ -123,6 +264,7 @@ const contentPageShortcutsRefs = z.object({ const contentPageBlockRefsItem = z.discriminatedUnion("__typename", [ contentPageBlockTextContentRefs, contentPageShortcutsRefs, + contentPageCardsRefs, ]) export const validateContentPageRefsSchema = z.object({ diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index b473bb16c..7611ea410 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -18,10 +18,14 @@ import { fetchContentPageRefs, generatePageTags, getContentPageCounter, + makeButtonObject, validateContentPageRefs, } from "./utils" -import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" +import { + CardsGridEnum, + ContentBlocksTypenameEnum, +} from "@/types/components/content/enums" import { TrackingChannelEnum, TrackingSDKPageData, @@ -85,6 +89,38 @@ export const contentPageQueryRouter = router({ })), }, } + case ContentBlocksTypenameEnum.ContentPageBlocksCardsGrid: + return { + ...block, + cards_grid: { + ...block.cards_grid, + cards: block.cards_grid.cardConnection.edges.map( + ({ node: card }: { node: any }) => { + switch (card.__typename) { + case CardsGridEnum.Card: + return { + ...card, + backgroundImage: makeImageVaultImage( + card.background_image + ), + primaryButton: card.has_primary_button + ? makeButtonObject(card.primary_button) + : undefined, + secondaryButton: card.has_secondary_button + ? makeButtonObject(card.secondary_button) + : undefined, + } + case CardsGridEnum.LoyaltyCard: + return { + ...card, + image: makeImageVaultImage(card.image), + link: makeButtonObject(card.link), + } + } + } + ), + }, + } default: return block } diff --git a/server/routers/contentstack/contentPage/utils.ts b/server/routers/contentstack/contentPage/utils.ts index 50c49e5e2..3f2689fc0 100644 --- a/server/routers/contentstack/contentPage/utils.ts +++ b/server/routers/contentstack/contentPage/utils.ts @@ -6,6 +6,7 @@ import { request } from "@/lib/graphql/request" import { notFound } from "@/server/errors/trpc" import { generateTag, generateTags } from "@/utils/generateTag" +import { removeMultipleSlashes } from "@/utils/url" import { removeEmptyObjects } from "../../utils" import { ContentPageRefsDataRaw, validateContentPageRefsSchema } from "./output" @@ -130,3 +131,29 @@ export function getConnections(refs: ContentPageRefsDataRaw) { } return connections } + +export function makeButtonObject(button: any) { + if (!button) return null + + const isContenstackLink = + button?.is_contentstack_link || button.linkConnection?.edges?.length + const linkConnnectionNode = isContenstackLink + ? button.linkConnection.edges[0]?.node + : null + + return { + openInNewTab: button?.open_in_new_tab, + title: + button.cta_text || + (linkConnnectionNode + ? linkConnnectionNode.title + : button.external_link.title), + href: linkConnnectionNode + ? linkConnnectionNode.web?.original_url || + removeMultipleSlashes( + `/${linkConnnectionNode.system.locale}/${linkConnnectionNode.url}` + ) + : button.external_link.href, + isExternal: !isContenstackLink, + } +} diff --git a/types/components/content/blocks.ts b/types/components/content/blocks.ts index af4815f69..702a086d4 100644 --- a/types/components/content/blocks.ts +++ b/types/components/content/blocks.ts @@ -1,5 +1,12 @@ -import { Block } from "@/server/routers/contentstack/contentPage/output" +import { + Block, + CardsGrid, +} from "@/server/routers/contentstack/contentPage/output" export type BlocksProps = { blocks: Block[] } + +export type CardsGridProps = Pick & { + firstItem?: boolean +} diff --git a/types/components/content/enums.ts b/types/components/content/enums.ts index 77fad4d68..a629a32c6 100644 --- a/types/components/content/enums.ts +++ b/types/components/content/enums.ts @@ -1,4 +1,10 @@ export enum ContentBlocksTypenameEnum { ContentPageBlocksContent = "ContentPageBlocksContent", ContentPageBlocksShortcuts = "ContentPageBlocksShortcuts", + ContentPageBlocksCardsGrid = "ContentPageBlocksCardsGrid", +} + +export enum CardsGridEnum { + LoyaltyCard = "LoyaltyCard", + Card = "Card", } From 1c2a34591ba3927fafcdcf0f10e52dc85f4aaa22 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Mon, 2 Sep 2024 20:01:48 +0200 Subject: [PATCH 168/319] feat(SW-285): ship support for dynamic content --- .../HowItWorks/howItWorks.module.css | 9 + .../DynamicContent/HowItWorks/index.tsx | 13 + .../DynamicContent/LoyaltyLevels/data/DA.json | 117 ++++ .../DynamicContent/LoyaltyLevels/data/DE.json | 117 ++++ .../DynamicContent/LoyaltyLevels/data/EN.json | 117 ++++ .../DynamicContent/LoyaltyLevels/data/FI.json | 117 ++++ .../DynamicContent/LoyaltyLevels/data/NO.json | 117 ++++ .../DynamicContent/LoyaltyLevels/data/SV.json | 117 ++++ .../LoyaltyLevels/data/index.ts | 19 + .../DynamicContent/LoyaltyLevels/index.tsx | 119 ++++ .../LoyaltyLevels/loyaltyLevels.module.css | 55 ++ .../BenefitCard/benefitCard.module.css | 56 ++ .../OverviewTable/BenefitCard/index.tsx | 53 ++ .../BenefitList/benefitList.module.css | 19 + .../OverviewTable/BenefitList/index.tsx | 31 + .../BenefitValue/benefitValue.module.css | 19 + .../OverviewTable/BenefitValue/index.tsx | 26 + .../DesktopHeader/desktopHeader.module.css | 28 + .../LargeTable/DesktopHeader/index.tsx | 63 ++ .../OverviewTable/LargeTable/index.tsx | 77 +++ .../LargeTable/largeTable.module.css | 58 ++ .../OverviewTable/LevelSummary/index.tsx | 17 + .../LevelSummary/levelSummary.module.css | 35 ++ .../OverviewTable/YourLevelScript/index.tsx | 19 + .../YourLevelScript/yourLevel.module.css | 10 + .../DynamicContent/OverviewTable/data/DA.json | 538 ++++++++++++++++++ .../DynamicContent/OverviewTable/data/DE.json | 538 ++++++++++++++++++ .../DynamicContent/OverviewTable/data/EN.json | 538 ++++++++++++++++++ .../DynamicContent/OverviewTable/data/FI.json | 538 ++++++++++++++++++ .../DynamicContent/OverviewTable/data/NO.json | 538 ++++++++++++++++++ .../DynamicContent/OverviewTable/data/SV.json | 538 ++++++++++++++++++ .../DynamicContent/OverviewTable/index.tsx | 278 +++++++++ .../OverviewTable/overviewTable.module.css | 94 +++ .../DynamicContent/dynamicContent.module.css | 30 + .../Content/Blocks/DynamicContent/index.tsx | 67 +++ 35 files changed, 5125 insertions(+) create mode 100644 components/Content/Blocks/DynamicContent/HowItWorks/howItWorks.module.css create mode 100644 components/Content/Blocks/DynamicContent/HowItWorks/index.tsx create mode 100644 components/Content/Blocks/DynamicContent/LoyaltyLevels/data/DA.json create mode 100644 components/Content/Blocks/DynamicContent/LoyaltyLevels/data/DE.json create mode 100644 components/Content/Blocks/DynamicContent/LoyaltyLevels/data/EN.json create mode 100644 components/Content/Blocks/DynamicContent/LoyaltyLevels/data/FI.json create mode 100644 components/Content/Blocks/DynamicContent/LoyaltyLevels/data/NO.json create mode 100644 components/Content/Blocks/DynamicContent/LoyaltyLevels/data/SV.json create mode 100644 components/Content/Blocks/DynamicContent/LoyaltyLevels/data/index.ts create mode 100644 components/Content/Blocks/DynamicContent/LoyaltyLevels/index.tsx create mode 100644 components/Content/Blocks/DynamicContent/LoyaltyLevels/loyaltyLevels.module.css create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/BenefitCard/benefitCard.module.css create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/BenefitCard/index.tsx create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/BenefitList/benefitList.module.css create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/BenefitList/index.tsx create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/BenefitValue/benefitValue.module.css create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/BenefitValue/index.tsx create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/desktopHeader.module.css create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/index.tsx create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/LargeTable/largeTable.module.css create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/LevelSummary/index.tsx create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/LevelSummary/levelSummary.module.css create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/YourLevelScript/index.tsx create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/YourLevelScript/yourLevel.module.css create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/data/DA.json create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/data/DE.json create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/data/EN.json create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/data/FI.json create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/data/NO.json create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/data/SV.json create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/index.tsx create mode 100644 components/Content/Blocks/DynamicContent/OverviewTable/overviewTable.module.css create mode 100644 components/Content/Blocks/DynamicContent/dynamicContent.module.css create mode 100644 components/Content/Blocks/DynamicContent/index.tsx diff --git a/components/Content/Blocks/DynamicContent/HowItWorks/howItWorks.module.css b/components/Content/Blocks/DynamicContent/HowItWorks/howItWorks.module.css new file mode 100644 index 000000000..b9a1afac8 --- /dev/null +++ b/components/Content/Blocks/DynamicContent/HowItWorks/howItWorks.module.css @@ -0,0 +1,9 @@ +.container { + align-items: center; + background-color: var(--UI-Grey-10); + border-radius: var(--Corner-radius-xLarge); + display: flex; + height: 370px; + justify-content: center; + width: 100%; +} diff --git a/components/Content/Blocks/DynamicContent/HowItWorks/index.tsx b/components/Content/Blocks/DynamicContent/HowItWorks/index.tsx new file mode 100644 index 000000000..447a4fb1e --- /dev/null +++ b/components/Content/Blocks/DynamicContent/HowItWorks/index.tsx @@ -0,0 +1,13 @@ +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./howItWorks.module.css" + +export default async function HowItWorks() { + const { formatMessage } = await getIntl() + return ( +
+ {formatMessage({ id: "How it works" })} +
+ ) +} diff --git a/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/DA.json b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/DA.json new file mode 100644 index 000000000..4587fde60 --- /dev/null +++ b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/DA.json @@ -0,0 +1,117 @@ +{ + "levels": [ + { + "level": 1, + "name": "New Friend", + "requiredPoints": 0, + "requiredNights": 0, + "benefits": [ + { + "title": "Prisvenlige værelser" + }, + { + "title": "10% weekendrabat på mad" + }, + { + "title": "Gratis mocktail til børn under opholdet" + } + ] + }, + { + "level": 2, + "name": "Good Friend", + "requiredPoints": 5000, + "requiredNights": 0, + "benefits": [ + { + "title": "15% weekendrabat på mad" + } + ] + }, + { + "level": 3, + "name": "Close Friend", + "requiredPoints": 10000, + "requiredNights": 0, + "benefits": [ + { + "title": "Sen check ud – 1 time, når tilgængeligt" + }, + { + "title": "Voucher på DKK 50,-" + } + ] + }, + { + "level": 4, + "name": "Dear Friend", + "requiredPoints": 25000, + "requiredNights": 0, + "benefits": [ + { + "title": "25% optjeningsrate" + }, + { + "title": "Tidlig check ind, når tilgængeligt" + }, + { + "title": "Voucher på DKK 75,-" + } + ] + }, + { + "level": 5, + "name": "Loyal Friend", + "requiredPoints": 100000, + "requiredNights": 0, + "benefits": [ + { + "title": "Gratis opgraderinger, når tilgængelige" + }, + { + "title": "Voucher på DKK 100,-" + }, + { + "title": "2-for-1 morgenmad" + } + ] + }, + { + "level": 6, + "name": "True Friend", + "requiredPoints": 250000, + "requiredNights": 0, + "benefits": [ + { + "title": "50% optjeningsrate" + }, + { + "title": "Voucher på DKK 150,-" + }, + { + "title": "48-timers værelsesgaranti" + }, + { + "title": "Altid gratis morgenmad" + } + ] + }, + { + "level": 7, + "name": "Best Friend", + "requiredPoints": 400000, + "requiredNights": 100, + "benefits": [ + { + "title": "Voucher på DKK 200,-" + }, + { + "title": "Årlig eksklusiv gave" + }, + { + "title": "Børneboost" + } + ] + } + ] +} diff --git a/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/DE.json b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/DE.json new file mode 100644 index 000000000..64d2a3923 --- /dev/null +++ b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/DE.json @@ -0,0 +1,117 @@ +{ + "levels": [ + { + "level": 1, + "name": "New Friend", + "requiredPoints": 0, + "requiredNights": 0, + "benefits": [ + { + "title": "Freundschaftspreise für Hotelzimmer" + }, + { + "title": "10 % Rabatt auf Speisen an den Wochenenden" + }, + { + "title": "Kostenloser Kinder-Mocktail während des Aufenthalts" + } + ] + }, + { + "level": 2, + "name": "Good Friend", + "requiredPoints": 5000, + "requiredNights": 0, + "benefits": [ + { + "title": "15 % Rabatt auf Speisen an den Wochenenden" + } + ] + }, + { + "level": 3, + "name": "Close Friend", + "requiredPoints": 10000, + "requiredNights": 0, + "benefits": [ + { + "title": "Später Check-Out – 1 Stunde, wenn verfügbar" + }, + { + "title": "Gutschein über 5 EUR" + } + ] + }, + { + "level": 4, + "name": "Dear Friend", + "requiredPoints": 25000, + "requiredNights": 0, + "benefits": [ + { + "title": "25 % mehr Punkte" + }, + { + "title": "Früher Check-In – 1 Stunde, wenn verfügbar" + }, + { + "title": "Gutschein über 7,50 EUR" + } + ] + }, + { + "level": 5, + "name": "Loyal Friend", + "requiredPoints": 100000, + "requiredNights": 0, + "benefits": [ + { + "title": "Kostenloses Zimmer-Upgrade, wenn verfügbar" + }, + { + "title": "Gutschein über 10 EUR" + }, + { + "title": "Frühstück für Zwei zum Preis von einem" + } + ] + }, + { + "level": 6, + "name": "True Friend", + "requiredPoints": 250000, + "requiredNights": 0, + "benefits": [ + { + "title": "50 % mehr Punkte" + }, + { + "title": "Gutschein über 15 EUR" + }, + { + "title": "48-Stunden-Zimmergarantie" + }, + { + "title": "Jederzeit ein kostenloses Frühstück" + } + ] + }, + { + "level": 7, + "name": "Best Friend", + "requiredPoints": 400000, + "requiredNights": 100, + "benefits": [ + { + "title": "Gutschein über 20 EUR" + }, + { + "title": "Ein exklusives Geschenk pro Jahr" + }, + { + "title": "Ein Geschenk für Kinder" + } + ] + } + ] +} diff --git a/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/EN.json b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/EN.json new file mode 100644 index 000000000..958d9c4cb --- /dev/null +++ b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/EN.json @@ -0,0 +1,117 @@ +{ + "levels": [ + { + "level": 1, + "name": "New Friend", + "requiredPoints": 0, + "requiredNights": 0, + "benefits": [ + { + "title": "Friendly room rates" + }, + { + "title": "10% off on food on weekends" + }, + { + "title": "Free kids mocktail during stay" + } + ] + }, + { + "level": 2, + "name": "Good Friend", + "requiredPoints": 5000, + "requiredNights": 0, + "benefits": [ + { + "title": "15% on food on weekends" + } + ] + }, + { + "level": 3, + "name": "Close Friend", + "requiredPoints": 10000, + "requiredNights": 0, + "benefits": [ + { + "title": "Late checkout - 1 hour when available" + }, + { + "title": "5 EUR voucher" + } + ] + }, + { + "level": 4, + "name": "Dear Friend", + "requiredPoints": 25000, + "requiredNights": 0, + "benefits": [ + { + "title": "25% earn rate" + }, + { + "title": "Early check-in - 1 hour when available" + }, + { + "title": "7.50 EUR voucher" + } + ] + }, + { + "level": 5, + "name": "Loyal Friend", + "requiredPoints": 100000, + "requiredNights": 0, + "benefits": [ + { + "title": "Free room upgrade when available" + }, + { + "title": "10 EUR voucher" + }, + { + "title": "2-for-1 breakfast" + } + ] + }, + { + "level": 6, + "name": "True Friend", + "requiredPoints": 250000, + "requiredNights": 0, + "benefits": [ + { + "title": "50% earn rate" + }, + { + "title": "15 EUR voucher" + }, + { + "title": "48h room guarantee" + }, + { + "title": "Always free breakfast" + } + ] + }, + { + "level": 7, + "name": "Best Friend", + "requiredPoints": 400000, + "requiredNights": 100, + "benefits": [ + { + "title": "20 EUR voucher" + }, + { + "title": "Yearly exclusive gift" + }, + { + "title": "Kid's boost" + } + ] + } + ] +} diff --git a/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/FI.json b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/FI.json new file mode 100644 index 000000000..dcd499757 --- /dev/null +++ b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/FI.json @@ -0,0 +1,117 @@ +{ + "levels": [ + { + "level": 1, + "name": "New Friend", + "requiredPoints": 0, + "requiredNights": 0, + "benefits": [ + { + "title": "Ystävälliset huonehinnat" + }, + { + "title": "10 % alennusta ruoasta viikonloppuisin" + }, + { + "title": "Maksuton lasten mocktail majoituksen aikana" + } + ] + }, + { + "level": 2, + "name": "Good Friend", + "requiredPoints": 5000, + "requiredNights": 0, + "benefits": [ + { + "title": "15 % alennusta ruoasta viikonloppuisin" + } + ] + }, + { + "level": 3, + "name": "Close Friend", + "requiredPoints": 10000, + "requiredNights": 0, + "benefits": [ + { + "title": "Myöhäinen uloskirjautuminen – 1 tunti lisäaikaa varaustilanteen mukaan" + }, + { + "title": "Ravintolakuponki (arvo 5 €)" + } + ] + }, + { + "level": 4, + "name": "Dear Friend", + "requiredPoints": 25000, + "requiredNights": 0, + "benefits": [ + { + "title": "Ansaintakerroin +25 %" + }, + { + "title": "Aikainen sisäänkirjautuminen – 1 tunti lisäaikaa varaustilanteen mukaan" + }, + { + "title": "Ravintolakuponki (arvo 7,50 €)" + } + ] + }, + { + "level": 5, + "name": "Loyal Friend", + "requiredPoints": 100000, + "requiredNights": 0, + "benefits": [ + { + "title": "Maksuton huoneluokan korotus varaustilanteen mukaan" + }, + { + "title": "Ravintolakuponki (arvo 10 €)" + }, + { + "title": "Aamiainen – kaksi yhden hinnalla" + } + ] + }, + { + "level": 6, + "name": "True Friend", + "requiredPoints": 250000, + "requiredNights": 0, + "benefits": [ + { + "title": "Ansaintakerroin +50 %" + }, + { + "title": "Ravintolakuponki (arvo 15 €)" + }, + { + "title": "48 tunnin huonetakuu" + }, + { + "title": "Aamiainen aina maksutta" + } + ] + }, + { + "level": 7, + "name": "Best Friend", + "requiredPoints": 400000, + "requiredNights": 100, + "benefits": [ + { + "title": "Ravintolakuponki (arvo 20 €)" + }, + { + "title": "Henkilökohtainen lahja vuosittain" + }, + { + "title": "Tervetuliaislahja lapselle" + } + ] + } + ] +} diff --git a/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/NO.json b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/NO.json new file mode 100644 index 000000000..f4fa72f19 --- /dev/null +++ b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/NO.json @@ -0,0 +1,117 @@ +{ + "levels": [ + { + "level": 1, + "name": "New Friend", + "requiredPoints": 0, + "requiredNights": 0, + "benefits": [ + { + "title": "Vennlige rompriser" + }, + { + "title": "10 % rabatt på mat i helger" + }, + { + "title": "Gratis barne-mocktail under oppholdet" + } + ] + }, + { + "level": 2, + "name": "Good Friend", + "requiredPoints": 5000, + "requiredNights": 0, + "benefits": [ + { + "title": "15 % rabatt på mat i helger" + } + ] + }, + { + "level": 3, + "name": "Close Friend", + "requiredPoints": 10000, + "requiredNights": 0, + "benefits": [ + { + "title": "Sen utsjekking – 1 time når tilgjengelig" + }, + { + "title": "Kupong på 50 NOK" + } + ] + }, + { + "level": 4, + "name": "Dear Friend", + "requiredPoints": 25000, + "requiredNights": 0, + "benefits": [ + { + "title": "25 % opptjeningsrate" + }, + { + "title": "Tidlig innsjekk – 1 time når tilgjengelig" + }, + { + "title": "Kupong på 75 NOK" + } + ] + }, + { + "level": 5, + "name": "Loyal Friend", + "requiredPoints": 100000, + "requiredNights": 0, + "benefits": [ + { + "title": "Gratis romoppgradering når tilgjengelig" + }, + { + "title": "Kupong på 100 NOK" + }, + { + "title": "2-for-1 frokost" + } + ] + }, + { + "level": 6, + "name": "True Friend", + "requiredPoints": 250000, + "requiredNights": 0, + "benefits": [ + { + "title": "50 % opptjeningsrate" + }, + { + "title": "Kupong på 150 NOK" + }, + { + "title": "Romgaranti i 48 timer" + }, + { + "title": "Alltid gratis frokost" + } + ] + }, + { + "level": 7, + "name": "Best Friend", + "requiredPoints": 400000, + "requiredNights": 100, + "benefits": [ + { + "title": "Kupong på 200 NOK" + }, + { + "title": "Årlig eksklusiv gave" + }, + { + "title": "Boost for barn" + } + ] + } + ] +} diff --git a/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/SV.json b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/SV.json new file mode 100644 index 000000000..c543b884a --- /dev/null +++ b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/SV.json @@ -0,0 +1,117 @@ +{ + "levels": [ + { + "level": 1, + "name": "New Friend", + "requiredPoints": 0, + "requiredNights": 0, + "benefits": [ + { + "title": "Friendspriser på rum" + }, + { + "title": "10 % rabatt på mat under helger" + }, + { + "title": "Fri mocktail för barn under vistelse" + } + ] + }, + { + "level": 2, + "name": "Good Friend", + "requiredPoints": 5000, + "requiredNights": 0, + "benefits": [ + { + "title": "15 % rabatt på mat under helger" + } + ] + }, + { + "level": 3, + "name": "Close Friend", + "requiredPoints": 10000, + "requiredNights": 0, + "benefits": [ + { + "title": "Sen utcheckning – 1 timme, i mån av plats" + }, + { + "title": "Kupong 50 kr" + } + ] + }, + { + "level": 4, + "name": "Dear Friend", + "requiredPoints": 25000, + "requiredNights": 0, + "benefits": [ + { + "title": "25 % poängboost" + }, + { + "title": "Tidig incheckning – 1 timme, i mån av plats" + }, + { + "title": "Kupong 75 kr" + } + ] + }, + { + "level": 5, + "name": "Loyal Friend", + "requiredPoints": 100000, + "requiredNights": 0, + "benefits": [ + { + "title": "Kostnadsfri uppgradering av rum, i mån av plats" + }, + { + "title": "Kupong 100 kr" + }, + { + "title": "Frukost 2 för 1" + } + ] + }, + { + "level": 6, + "name": "True Friend", + "requiredPoints": 250000, + "requiredNights": 0, + "benefits": [ + { + "title": "50 % poängboost" + }, + { + "title": "Kupong 150 kr" + }, + { + "title": "48 timmars rumsgaranti" + }, + { + "title": "Alltid kostnadsfri frukost" + } + ] + }, + { + "level": 7, + "name": "Best Friend", + "requiredPoints": 400000, + "requiredNights": 100, + "benefits": [ + { + "title": "Kupong 200 kr" + }, + { + "title": "Spännande gåva varje år" + }, + { + "title": "Boost för barn" + } + ] + } + ] +} diff --git a/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/index.ts b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/index.ts new file mode 100644 index 000000000..04054d3fd --- /dev/null +++ b/components/Content/Blocks/DynamicContent/LoyaltyLevels/data/index.ts @@ -0,0 +1,19 @@ +import { Lang } from "@/constants/languages" + +import DA from "./DA.json" +import DE from "./DE.json" +import EN from "./EN.json" +import FI from "./FI.json" +import NO from "./NO.json" +import SV from "./SV.json" + +const levelsData = { + [Lang.en]: EN, + [Lang.sv]: SV, + [Lang.no]: NO, + [Lang.fi]: FI, + [Lang.da]: DA, + [Lang.de]: DE, +} + +export default levelsData diff --git a/components/Content/Blocks/DynamicContent/LoyaltyLevels/index.tsx b/components/Content/Blocks/DynamicContent/LoyaltyLevels/index.tsx new file mode 100644 index 000000000..11de6b89d --- /dev/null +++ b/components/Content/Blocks/DynamicContent/LoyaltyLevels/index.tsx @@ -0,0 +1,119 @@ +"use client" + +import { notFound, useParams } from "next/navigation" +import { useIntl } from "react-intl" + +import { Lang } from "@/constants/languages" + +import { CheckIcon } from "@/components/Icons" +import { + BestFriend, + CloseFriend, + DearFriend, + GoodFriend, + LoyalFriend, + NewFriend, + TrueFriend, +} from "@/components/Levels" +import BiroScript from "@/components/TempDesignSystem/Text/BiroScript" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Title from "@/components/TempDesignSystem/Text/Title" + +import levelsData from "./data" + +import styles from "./loyaltyLevels.module.css" + +import type { Level, LevelCardProps } from "@/types/components/content/blocks" + +export default function LoyaltyLevels() { + const params = useParams() + const lang = params.lang as Lang + const { formatMessage } = useIntl() + + const { levels } = levelsData[lang] + return ( +
+ {levels.map((level: Level) => ( + + ))} +
+ ) +} + +function LevelCard({ formatMessage, lang, level }: LevelCardProps) { + let Level = null + switch (level.level) { + case 1: + Level = NewFriend + break + case 2: + Level = GoodFriend + break + case 3: + Level = CloseFriend + break + case 4: + Level = DearFriend + break + case 5: + Level = LoyalFriend + break + case 6: + Level = TrueFriend + break + case 7: + Level = BestFriend + break + default: { + const loyaltyLevel = level.level as never + console.error(`Unsupported loyalty level given: ${loyaltyLevel}`) + notFound() + } + } + const pointsString = `${level.requiredPoints.toLocaleString(lang)} ${formatMessage({ id: "points" })} ` + + return ( +
+
+ + {formatMessage({ id: "Level" })} {level.level} + + +
+ + {pointsString} + {level.requiredNights ? ( + <span className={styles.redText}> + {formatMessage({ id: "or" })} {level.requiredNights}{" "} + {formatMessage({ id: "nights" })} + </span> + ) : null} + +
+ {level.benefits.map((benefit) => ( + + + {benefit.title} + + ))} +
+
+ ) +} diff --git a/components/Content/Blocks/DynamicContent/LoyaltyLevels/loyaltyLevels.module.css b/components/Content/Blocks/DynamicContent/LoyaltyLevels/loyaltyLevels.module.css new file mode 100644 index 000000000..13b379c8e --- /dev/null +++ b/components/Content/Blocks/DynamicContent/LoyaltyLevels/loyaltyLevels.module.css @@ -0,0 +1,55 @@ +.cardContainer { + display: grid; + gap: var(--Spacing-x2); +} + +.link { + justify-self: center; +} + +.card { + background-color: var(--Scandic-Brand-Pale-Peach); + border-radius: var(--Corner-radius-xLarge); + display: grid; + gap: var(--Spacing-x2); + min-height: 280px; + justify-items: center; + padding: var(--Spacing-x5) var(--Spacing-x1); + grid-template-rows: auto auto 1fr; +} + +.textContainer { + align-content: flex-start; + display: flex; + gap: var(--Spacing-x-one-and-half); + width: 100%; + flex-wrap: wrap; + justify-content: center; +} + +.redText { + color: var(--Base-Text-Accent); +} + +.levelText { + margin: 0; +} + +.checkIcon { + vertical-align: middle; +} + +@media screen and (min-width: 1367px) { + .cardContainer { + display: grid; + grid-template-columns: repeat(12, 1fr); + } + + .card:nth-of-type(-n + 3) { + grid-column: span 4; + } + + .card:nth-of-type(n + 4) { + grid-column: span 3; + } +} diff --git a/components/Content/Blocks/DynamicContent/OverviewTable/BenefitCard/benefitCard.module.css b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitCard/benefitCard.module.css new file mode 100644 index 000000000..ae643de29 --- /dev/null +++ b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitCard/benefitCard.module.css @@ -0,0 +1,56 @@ +.benefitCard { + padding-bottom: var(--Spacing-x-one-and-half); + z-index: 2; + grid-column: 1/3; +} + +.benefitCardHeader { + display: grid; + grid-template-columns: 1fr auto; +} + +.benefitCardDescription { + font-size: var(--typography-Caption-Regular-fontSize); + line-height: 150%; + padding-right: var(--Spacing-x4); +} + +.benefitInfo { + padding-bottom: var(--Spacing-x-one-and-half); +} + +.benefitComparison { + display: grid; + grid-template-columns: 1fr 1fr; +} + +.comparisonItem { + display: flex; + justify-content: center; + align-items: center; + padding-top: var(--Spacing-x-one-and-half); +} + +.details[open] .chevron { + transform: rotate(180deg); +} + +.chevron { + display: flex; + align-items: center; + color: var(--UI-Grey-80); +} + +.summary::-webkit-details-marker { + display: none; +} + +.summary { + list-style: none; +} + +@media screen and (min-width: 950px) { + .benefitComparison { + grid-template-columns: 1fr 1fr 1fr; + } +} diff --git a/components/Content/Blocks/DynamicContent/OverviewTable/BenefitCard/index.tsx b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitCard/index.tsx new file mode 100644 index 000000000..ee02753a7 --- /dev/null +++ b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitCard/index.tsx @@ -0,0 +1,53 @@ +import { ChevronDown } from "react-feather" + +import Title from "@/components/TempDesignSystem/Text/Title" + +import BenefitValue from "../BenefitValue" + +import styles from "./benefitCard.module.css" + +import type { BenefitCardProps } from "@/types/components/loyalty/blocks" + +export default function BenefitCard({ + comparedValues, + title, + description, +}: BenefitCardProps) { + return ( +
+
+
+ +
+ + {title} + + + + +
+
+

+

+
+
+ {comparedValues.map((benefit, idx) => ( +
+ +
+ ))} +
+
+ ) +} diff --git a/components/Content/Blocks/DynamicContent/OverviewTable/BenefitList/benefitList.module.css b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitList/benefitList.module.css new file mode 100644 index 000000000..dba0dd085 --- /dev/null +++ b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitList/benefitList.module.css @@ -0,0 +1,19 @@ +.benefitCardWrapper { + border-bottom: 1px solid var(--Base-Border-Subtle); + position: relative; + display: grid; + grid-template-columns: 1fr 1fr; + grid-column: 1/3; + padding-top: 0; + margin: var(--Spacing-x1) var(--Spacing-x2); +} + +.benefitCardWrapper:last-child { + border: none; +} + +@media screen and (min-width: 950px) { + .benefitCardWrapper { + grid-column: 1/4; + } +} diff --git a/components/Content/Blocks/DynamicContent/OverviewTable/BenefitList/index.tsx b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitList/index.tsx new file mode 100644 index 000000000..dc947bfe0 --- /dev/null +++ b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitList/index.tsx @@ -0,0 +1,31 @@ +import { findBenefit, getUnlockedBenefits } from "@/utils/loyaltyTable" + +import BenefitCard from "../BenefitCard" + +import styles from "./benefitList.module.css" + +import type { BenefitListProps } from "@/types/components/loyalty/blocks" + +export default function BenefitList({ levels }: BenefitListProps) { + return getUnlockedBenefits(levels).map((benefit) => { + const levelBenefits = levels.map((level) => { + return findBenefit(benefit, level) + }) + return ( +
+ { + return { + key: `${benefit.name}-${idx}`, + value: benefit.value, + unlocked: benefit.unlocked, + valueDetails: benefit.valueDetails, + } + })} + /> +
+ ) + }) +} diff --git a/components/Content/Blocks/DynamicContent/OverviewTable/BenefitValue/benefitValue.module.css b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitValue/benefitValue.module.css new file mode 100644 index 000000000..c72e3dd7f --- /dev/null +++ b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitValue/benefitValue.module.css @@ -0,0 +1,19 @@ +.benefitValueContainer { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--Spacing-x-half); + padding: 0 var(--Spacing-x4) 0 var(--Spacing-x4); + text-wrap: balance; +} + +.benefitValue { + font-size: var(--typography-Body-Bold-fontSize); + font-weight: var(--typography-Body-Bold-fontWeight); +} + +.benefitValueDetails { + font-size: var(--typography-Footnote-Regular-fontSize); + text-align: center; + color: var(--UI-Grey-80); +} diff --git a/components/Content/Blocks/DynamicContent/OverviewTable/BenefitValue/index.tsx b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitValue/index.tsx new file mode 100644 index 000000000..e22b8f70e --- /dev/null +++ b/components/Content/Blocks/DynamicContent/OverviewTable/BenefitValue/index.tsx @@ -0,0 +1,26 @@ +import { Minus } from "react-feather" + +import CheckCircle from "@/components/Icons/CheckCircle" + +import styles from "./benefitValue.module.css" + +import type { BenefitValueProps } from "@/types/components/loyalty/blocks" + +export default function BenefitValue({ benefit }: BenefitValueProps) { + if (!benefit.unlocked) { + return + } + if (!benefit.value) { + return + } + return ( +
+ {benefit.value} + {benefit.valueDetails && ( + + {benefit.valueDetails} + + )} +
+ ) +} diff --git a/components/Content/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/desktopHeader.module.css b/components/Content/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/desktopHeader.module.css new file mode 100644 index 000000000..32ed2be4d --- /dev/null +++ b/components/Content/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/desktopHeader.module.css @@ -0,0 +1,28 @@ +.iconRow { + border-bottom: none; + position: sticky; + top: 0; + z-index: 1; +} + +.verticalTableHeader { + min-width: 242px; +} + +.iconTh { + padding: var(--Spacing-x5) var(--Spacing-x2) var(--Spacing-x2); + font-weight: var(--typography-Caption-Regular-fontWeight); + vertical-align: bottom; +} + +.summaryTh { + font-size: var(--typography-Caption-Regular-fontSize); + font-weight: var(--typography-Caption-Regular-fontWeight); + padding: 0 var(--Spacing-x2) var(--Spacing-x2); + vertical-align: top; +} + +.select { + font-weight: var(--typography-Caption-Regular-fontWeight); + padding: 0 var(--Spacing-x2) var(--Spacing-x2); +} diff --git a/components/Content/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/index.tsx b/components/Content/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/index.tsx new file mode 100644 index 000000000..edb176eba --- /dev/null +++ b/components/Content/Blocks/DynamicContent/OverviewTable/LargeTable/DesktopHeader/index.tsx @@ -0,0 +1,63 @@ +import Image from "@/components/Image" + +import LevelSummary from "../../LevelSummary" +import YourLevel from "../../YourLevelScript" + +import styles from "./desktopHeader.module.css" + +import type { + DesktopSelectColumns, + LargeTableProps, +} from "@/types/components/loyalty/blocks" + +export default function DesktopHeader({ + levels, + activeLevel, + Select, +}: LargeTableProps) { + return ( + + + + {levels.map((level, idx) => { + return ( + + {activeLevel === level.level ? : null} + {level.name} + + ) + })} + + + + {levels.map((level, idx) => { + return ( + + + + ) + })} + + {Select && ( + + + {["A", "B", "C"].map((column, idx) => { + return ( + + + + ) + } + + function SelectDesktop({ column }: DesktopSelectColumns) { + let selectedLevelDesktop: ComparisonLevel + let actionEnumDesktop: overviewTableActionsEnum + switch (column) { + case "A": + selectedLevelDesktop = selectionState.selectedLevelADesktop + actionEnumDesktop = + overviewTableActionsEnum.SET_SELECTED_LEVEL_A_DESKTOP + break + case "B": + selectedLevelDesktop = selectionState.selectedLevelBDesktop + actionEnumDesktop = + overviewTableActionsEnum.SET_SELECTED_LEVEL_B_DESKTOP + break + case "C": + selectedLevelDesktop = selectionState.selectedLevelCDesktop + actionEnumDesktop = + overviewTableActionsEnum.SET_SELECTED_LEVEL_C_DESKTOP + break + default: + return null + } + return ( + - ))} + ))} */}
{formatMessage({ id: "Hotel facilities" })} - {filters.hotelFacilities.map((hotelFilter) => ( + {/* {filters.hotelFacilities.map((hotelFilter) => (
- ))} + ))} */}
{formatMessage({ id: "Hotel surroundings" })} - {filters.hotelSurroundings.map((surroundings) => ( + {/* {filters.hotelSurroundings.map((surroundings) => (
- ))} + ))} */}
) diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 562a0e608..51106963b 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -22,5 +22,7 @@ export const getRatesInputSchema = z.object({ }) export const getFiltersInputSchema = z.object({ - hotelId: z.string(), + language: z.string(), + country: z.string(), + city: z.string(), }) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 192bab55c..39ccaeaf4 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -554,11 +554,29 @@ export const getRatesSchema = z.array(rate) export type Rate = z.infer -const hotelFilter = z.object({ - roomFacilities: z.array(z.string()), - hotelFacilities: z.array(z.string()), - hotelSurroundings: z.array(z.string()), +const hotelFilterSchema = z.object({ + data: z.array( + z.object({ + attributes: z.object({ + name: z.string(), + operaId: z.string(), + isPublished: z.boolean(), + cityId: z.string(), + cityName: z.string(), + ratings: ratingsSchema, + address: addressSchema, + location: locationSchema, + hotelContent: hotelContentSchema, + detailedFacilities: z.array(detailedFacilitySchema), + isActive: z.boolean(), + }), + id: z.string(), + language: z.unknown(), + hotelInformationSystemId: z.number(), + type: z.string(), + }) + ), }) -export const getFiltersSchema = hotelFilter -export type HotelFilter = z.infer +export const getHotelFilterSchema = hotelFilterSchema +export type HotelFilter = z.infer diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index b35ed7cd2..69aa9ad59 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -33,8 +33,8 @@ import { } from "./input" import { getAvailabilitySchema, - getFiltersSchema, getHotelDataSchema, + getHotelFilterSchema, getRatesSchema, roomSchema, } from "./output" @@ -57,6 +57,10 @@ const availabilityFailCounter = meter.createCounter( "trpc.hotel.availability-fail" ) +const filterCounter = meter.createCounter("trcp.hotel.filter") +const filterSuccessCounter = meter.createCounter("trcp.hotel.filter-success") +const filterFailCounter = meter.createCounter("trcp.hotel.filter-fail") + async function getContentstackData( locale: string, uid: string | null | undefined @@ -420,31 +424,97 @@ export const hotelQueryRouter = router({ }), }), filters: router({ - get: publicProcedure + get: serviceProcedure .input(getFiltersInputSchema) .query(async ({ input, ctx }) => { - console.info("api.hotels.filters start", JSON.stringify({})) + const { language, country, city } = input + const params: Record = { + language, + country, + city, + } - if (!tempFilterData) { + filterCounter.add(1, { + language, + country, + city, + }) + console.info( + "api.hotels.filters start", + JSON.stringify({ + query: { params }, + }) + ) + + const apiResponse = await api.get( + api.endpoints.v1.hotels, + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + }, + params + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + filterFailCounter.add(1, { + language, + country, + city, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) console.error( "api.hotels.filters error", - JSON.stringify({ error: null }) + JSON.stringify({ + query: { + params, + }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) ) - //Can't return null here since consuming component does not handle null yet - // return null + return null } - const validateFilterData = getFiltersSchema.safeParse(tempFilterData) + + const apiJson = await apiResponse.json() + const validateFilterData = getHotelFilterSchema.safeParse(apiJson) if (!validateFilterData.success) { + filterFailCounter.add(1, { + language, + country, + city, + error_type: "validation_error", + error: JSON.stringify(validateFilterData.error), + }) console.error( "api.hotels.filters validation error", JSON.stringify({ + query: { params }, error: validateFilterData.error, }) ) throw badRequestError() } - console.info("api.hotels.rates success", JSON.stringify({})) + + filterSuccessCounter.add(1, { + language, + country, + city, + }) + console.info( + "api.hotels.fuilters success", + JSON.stringify({ query: { params: params } }) + ) return validateFilterData.data }), }), From 6089af764a7c2b189bb9b07e6a70b462f0d12dfb Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 4 Sep 2024 15:57:32 +0200 Subject: [PATCH 263/319] feat(SW-251): filter request --- .../hotelreservation/select-hotel/page.tsx | 6 +---- .../SelectHotel/HotelFilter/index.tsx | 5 ++++- server/routers/hotels/output.ts | 8 ++++--- server/routers/hotels/query.ts | 15 +++++++++++-- server/routers/hotels/tempFilterData.json | 22 ------------------- .../selectHotel/hotelFilterProps.ts | 7 ++++-- 6 files changed, 28 insertions(+), 35 deletions(-) delete mode 100644 server/routers/hotels/tempFilterData.json diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx index 1352fbdcd..cb2e8cd43 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx @@ -23,13 +23,11 @@ export default async function SelectHotelPage({ const getHotelFitlers = await serverClient().hotel.filters.get({ language: params.lang, country: "Sweden", - city: "Halmstad", + city: "Helsingborg", }) if (!getHotelFitlers) return null - console.log(getHotelFitlers.data) - const getAvailableHotels = await serverClient().hotel.availability.get({ cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54", roomStayStartDate: "2024-11-02", @@ -41,8 +39,6 @@ export default async function SelectHotelPage({ const { availability } = getAvailableHotels - console.log(availability) - return (
diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index 84984b9d7..fe7b98fe9 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -4,7 +4,10 @@ import styles from "./hotelFilter.module.css" import { HotelFilterProps } from "@/types/components/hotelReservation/selectHotel/hotelFilterProps" -export default async function HotelFilter({ filters }: HotelFilterProps) { +export default async function HotelFilter({ + hotelId, + filters, +}: HotelFilterProps) { const { formatMessage } = await getIntl() return ( diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 39ccaeaf4..a72d08c3a 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -145,7 +145,7 @@ const hotelContentSchema = z.object({ restaurantsOverviewPageLinkText: z.string(), restaurantsOverviewPageLink: z.string(), restaurantsContentDescriptionShort: z.string(), - restaurantsContentDescriptionMedium: z.string(), + restaurantsContentDescriptionMedium: z.string().optional(), }), }) @@ -551,7 +551,6 @@ const rate = z.object({ }) export const getRatesSchema = z.array(rate) - export type Rate = z.infer const hotelFilterSchema = z.object({ @@ -566,7 +565,7 @@ const hotelFilterSchema = z.object({ ratings: ratingsSchema, address: addressSchema, location: locationSchema, - hotelContent: hotelContentSchema, + hotelContent: hotelContentSchema.optional(), detailedFacilities: z.array(detailedFacilitySchema), isActive: z.boolean(), }), @@ -580,3 +579,6 @@ const hotelFilterSchema = z.object({ export const getHotelFilterSchema = hotelFilterSchema export type HotelFilter = z.infer +export type HotelId = HotelFilter["data"][number]["id"] +export type HotelFilters = + HotelFilter["data"][number]["attributes"]["detailedFacilities"] diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 69aa9ad59..54a97be78 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -38,7 +38,6 @@ import { getRatesSchema, roomSchema, } from "./output" -import tempFilterData from "./tempFilterData.json" import tempRatesData from "./tempRatesData.json" import { HotelBlocksTypenameEnum } from "@/types/components/hotelPage/enums" @@ -515,7 +514,19 @@ export const hotelQueryRouter = router({ "api.hotels.fuilters success", JSON.stringify({ query: { params: params } }) ) - return validateFilterData.data + + const hotelData = validateFilterData.data.data + const filters = hotelData.flatMap( + (data) => data.attributes.detailedFacilities + ) + const unieqId = [...new Set(filters.map((data) => data.id))] + + return { + hotelId: hotelData.map((data) => data.id), + filters: unieqId.map((data) => + filters.find((find) => find.id === data) + ), + } }), }), }) diff --git a/server/routers/hotels/tempFilterData.json b/server/routers/hotels/tempFilterData.json deleted file mode 100644 index e58bdf50b..000000000 --- a/server/routers/hotels/tempFilterData.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "roomFacilities": ["Balcony", "Bathub", "View", "Conntecting doors"], - "hotelFacilities": [ - "Parking inside", - "Parking outside", - "Parking electric", - "Sauna", - "Pool", - "Restaurant", - "Bar", - "Sky/rooftop bar", - "Gym", - "Coworking" - ], - "hotelSurroundings": [ - "Beach", - "Lake or sea", - "Hiking", - "Mountains", - "Golf course" - ] -} diff --git a/types/components/hotelReservation/selectHotel/hotelFilterProps.ts b/types/components/hotelReservation/selectHotel/hotelFilterProps.ts index e100131ba..709e9378d 100644 --- a/types/components/hotelReservation/selectHotel/hotelFilterProps.ts +++ b/types/components/hotelReservation/selectHotel/hotelFilterProps.ts @@ -1,3 +1,6 @@ -import { HotelFilter } from "@/server/routers/hotels/output" +import { HotelFilters, HotelId } from "@/server/routers/hotels/output" -export type HotelFilterProps = { filters: HotelFilter } +export type HotelFilterProps = { + hotelId: HotelId + filters: HotelFilters +} From 024a095dc65e8e1b55750ea849f8624cfb626aec Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 5 Sep 2024 13:25:46 +0200 Subject: [PATCH 264/319] feat(SW-251): check for undefined --- .../hotelreservation/select-hotel/page.tsx | 4 +++- .../SelectHotel/HotelFilter/index.tsx | 19 +++++++------------ server/routers/hotels/query.ts | 10 +++++----- .../selectHotel/hotelFilterProps.ts | 3 +-- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx index cb2e8cd43..1a75c56f5 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx @@ -28,6 +28,8 @@ export default async function SelectHotelPage({ if (!getHotelFitlers) return null + const { filters } = getHotelFitlers + const getAvailableHotels = await serverClient().hotel.availability.get({ cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54", roomStayStartDate: "2024-11-02", @@ -54,7 +56,7 @@ export default async function SelectHotelPage({ {intl.formatMessage({ id: "Show map" })} - {/* */} +
{availability.length ? ( diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index fe7b98fe9..f002c83b9 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -4,25 +4,20 @@ import styles from "./hotelFilter.module.css" import { HotelFilterProps } from "@/types/components/hotelReservation/selectHotel/hotelFilterProps" -export default async function HotelFilter({ - hotelId, - filters, -}: HotelFilterProps) { +export default async function HotelFilter({ filters }: HotelFilterProps) { const { formatMessage } = await getIntl() + console.log(filters) + return (
{availability.length ? ( diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 43cf56a03..0f37287f1 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -1,3 +1,4 @@ +import { serverClient } from "@/lib/trpc/server" import tempHotelData from "@/server/routers/hotels/tempHotelData.json" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" @@ -15,6 +16,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" import styles from "./hotelCard.module.css" @@ -31,7 +33,16 @@ export default async function HotelCard({ // TODO: Use real endpoint. const hotel = tempHotelData.data.attributes - const sortedAmenities = hotel.detailedFacilities + const hotelResponse = await serverClient().hotel.hotel.get({ + hotelId: hotelId.toString(), + language: getLang(), + }) + + if (!hotelResponse) return null + + const { data } = hotelResponse + + const sortedAmenities = data.attributes.detailedFacilities .sort((a, b) => b.sortOrder - a.sortOrder) .slice(0, 5) @@ -39,8 +50,8 @@ export default async function HotelCard({
{hotel.hotelContent.images.metaData.altText} - {hotel.ratings?.tripAdvisor.rating} + {data.attributes.ratings?.tripAdvisor.rating}
@@ -58,10 +69,10 @@ export default async function HotelCard({ {hotel.name} - {`${hotel.address.streetAddress}, ${hotel.address.city}`} + {`${data.attributes.address.streetAddress}, ${data.attributes.address.city}`} - {`${hotel.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} + {`${data.attributes.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index f002c83b9..81230461d 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -2,28 +2,13 @@ import { getIntl } from "@/i18n" import styles from "./hotelFilter.module.css" -import { HotelFilterProps } from "@/types/components/hotelReservation/selectHotel/hotelFilterProps" - -export default async function HotelFilter({ filters }: HotelFilterProps) { +export default async function HotelFilter() { const { formatMessage } = await getIntl() - console.log(filters) - return (
- {hotel.name} + {hotelData.name} - {`${data.attributes.address.streetAddress}, ${data.attributes.address.city}`} + {`${hotelData.address?.streetAddress}, ${hotelData.address?.city}`} - {`${data.attributes.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} + {`${hotelData.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
- {sortedAmenities.map((facility) => { + {sortedAmenities?.map((facility) => { const IconComponent = mapFacilityToIcon(facility.name) return (
diff --git a/components/HotelReservation/HotelCardListing/hotelCardListing.module.css b/components/HotelReservation/HotelCardListing/hotelCardListing.module.css new file mode 100644 index 000000000..be62321df --- /dev/null +++ b/components/HotelReservation/HotelCardListing/hotelCardListing.module.css @@ -0,0 +1,5 @@ +.hotelCards { + display: flex; + flex-direction: column; + gap: var(--Spacing-x4); +} diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx new file mode 100644 index 000000000..2af4f0b9c --- /dev/null +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -0,0 +1,25 @@ +"use client" + +import Title from "@/components/TempDesignSystem/Text/Title" + +import HotelCard from "../HotelCard" + +import styles from "./hotelCardListing.module.css" + +import { HotelCardListingProps } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" + +export default function HotelCardListing({ hotelData }: HotelCardListingProps) { + if (!hotelData) return null + + return ( +
+ {hotelData && hotelData.length ? ( + hotelData.map((hotel) => ( + + )) + ) : ( + Hallå + )} +
+ ) +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index 81230461d..cc171f654 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -3,15 +3,15 @@ import { getIntl } from "@/i18n" import styles from "./hotelFilter.module.css" export default async function HotelFilter() { - const { formatMessage } = await getIntl() + const intl = await getIntl() return (
- {hotelData.name} + {hotelData?.name} - {`${hotelData.address?.streetAddress}, ${hotelData.address?.city}`} + {`${hotelData?.address?.streetAddress}, ${hotelData?.address?.city}`} - {`${hotelData.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} + {`${hotelData?.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index cc171f654..a3a993aee 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -1,23 +1,28 @@ -import { getIntl } from "@/i18n" +"use client" + +import { useIntl } from "react-intl" import styles from "./hotelFilter.module.css" -export default async function HotelFilter() { - const intl = await getIntl() +import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters" + +export default function HotelFilter({ filters }: HotelFiltersProps) { + const intl = useIntl() return ( ) diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index a89c55dc2..416d41eee 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -21,7 +21,7 @@ export const getRatesInputSchema = z.object({ hotelId: z.string(), }) -export const getPlaceholderInputSchema = z.object({ +export const getlHotelDataInputSchema = z.object({ hotelId: z.string(), language: z.string(), include: z diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 1532d1690..5f8ae5dd9 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -28,7 +28,7 @@ import { import { getAvailabilityInputSchema, getHotelInputSchema, - getPlaceholderInputSchema, + getlHotelDataInputSchema, getRatesInputSchema, } from "./input" import { @@ -55,10 +55,6 @@ const availabilityFailCounter = meter.createCounter( "trpc.hotel.availability-fail" ) -const filterCounter = meter.createCounter("trcp.hotel.filter") -const filterSuccessCounter = meter.createCounter("trcp.hotel.filter-success") -const filterFailCounter = meter.createCounter("trcp.hotel.filter-fail") - async function getContentstackData( locale: string, uid: string | null | undefined @@ -421,9 +417,9 @@ export const hotelQueryRouter = router({ return validatedHotelData.data }), }), - hotel: router({ + hotelData: router({ get: serviceProcedure - .input(getPlaceholderInputSchema) + .input(getlHotelDataInputSchema) .query(async ({ ctx, input }) => { const { hotelId, language, include } = input diff --git a/types/components/hotelReservation/selectHotel/hotelFilters.ts b/types/components/hotelReservation/selectHotel/hotelFilters.ts new file mode 100644 index 000000000..1be84f63d --- /dev/null +++ b/types/components/hotelReservation/selectHotel/hotelFilters.ts @@ -0,0 +1,5 @@ +import { Hotel } from "@/types/hotel" + +export type HotelFiltersProps = { + filters: Hotel["detailedFacilities"] +} From 98b903b314e01a36e13ea3972b76a28be6c0e97a Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 11 Sep 2024 16:19:59 +0200 Subject: [PATCH 268/319] feat(SW-251): add input --- .../(public)/hotelreservation/select-hotel/page.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx index 032ff30f0..634c4cf1c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx @@ -20,12 +20,20 @@ async function getAvailableHotels({ roomStayStartDate, roomStayEndDate, adults, + children, + promotionCode, + attachedProfileId, + reservationProfileType, }: AvailabilityInput): Promise { const getAvailableHotels = await serverClient().hotel.availability.get({ cityId: cityId, roomStayStartDate: roomStayStartDate, roomStayEndDate: roomStayEndDate, adults: adults, + children: children, + promotionCode: promotionCode, + attachedProfileId: attachedProfileId, + reservationProfileType: reservationProfileType, }) if (!getAvailableHotels) return null From 297fdfab5742c087e890827618bda01e4c4c866d Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 11 Sep 2024 16:29:50 +0200 Subject: [PATCH 269/319] feat(SW-251): add error text --- components/HotelReservation/HotelCardListing/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index 2af4f0b9c..a98e0e906 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -18,7 +18,7 @@ export default function HotelCardListing({ hotelData }: HotelCardListingProps) { )) ) : ( - Hallå + No hotels found )}
) From d338fee8a90d46c10375a129dc38ede2839f9e24 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 11 Sep 2024 16:34:08 +0200 Subject: [PATCH 270/319] feat(Sw-251): rename file --- components/HotelReservation/SelectHotel/HotelFilter/index.tsx | 2 +- .../selectHotel/{hotelFilters.ts => hotelFiltersProps.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename types/components/hotelReservation/selectHotel/{hotelFilters.ts => hotelFiltersProps.ts} (100%) diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index a3a993aee..52bf5a7fe 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -4,7 +4,7 @@ import { useIntl } from "react-intl" import styles from "./hotelFilter.module.css" -import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters" +import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFiltersProps" export default function HotelFilter({ filters }: HotelFiltersProps) { const intl = useIntl() diff --git a/types/components/hotelReservation/selectHotel/hotelFilters.ts b/types/components/hotelReservation/selectHotel/hotelFiltersProps.ts similarity index 100% rename from types/components/hotelReservation/selectHotel/hotelFilters.ts rename to types/components/hotelReservation/selectHotel/hotelFiltersProps.ts From 21ff8f8b5d920f71ba93589fc5d2e15785791946 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 11 Sep 2024 16:41:29 +0200 Subject: [PATCH 271/319] feat(SW-251): update logging texts --- server/routers/hotels/output.ts | 2 +- server/routers/hotels/query.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index ff7437b2a..1eba75ea0 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -145,7 +145,7 @@ const hotelContentSchema = z.object({ restaurantsOverviewPageLinkText: z.string(), restaurantsOverviewPageLink: z.string(), restaurantsContentDescriptionShort: z.string(), - restaurantsContentDescriptionMedium: z.string().optional(), + restaurantsContentDescriptionMedium: z.string(), }), }) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 5f8ae5dd9..fe92fa109 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -438,7 +438,7 @@ export const hotelQueryRouter = router({ include, }) console.info( - "api.hotels.hotel start", + "api.hotels.hotelData start", JSON.stringify({ query: { hotelId, params } }) ) @@ -466,7 +466,7 @@ export const hotelQueryRouter = router({ }), }) console.error( - "api.hotels.hotel error", + "api.hotels.hotelData error", JSON.stringify({ query: { hotelId, params }, error: { @@ -492,7 +492,7 @@ export const hotelQueryRouter = router({ }) console.error( - "api.hotels.hotel validation error", + "api.hotels.hotelData validation error", JSON.stringify({ query: { hotelId, params }, error: validateHotelData.error, @@ -507,7 +507,7 @@ export const hotelQueryRouter = router({ include, }) console.info( - "api.hotels.hotel success", + "api.hotels.hotelData success", JSON.stringify({ query: { hotelId, params: params }, }) From 85460e95e57408490ee7d386bf29ff5fb7e895c2 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 12 Sep 2024 13:48:27 +0200 Subject: [PATCH 272/319] feat(SW-251): type assertion --- .../hotelreservation/select-hotel/page.tsx | 18 ++++++++---------- .../HotelReservation/HotelCard/index.tsx | 16 ++++++++-------- .../HotelCardListing/index.tsx | 4 +--- .../SelectHotel/HotelFilter/index.tsx | 13 +++++++++++-- .../selectHotel/hotelCardListingProps.ts | 2 +- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx index 634c4cf1c..4e5f85841 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx @@ -1,5 +1,4 @@ import { serverClient } from "@/lib/trpc/server" -import { notFound } from "@/server/errors/next" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" @@ -24,7 +23,7 @@ async function getAvailableHotels({ promotionCode, attachedProfileId, reservationProfileType, -}: AvailabilityInput): Promise { +}: AvailabilityInput): Promise { const getAvailableHotels = await serverClient().hotel.availability.get({ cityId: cityId, roomStayStartDate: roomStayStartDate, @@ -36,7 +35,7 @@ async function getAvailableHotels({ reservationProfileType: reservationProfileType, }) - if (!getAvailableHotels) return null + if (!getAvailableHotels) throw new Error() const { availability } = getAvailableHotels @@ -46,8 +45,10 @@ async function getAvailableHotels({ language: getLang(), }) + if (!hotelData) throw new Error() + return { - hotelData: hotelData?.data.attributes, + hotelData: hotelData.data.attributes, price: hotel.bestPricePerNight, } }) @@ -70,12 +71,9 @@ export default async function SelectHotelPage({ adults: 1, }) - if (!hotels) return null - if (hotels.some((item) => item?.hotelData === undefined)) return notFound() + const filters = hotels.flatMap((data) => data.hotelData.detailedFacilities) - const filters = hotels.flatMap((data) => data.hotelData?.detailedFacilities) - - const filterId = [...new Set(filters.map((data) => data?.id))] + const filterId = [...new Set(filters.map((data) => data.id))] const filterList: { name: string id: number @@ -86,7 +84,7 @@ export default async function SelectHotelPage({ code?: string iconName?: string }[] = filterId - .map((data) => filters.find((find) => find?.id === data)) + .map((data) => filters.find((find) => find.id === data)) .filter( ( filter diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 83d830515..76d702b98 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -25,7 +25,7 @@ export default function HotelCard({ hotel }: HotelCardProps) { const { hotelData } = hotel const { price } = hotel - const sortedAmenities = hotelData?.detailedFacilities + const sortedAmenities = hotelData.detailedFacilities .sort((a, b) => b.sortOrder - a.sortOrder) .slice(0, 5) @@ -33,8 +33,8 @@ export default function HotelCard({ hotel }: HotelCardProps) {
{hotelData?.hotelContent.images.metaData.altText - {hotelData?.ratings?.tripAdvisor.rating} + {hotelData.ratings?.tripAdvisor.rating}
- {hotelData?.name} + {hotelData.name} - {`${hotelData?.address?.streetAddress}, ${hotelData?.address?.city}`} + {`${hotelData.address.streetAddress}, ${hotelData.address.city}`} - {`${hotelData?.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} + {`${hotelData.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
- {sortedAmenities?.map((facility) => { + {sortedAmenities.map((facility) => { const IconComponent = mapFacilityToIcon(facility.name) return (
diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index a98e0e906..8a3da1dba 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -9,13 +9,11 @@ import styles from "./hotelCardListing.module.css" import { HotelCardListingProps } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" export default function HotelCardListing({ hotelData }: HotelCardListingProps) { - if (!hotelData) return null - return (
{hotelData && hotelData.length ? ( hotelData.map((hotel) => ( - + )) ) : ( No hotels found diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index 52bf5a7fe..f10191cec 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -9,6 +9,10 @@ import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHot export default function HotelFilter({ filters }: HotelFiltersProps) { const intl = useIntl() + function handleOnChange() { + // TODO: Update URL with selected values + } + return (
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 290/319] 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 292/319] 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 293/319] 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 294/319] 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 From 2849c69c522dcccd4412ea902a9fd0f0fa0fa842 Mon Sep 17 00:00:00 2001 From: Arvid Norlin Date: Tue, 3 Sep 2024 15:35:06 +0200 Subject: [PATCH 295/319] refactor: Move Sidepeek param logic to SidePeekProvider --- .../HotelPage/IntroSection/index.tsx | 1 + .../ContentType/HotelPage/SidePeeks.tsx | 99 ----------------- components/ContentType/HotelPage/index.tsx | 55 ++++++++- components/SidePeekProvider/index.tsx | 45 ++++++++ components/TempDesignSystem/Link/index.tsx | 2 +- .../TempDesignSystem/SidePeek/Item/index.tsx | 39 ------- .../SidePeek/Item/sidePeekItem.module.css | 27 ----- .../TempDesignSystem/SidePeek/index.tsx | 75 +++++++++---- .../SidePeek/sidePeek.module.css | 105 ++++++++++++------ .../TempDesignSystem/SidePeek/sidePeek.ts | 6 +- components/TempDesignSystem/SidePeek/types.ts | 4 +- 11 files changed, 227 insertions(+), 231 deletions(-) delete mode 100644 components/ContentType/HotelPage/SidePeeks.tsx create mode 100644 components/SidePeekProvider/index.tsx delete mode 100644 components/TempDesignSystem/SidePeek/Item/index.tsx delete mode 100644 components/TempDesignSystem/SidePeek/Item/sidePeekItem.module.css diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index d20486c60..cb62ca01c 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -73,6 +73,7 @@ export default async function IntroSection({ color="burgundy" variant="icon" href={`?s=${about[lang]}`} + scroll={false} > {intl.formatMessage({ id: "Read more about the hotel" })} diff --git a/components/ContentType/HotelPage/SidePeeks.tsx b/components/ContentType/HotelPage/SidePeeks.tsx deleted file mode 100644 index 2fcf9b46f..000000000 --- a/components/ContentType/HotelPage/SidePeeks.tsx +++ /dev/null @@ -1,99 +0,0 @@ -"use client" - -import { usePathname, useRouter, useSearchParams } from "next/navigation" -import { useEffect, useState } from "react" -import { useIntl } from "react-intl" - -import { - about, - activities, - amenities, - meetingsAndConferences, - restaurantAndBar, - wellnessAndExercise, -} from "@/constants/routes/hotelPageParams" - -import SidePeek from "@/components/TempDesignSystem/SidePeek" -import SidePeekItem from "@/components/TempDesignSystem/SidePeek/Item" -import { SidePeekContentKey } from "@/components/TempDesignSystem/SidePeek/types" -import useLang from "@/hooks/useLang" - -function SidePeekContainer() { - const router = useRouter() - const pathname = usePathname() - const searchParams = useSearchParams() - const [activeSidePeek, setActiveSidePeek] = - useState(() => { - const sidePeekParam = searchParams.get("s") as SidePeekContentKey | null - return sidePeekParam || null - }) - - const lang = useLang() - const intl = useIntl() - - useEffect(() => { - const sidePeekParam = searchParams.get("s") as SidePeekContentKey | null - if (sidePeekParam !== activeSidePeek) { - setActiveSidePeek(sidePeekParam) - } - }, [searchParams, activeSidePeek]) - - function handleClose(isOpen: boolean) { - if (!isOpen) { - setActiveSidePeek(null) - - const nextSearchParams = new URLSearchParams(searchParams.toString()) - nextSearchParams.delete("s") - - router.push(`${pathname}?${nextSearchParams}`, { scroll: false }) - } - } - - return ( - - - {/* TODO: Render amenities as per the design. */} - Read more about the amenities here - - - Some additional information about the hotel - - - {/* TODO */} - Restaurant & Bar - - - {/* TODO */} - Wellness & Exercise - - - {/* TODO */} - Activities - - - {/* TODO */} - Meetings & Conferences - - - ) -} - -export default SidePeekContainer diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index f37750db4..210bf9b7c 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -1,6 +1,12 @@ +import hotelPageParams from "@/constants/routes/hotelPageParams" import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" +import SidePeekProvider from "@/components/SidePeekProvider" +import SidePeek from "@/components/TempDesignSystem/SidePeek" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" + import { MOCK_FACILITIES } from "./Facilities/mockData" import { setActivityCard } from "./Facilities/utils" import DynamicMap from "./Map/DynamicMap" @@ -12,13 +18,14 @@ import Facilities from "./Facilities" import IntroSection from "./IntroSection" import PreviewImages from "./PreviewImages" import { Rooms } from "./Rooms" -import SidePeeks from "./SidePeeks" import TabNavigation from "./TabNavigation" import styles from "./hotelPage.module.css" export default async function HotelPage() { const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY + const intl = await getIntl() + const lang = getLang() const hotelData = await serverClient().hotel.get({ include: ["RoomCategories"], }) @@ -61,6 +68,51 @@ export default async function HotelPage() { address={hotelAddress} tripAdvisor={hotelRatings?.tripAdvisor} /> + + {/* eslint-disable import/no-named-as-default-member */} + + {/* TODO: Render amenities as per the design. */} + Read more about the amenities here + + + Some additional information about the hotel + + + {/* TODO */} + Restaurant & Bar + + + {/* TODO */} + Wellness & Exercise + + + {/* TODO */} + Activities + + + {/* TODO */} + Meetings & Conferences + + {/* eslint-enable import/no-named-as-default-member */} +
@@ -80,7 +132,6 @@ export default async function HotelPage() { /> ) : null} -
) } diff --git a/components/SidePeekProvider/index.tsx b/components/SidePeekProvider/index.tsx new file mode 100644 index 000000000..dad577b55 --- /dev/null +++ b/components/SidePeekProvider/index.tsx @@ -0,0 +1,45 @@ +"use client" +import { usePathname, useRouter, useSearchParams } from "next/navigation" +import { createContext, useEffect, useState } from "react" + +interface ISidePeekContext { + handleClose: (isOpen: boolean) => void + activeSidePeek: string | null +} + +export const SidePeekContext = createContext(null) + +function SidePeekProvider({ children }: React.PropsWithChildren) { + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + const [activeSidePeek, setActiveSidePeek] = useState(() => { + const sidePeekParam = searchParams.get("s") + return sidePeekParam || null + }) + + useEffect(() => { + const sidePeekParam = searchParams.get("s") + if (sidePeekParam !== activeSidePeek) { + setActiveSidePeek(sidePeekParam) + } + }, [searchParams, activeSidePeek]) + + function handleClose(isOpen: boolean) { + if (!isOpen) { + const nextSearchParams = new URLSearchParams(searchParams.toString()) + nextSearchParams.delete("s") + + router.push(`${pathname}?${nextSearchParams}`, { scroll: false }) + setActiveSidePeek(null) + } + } + + return ( + + {children} + + ) +} + +export default SidePeekProvider diff --git a/components/TempDesignSystem/Link/index.tsx b/components/TempDesignSystem/Link/index.tsx index 6856f87aa..23cf15e46 100644 --- a/components/TempDesignSystem/Link/index.tsx +++ b/components/TempDesignSystem/Link/index.tsx @@ -75,7 +75,7 @@ export default function Link({ trackPageViewStart() startTransition(() => { startRouterTransition() - router.push(href) + router.push(href, { scroll }) }) }} href={href} diff --git a/components/TempDesignSystem/SidePeek/Item/index.tsx b/components/TempDesignSystem/SidePeek/Item/index.tsx deleted file mode 100644 index fa897aae9..000000000 --- a/components/TempDesignSystem/SidePeek/Item/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client" - -import { PropsWithChildren } from "react" - -import { CloseIcon } from "@/components/Icons" -import { SidePeekContentProps } from "@/components/TempDesignSystem/SidePeek/types" -import Title from "@/components/TempDesignSystem/Text/Title" - -import Button from "../../Button" - -import styles from "./sidePeekItem.module.css" - -function SidePeekItem({ - title, - children, - isActive = false, - onClose, -}: PropsWithChildren) { - return isActive ? ( - - ) : null -} - -export default SidePeekItem \ No newline at end of file diff --git a/components/TempDesignSystem/SidePeek/Item/sidePeekItem.module.css b/components/TempDesignSystem/SidePeek/Item/sidePeekItem.module.css deleted file mode 100644 index eb90ed60b..000000000 --- a/components/TempDesignSystem/SidePeek/Item/sidePeekItem.module.css +++ /dev/null @@ -1,27 +0,0 @@ -.sidePeekItem { - display: grid; - grid-template-rows: min-content auto; - gap: var(--Spacing-x4); - height: 100%; -} - -.content>* { - padding: var(--Spacing-x3) var(--Spacing-x2); -} - -.header { - display: flex; - justify-content: flex-end; - border-bottom: 1px solid var(--Base-Border-Subtle); - align-items: center; -} - -.header:has(> h2) { - justify-content: space-between; -} - -@media screen and (min-width: 1367px) { - .content>* { - padding: var(--Spacing-x4); - } -} \ No newline at end of file diff --git a/components/TempDesignSystem/SidePeek/index.tsx b/components/TempDesignSystem/SidePeek/index.tsx index 271ad30e5..df0b8dc44 100644 --- a/components/TempDesignSystem/SidePeek/index.tsx +++ b/components/TempDesignSystem/SidePeek/index.tsx @@ -1,15 +1,20 @@ "use client" import { useIsSSR } from "@react-aria/ssr" -import React, { Children, cloneElement } from "react" +import { useContext } from "react" import { Dialog, DialogTrigger, Modal, ModalOverlay, } from "react-aria-components" +import { useIntl } from "react-intl" -import { SidePeekContentKey } from "@/components/TempDesignSystem/SidePeek/types" +import { CloseIcon } from "@/components/Icons" +import { SidePeekContext } from "@/components/SidePeekProvider" + +import Button from "../Button" +import Title from "../Text/Title" import styles from "./sidePeek.module.css" @@ -17,33 +22,61 @@ import type { SidePeekProps } from "./sidePeek" function SidePeek({ children, + title, + contentKey, handleClose, - activeSidePeek, + isOpen, }: React.PropsWithChildren) { - const sidePeekChildren = Children.map(children, (child) => { - if (!React.isValidElement(child)) { - return child - } - return cloneElement(child as React.ReactElement, { - isActive: - (child.props.contentKey as SidePeekContentKey) === activeSidePeek, - onClose: handleClose, - }) - }) - const isSSR = useIsSSR() - return isSSR ? ( -
{children}
- ) : ( + const intl = useIntl() + const context = useContext(SidePeekContext) + function onClose() { + const closeHandler = handleClose || context?.handleClose + closeHandler && closeHandler(false) + } + + if (isSSR) { + return ( +
+

{title}

+ {children} +
+ ) + } + return ( - - {sidePeekChildren} + + + + diff --git a/components/TempDesignSystem/SidePeek/sidePeek.module.css b/components/TempDesignSystem/SidePeek/sidePeek.module.css index 0ed9304c5..2d6de4f00 100644 --- a/components/TempDesignSystem/SidePeek/sidePeek.module.css +++ b/components/TempDesignSystem/SidePeek/sidePeek.module.css @@ -1,38 +1,9 @@ -.sidePeek { - position: fixed; - top: var(--current-mobile-site-header-height); - right: auto; - bottom: 0; - width: 100%; - height: calc(100vh - var(--current-mobile-site-header-height)); - background-color: var(--Base-Background-Primary-Normal); - z-index: 100; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.85); -} - -.sidePeek[data-entering] { - animation: slide-up 300ms; -} - -.sidePeek[data-exiting] { - animation: slide-up 300ms reverse; -} - -.dialog { - height: 100%; -} - -.overlay { - position: absolute; - top: var(--current-mobile-site-header-height); - bottom: 0; - left: 0; - right: 0; - z-index: 99; +.modal { + --sidepeek-desktop-width: 600px; } @keyframes slide-in { from { - right: -600px; + right: calc(-1 * var(--sidepeek-desktop-width)); } to { @@ -46,24 +17,84 @@ } to { - top: var(--current-mobile-site-header-height); + top: 0; } } +.overlay { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 99; +} + +.modal { + position: fixed; + top: 0; + right: auto; + bottom: 0; + width: 100%; + height: 100vh; + background-color: var(--Base-Background-Primary-Normal); + z-index: 100; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.85); +} + +.modal[data-entering] { + animation: slide-up 300ms; +} + +.modal[data-exiting] { + animation: slide-up 300ms reverse; +} + +.dialog { + height: 100%; +} + +.sidePeek { + display: grid; + grid-template-rows: min-content auto; + height: 100%; +} + +.header { + display: flex; + justify-content: flex-end; + border-bottom: 1px solid var(--Base-Border-Subtle); + align-items: center; + padding: var(--Spacing-x4); +} + +.header:has(> h2) { + justify-content: space-between; +} + +.closeButton { + padding: 0; +} + +.sidePeekContent { + padding: var(--Spacing-x4); +} @media screen and (min-width: 1367px) { - .sidePeek { + .modal { top: 0; right: 0px; - width: 600px; + width: var(--sidepeek-desktop-width); height: 100vh; } - .sidePeek[data-entering] { + + .modal[data-entering] { animation: slide-in 250ms; } - .sidePeek[data-exiting] { + .modal[data-exiting] { animation: slide-in 250ms reverse; } + .overlay { top: 0; } diff --git a/components/TempDesignSystem/SidePeek/sidePeek.ts b/components/TempDesignSystem/SidePeek/sidePeek.ts index 626fc640c..e1781f137 100644 --- a/components/TempDesignSystem/SidePeek/sidePeek.ts +++ b/components/TempDesignSystem/SidePeek/sidePeek.ts @@ -1,4 +1,6 @@ export interface SidePeekProps { - handleClose: (isOpen: boolean) => void - activeSidePeek: string | null + contentKey: string + title: string + isOpen?: boolean + handleClose?: (isOpen: boolean) => void } diff --git a/components/TempDesignSystem/SidePeek/types.ts b/components/TempDesignSystem/SidePeek/types.ts index f506fc6bf..e37554aff 100644 --- a/components/TempDesignSystem/SidePeek/types.ts +++ b/components/TempDesignSystem/SidePeek/types.ts @@ -1,5 +1,3 @@ -export type SidePeekContentKey = string - export type SidePeekProps = { activeContent: string | null onClose: (isOpen: boolean) => void @@ -7,7 +5,7 @@ export type SidePeekProps = { export type SidePeekContentProps = { title?: string - contentKey: SidePeekContentKey + contentKey: string isActive?: boolean onClose?: () => void } From a6222255d40d236ee988a250a689cb6b33ced5a9 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Tue, 17 Sep 2024 08:09:29 +0200 Subject: [PATCH 296/319] PR fixes --- app/[lang]/(live)/@header/page.tsx | 2 +- app/[lang]/(live)/layout.tsx | 1 - .../{MobileMenuServer => MobileMenuWrapper}/index.tsx | 3 +-- .../{MyPagesMenuServer => MyPagesMenuWrapper}/index.tsx | 3 +-- .../MainMenu/NavigationMenu/NavigationMenuList/index.tsx | 8 ++++++-- components/Header/MainMenu/index.tsx | 8 +++----- components/Header/TopMenu/index.tsx | 6 +----- components/Header/index.tsx | 2 +- types/components/header/mainMenu.ts | 1 - types/components/header/topMenu.ts | 1 - 10 files changed, 14 insertions(+), 21 deletions(-) rename components/Header/MainMenu/{MobileMenuServer => MobileMenuWrapper}/index.tsx (85%) rename components/Header/MainMenu/{MyPagesMenuServer => MyPagesMenuWrapper}/index.tsx (95%) delete mode 100644 types/components/header/mainMenu.ts delete mode 100644 types/components/header/topMenu.ts diff --git a/app/[lang]/(live)/@header/page.tsx b/app/[lang]/(live)/@header/page.tsx index adccd9484..7f01d8eb5 100644 --- a/app/[lang]/(live)/@header/page.tsx +++ b/app/[lang]/(live)/@header/page.tsx @@ -3,7 +3,7 @@ import { setLang } from "@/i18n/serverContext" import { LangParams, PageArgs } from "@/types/params" -export default async function HeaderPage({ params }: PageArgs) { +export default function HeaderPage({ params }: PageArgs) { setLang(params.lang) return
diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index a1d7e0408..33ba651d4 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -10,7 +10,6 @@ import TokenRefresher from "@/components/Auth/TokenRefresher" import AdobeSDKScript from "@/components/Current/AdobeSDKScript" import VwoScript from "@/components/Current/VwoScript" import Footer from "@/components/Footer" -import Header from "@/components/Header" import LoadingSpinner from "@/components/LoadingSpinner" import { ToastHandler } from "@/components/TempDesignSystem/Toasts" import { preloadUserTracking } from "@/components/TrackingSDK" diff --git a/components/Header/MainMenu/MobileMenuServer/index.tsx b/components/Header/MainMenu/MobileMenuWrapper/index.tsx similarity index 85% rename from components/Header/MainMenu/MobileMenuServer/index.tsx rename to components/Header/MainMenu/MobileMenuWrapper/index.tsx index 1932603f2..b61b086fd 100644 --- a/components/Header/MainMenu/MobileMenuServer/index.tsx +++ b/components/Header/MainMenu/MobileMenuWrapper/index.tsx @@ -1,9 +1,8 @@ -"use server" import { serverClient } from "@/lib/trpc/server" import MobileMenu from "../MobileMenu" -export default async function MobileMenuServer({}) { +export default async function MobileMenuServer() { const [languages, headerData] = await Promise.all([ serverClient().contentstack.languageSwitcher.get(), serverClient().contentstack.base.header(), diff --git a/components/Header/MainMenu/MyPagesMenuServer/index.tsx b/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx similarity index 95% rename from components/Header/MainMenu/MyPagesMenuServer/index.tsx rename to components/Header/MainMenu/MyPagesMenuWrapper/index.tsx index eaf5440c8..da941389a 100644 --- a/components/Header/MainMenu/MyPagesMenuServer/index.tsx +++ b/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx @@ -1,4 +1,3 @@ -"use server" import { Link } from "react-feather" import { myPages } from "@/constants/routes/myPages" @@ -13,7 +12,7 @@ import MyPagesMobileMenu from "../MyPagesMobileMenu" import styles from "../mainMenu.module.css" -export default async function MyPagesMenuServer({}) { +export default async function MyPagesMenuServer() { const lang = getLang() const [intl, myPagesNavigation, user, membership] = await Promise.all([ getIntl(), diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx b/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx index 60e4a5c7e..c59797c6c 100644 --- a/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx @@ -1,4 +1,8 @@ "use client" +import { z } from "zod" + +import { getHeaderSchema } from "@/server/routers/contentstack/base/output" + import NavigationMenuItem from "../NavigationMenuItem" import styles from "../navigationMenu.module.css" @@ -8,13 +12,13 @@ export default function NavigationMenuList({ items, }: { isMobile: boolean - items: any + items: z.infer["menuItems"] }) { return (
    - {items.map((item: any) => ( + {items.map((item) => (
  • diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx index 2727fd16d..f923a00af 100644 --- a/components/Header/MainMenu/index.tsx +++ b/components/Header/MainMenu/index.tsx @@ -5,15 +5,13 @@ import Image from "@/components/Image" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import MobileMenuServer from "./MobileMenuServer" -import MyPagesMenuServer from "./MyPagesMenuServer" +import MobileMenuServer from "./MobileMenuWrapper" +import MyPagesMenuServer from "./MyPagesMenuWrapper" import NavigationMenu from "./NavigationMenu" import styles from "./mainMenu.module.css" -import type { MainMenuProps } from "@/types/components/header/mainMenu" - -export default async function MainMenu({}: MainMenuProps) { +export default async function MainMenu() { const lang = getLang() const intl = await getIntl() diff --git a/components/Header/TopMenu/index.tsx b/components/Header/TopMenu/index.tsx index adf8ca96d..ee676bbf0 100644 --- a/components/Header/TopMenu/index.tsx +++ b/components/Header/TopMenu/index.tsx @@ -1,5 +1,3 @@ -import { Suspense } from "react" - import { serverClient } from "@/lib/trpc/server" import { GiftIcon, SearchIcon } from "@/components/Icons" @@ -10,9 +8,7 @@ import HeaderLink from "../HeaderLink" import styles from "./topMenu.module.css" -import type { TopMenuProps } from "@/types/components/header/topMenu" - -export default async function TopMenu({}: TopMenuProps) { +export default async function TopMenu() { const [intl, languages, headerData] = await Promise.all([ getIntl(), serverClient().contentstack.languageSwitcher.get(), diff --git a/components/Header/index.tsx b/components/Header/index.tsx index 2bb580c2b..026cc72f0 100644 --- a/components/Header/index.tsx +++ b/components/Header/index.tsx @@ -5,7 +5,7 @@ import TopMenu from "./TopMenu" import styles from "./header.module.css" -export default async function Header({}) { +export default function Header({}) { return (
    diff --git a/types/components/header/mainMenu.ts b/types/components/header/mainMenu.ts deleted file mode 100644 index 8053fc40f..000000000 --- a/types/components/header/mainMenu.ts +++ /dev/null @@ -1 +0,0 @@ -export interface MainMenuProps {} diff --git a/types/components/header/topMenu.ts b/types/components/header/topMenu.ts deleted file mode 100644 index f5ef829e6..000000000 --- a/types/components/header/topMenu.ts +++ /dev/null @@ -1 +0,0 @@ -export interface TopMenuProps {} From b8f37c810d214c7c801bf1c9751ccb590d1681eb Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Tue, 17 Sep 2024 14:00:21 +0200 Subject: [PATCH 297/319] Turn off netlify-plugin-cypress --- netlify.toml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/netlify.toml b/netlify.toml index ce2835057..022eee2ee 100644 --- a/netlify.toml +++ b/netlify.toml @@ -8,15 +8,15 @@ command = "npm run lint && npm run build" [context.deploy-preview] command = "npm run lint && npm run build" -[[plugins]] -package = "netlify-plugin-cypress" -[plugins.inputs] -configFile = "cypress.config.ts" -[plugins.inputs.postBuild] -enable = true -start = "npm start" -wait-on = "http://127.0.0.1:3000/en/sponsoring" -wait-on-timeout = "30" # seconds +# [[plugins]] +# package = "netlify-plugin-cypress" +# [plugins.inputs] +# configFile = "cypress.config.ts" +# [plugins.inputs.postBuild] +# enable = true +# start = "npm start" +# wait-on = "http://127.0.0.1:3000/en/sponsoring" +# wait-on-timeout = "30" # seconds [build.environment] # cache Cypress binary in local "node_modules" folder From 1729f4b9c716e7691d7d1e96d1b5f7f26d1a5941 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 18 Sep 2024 09:48:31 +0200 Subject: [PATCH 298/319] fix: hide tier nights --- .../MyPages/Blocks/Overview/Stats/Points/index.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/components/MyPages/Blocks/Overview/Stats/Points/index.tsx b/components/MyPages/Blocks/Overview/Stats/Points/index.tsx index 0b3e450d7..09784bfe1 100644 --- a/components/MyPages/Blocks/Overview/Stats/Points/index.tsx +++ b/components/MyPages/Blocks/Overview/Stats/Points/index.tsx @@ -6,11 +6,7 @@ import { getMembershipLevelObject } from "@/utils/membershipLevel" import { getMembership } from "@/utils/user" import PointsContainer from "./Container" -import { - NextLevelNightsColumn, - NextLevelPointsColumn, - YourPointsColumn, -} from "./PointsColumn" +import { NextLevelPointsColumn, YourPointsColumn } from "./PointsColumn" import { UserProps } from "@/types/components/myPages/user" @@ -32,7 +28,8 @@ export default async function Points({ user }: UserProps) { subtitle={`${formatMessage({ id: "next level:" })} ${nextLevel.name}`} /> )} - {membership?.nightsToTopTier && ( + {/* TODO: Show NextLevelNightsColumn when nightsToTopTier data is correct from Antavo */} + {/* {membership?.nightsToTopTier && ( - )} + )} */} ) } From efb8d278cd8223bc3b3d847fc88894fa7b77a565 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 18 Sep 2024 12:41:40 +0200 Subject: [PATCH 299/319] fix(sw-398): moved logic for disable scroll in mobile and fixed issue that page is scrolling when toggling languageswitcher --- app/globals.css | 11 ++++++++ components/LanguageSwitcher/index.tsx | 36 +++++++++++++-------------- server/trpc.ts | 2 +- stores/main-menu.ts | 5 ++++ 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/app/globals.css b/app/globals.css index 67624cbcf..5ec453db0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -123,6 +123,7 @@ html, body { margin: 0; padding: 0; + scroll-behavior: smooth; } body { @@ -130,6 +131,16 @@ body { overflow-x: hidden; } +body.overflow-hidden { + overflow: hidden; +} +@media screen and (min-width: 768px) { + body.overflow-hidden { + overflow: auto; + overflow-x: hidden; + } +} + ul { padding-inline-start: 0; margin-block-start: 0; diff --git a/components/LanguageSwitcher/index.tsx b/components/LanguageSwitcher/index.tsx index 7a5ecb6b2..3d0543051 100644 --- a/components/LanguageSwitcher/index.tsx +++ b/components/LanguageSwitcher/index.tsx @@ -1,6 +1,5 @@ "use client" -import { useEffect } from "react" import { useIntl } from "react-intl" import { languages } from "@/constants/languages" @@ -28,12 +27,16 @@ export default function LanguageSwitcher({ }: LanguageSwitcherProps) { const intl = useIntl() const currentLanguage = useLang() - const { - toggleDropdown, - isFooterLanguageSwitcherOpen, - isHeaderLanguageSwitcherOpen, - isHeaderLanguageSwitcherMobileOpen, - } = useDropdownStore() + const toggleDropdown = useDropdownStore((state) => state.toggleDropdown) + const isFooterLanguageSwitcherOpen = useDropdownStore( + (state) => state.isFooterLanguageSwitcherOpen + ) + const isHeaderLanguageSwitcherOpen = useDropdownStore( + (state) => state.isHeaderLanguageSwitcherOpen + ) + const isHeaderLanguageSwitcherMobileOpen = useDropdownStore( + (state) => state.isHeaderLanguageSwitcherMobileOpen + ) const isFooter = type === LanguageSwitcherTypesEnum.Footer const isHeader = !isFooter @@ -58,17 +61,14 @@ export default function LanguageSwitcher({ } }) - useEffect(() => { - if (isFooter && isFooterLanguageSwitcherOpen) { - document.body.style.overflow = "hidden" - } else { - document.body.style.overflow = "" - } + function handleClick() { + const scrollPosition = window.scrollY + toggleDropdown(dropdownType) - return () => { - document.body.style.overflow = "" - } - }, [isFooter, isFooterLanguageSwitcherOpen]) + requestAnimationFrame(() => { + window.scrollTo(0, scrollPosition) + }) + } const classNames = languageSwitcherVariants({ color, position }) @@ -82,7 +82,7 @@ export default function LanguageSwitcher({ ? "Close language menu" : "Open language menu", })} - onClick={() => toggleDropdown(dropdownType)} + onClick={handleClick} > {languages[currentLanguage]} diff --git a/server/trpc.ts b/server/trpc.ts index 7a7a7f7ea..e3085f216 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -4,13 +4,13 @@ import { ZodError } from "zod" import { env } from "@/env/server" -import { type Context, createContext } from "./context" import { badRequestError, internalServerError, sessionExpiredError, unauthorizedError, } from "./errors/trpc" +import { type Context, createContext } from "./context" import { fetchServiceToken } from "./tokenManager" import { transformer } from "./transformer" diff --git a/stores/main-menu.ts b/stores/main-menu.ts index 29e039e99..4ccbcb290 100644 --- a/stores/main-menu.ts +++ b/stores/main-menu.ts @@ -85,6 +85,11 @@ const useDropdownStore = create((set, get) => ({ state.isMyPagesMenuOpen = false state.isHeaderLanguageSwitcherOpen = false state.isHeaderLanguageSwitcherMobileOpen = false + if (state.isFooterLanguageSwitcherOpen) { + document.body.classList.add("overflow-hidden") + } else { + document.body.classList.remove("overflow-hidden") + } break } }) From e0e0ef2712b97c177ecbe9e5580e11aaacace739 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 18 Sep 2024 15:12:11 +0200 Subject: [PATCH 300/319] feat(SW-213): Created Text Cols component and added to Blocks in Contentstack --- components/Content/Blocks/TextCols/index.tsx | 26 ++++++++++++++ .../Blocks/TextCols/textcols.module.css | 36 +++++++++++++++++++ components/Content/Blocks/index.tsx | 3 ++ lib/graphql/Query/ContentPage.graphql | 22 ++++++++++++ .../contentstack/contentPage/output.ts | 22 +++++++++--- types/components/content/blocks.ts | 3 ++ types/components/content/enums.ts | 1 + .../trpc/routers/contentstack/contentPage.ts | 21 ++++++++++- 8 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 components/Content/Blocks/TextCols/index.tsx create mode 100644 components/Content/Blocks/TextCols/textcols.module.css diff --git a/components/Content/Blocks/TextCols/index.tsx b/components/Content/Blocks/TextCols/index.tsx new file mode 100644 index 000000000..4777c496a --- /dev/null +++ b/components/Content/Blocks/TextCols/index.tsx @@ -0,0 +1,26 @@ +import JsonToHtml from "@/components/JsonToHtml" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import styles from "./textcols.module.css" + +import { TextColsProps } from "@/types/components/content/blocks" + +export default function TextCols({ text_cols }: TextColsProps) { + return ( +
    + {text_cols.columns.map((col) => { + return ( +
    + {col.title} +
    + +
    +
    + ) + })} +
    + ) +} diff --git a/components/Content/Blocks/TextCols/textcols.module.css b/components/Content/Blocks/TextCols/textcols.module.css new file mode 100644 index 000000000..d9fd60a1c --- /dev/null +++ b/components/Content/Blocks/TextCols/textcols.module.css @@ -0,0 +1,36 @@ +.columns { + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); + padding: var(--Spacing-x3) var(--Spacing-x4); + background-color: var(--UI-Opacity-White-100); +} + +.column { + padding-bottom: var(--Spacing-x2); + border-bottom: 1px solid var(--Base-Border-Subtle); + gap: var(--Spacing-x1); + display: flex; + flex-direction: column; +} + +.text p { + color: var(--UI-Text-High-contrast); + line-height: var(--Spacing-x3); +} + +.text section { + gap: 0; +} + +@media (min-width: 768px) { + .columns { + flex-direction: row; + flex-wrap: wrap; + } + + .column { + flex: 0 0 calc(50% - var(--Spacing-x3)); + max-width: calc(50% - var(--Spacing-x3)); + } +} diff --git a/components/Content/Blocks/index.tsx b/components/Content/Blocks/index.tsx index 9c396ed11..fc162f590 100644 --- a/components/Content/Blocks/index.tsx +++ b/components/Content/Blocks/index.tsx @@ -3,6 +3,7 @@ import JsonToHtml from "@/components/JsonToHtml" import Shortcuts from "@/components/MyPages/Blocks/Shortcuts" import CardsGrid from "./CardsGrid" +import TextCols from "./TextCols" import type { BlocksProps } from "@/types/components/content/blocks" import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" @@ -38,6 +39,8 @@ export function Blocks({ blocks }: BlocksProps) { firstItem={firstItem} /> ) + case ContentBlocksTypenameEnum.ContentPageBlocksTextCols: + return default: return null } diff --git a/lib/graphql/Query/ContentPage.graphql b/lib/graphql/Query/ContentPage.graphql index dec4b0d6f..e84612c0c 100644 --- a/lib/graphql/Query/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage.graphql @@ -98,6 +98,28 @@ query GetContentPage($locale: String!, $uid: String!) { } } } + ... on ContentPageBlocksTextCols { + __typename + text_cols { + columns { + title + text { + json + embedded_itemsConnection { + edges { + node { + __typename + ...LoyaltyPageLink + ...ContentPageLink + ...HotelPageLink + } + } + totalCount + } + } + } + } + } } title header { diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 1aa324041..21d7b1708 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -12,12 +12,8 @@ import { SidebarDynamicComponentEnum, SidebarTypenameEnum, } from "@/types/components/content/enums" -import { ImageVaultAsset } from "@/types/components/imageVault" -import { Embeds } from "@/types/requests/embeds" import { PageLinkEnum } from "@/types/requests/pageLinks" import { RTEEmbedsEnum } from "@/types/requests/rte" -import { EdgesWithTotalCount } from "@/types/requests/utils/edges" -import { RTEDocument } from "@/types/rte/node" // Block schemas export const contentPageBlockTextContent = z.object({ @@ -135,11 +131,29 @@ export const contentPageCards = z.object({ }), }) +export const contentPageTextCols = z.object({ + __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksTextCols), + text_cols: z.object({ + columns: z.array( + z.object({ + title: z.string(), + text: z.object({ + json: z.any(), + embedded_itemsConnection: z.object({ + edges: z.array(z.any()), + totalCount: z.number(), + }), + }), + }) + ), + }), +}) const contentPageBlockItem = z.discriminatedUnion("__typename", [ contentPageBlockTextContent, contentPageCards, contentPageDynamicContent, contentPageShortcuts, + contentPageTextCols, ]) export const contentPageSidebarTextContent = z.object({ diff --git a/types/components/content/blocks.ts b/types/components/content/blocks.ts index a97ac9780..a9f27bafb 100644 --- a/types/components/content/blocks.ts +++ b/types/components/content/blocks.ts @@ -7,6 +7,7 @@ import { Block, CardsGrid, DynamicContent, + TextCols, } from "@/types/trpc/routers/contentstack/contentPage" export type BlocksProps = { @@ -17,6 +18,8 @@ export type CardsGridProps = Pick & { firstItem?: boolean } +export type TextColsProps = Pick + export type DynamicContentProps = { dynamicContent: DynamicContent["dynamic_content"] firstItem: boolean diff --git a/types/components/content/enums.ts b/types/components/content/enums.ts index 3bc20ac48..cafb608fa 100644 --- a/types/components/content/enums.ts +++ b/types/components/content/enums.ts @@ -6,6 +6,7 @@ export enum ContentBlocksTypenameEnum { ContentPageBlocksShortcuts = "ContentPageBlocksShortcuts", ContentPageBlocksCardsGrid = "ContentPageBlocksCardsGrid", ContentPageBlocksDynamicContent = "ContentPageBlocksDynamicContent", + ContentPageBlocksTextCols = "ContentPageBlocksTextCols", } export enum CardsGridEnum { diff --git a/types/trpc/routers/contentstack/contentPage.ts b/types/trpc/routers/contentstack/contentPage.ts index 4c3f27ce6..35bb9118f 100644 --- a/types/trpc/routers/contentstack/contentPage.ts +++ b/types/trpc/routers/contentstack/contentPage.ts @@ -9,6 +9,7 @@ import { contentPageShortcuts, contentPageSidebarDynamicContent, contentPageSidebarTextContent, + contentPageTextCols, loyaltyCardBlock, validateContentPageRefsSchema, validateContentPageSchema, @@ -81,4 +82,22 @@ export type CardsGrid = Omit & { } export type CardsRaw = CardsGrid["cards_grid"]["cards"][number] -export type Block = RteBlockContent | Shortcuts | CardsGrid | DynamicContent +type TextColsRaw = z.infer +export interface TextCols extends TextColsRaw { + text_cols: { + columns: { + title: string + text: { + json: RTEDocument + embedded_itemsConnection: EdgesWithTotalCount + } + }[] + } +} + +export type Block = + | RteBlockContent + | Shortcuts + | CardsGrid + | DynamicContent + | TextCols From dede0cebb37e7bbfea9d6aedbf82708c4d8a3cec Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 18 Sep 2024 15:14:01 +0200 Subject: [PATCH 301/319] feat(sw-213): added type --- components/Content/Blocks/TextCols/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Content/Blocks/TextCols/index.tsx b/components/Content/Blocks/TextCols/index.tsx index 4777c496a..8fd79c478 100644 --- a/components/Content/Blocks/TextCols/index.tsx +++ b/components/Content/Blocks/TextCols/index.tsx @@ -3,7 +3,7 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./textcols.module.css" -import { TextColsProps } from "@/types/components/content/blocks" +import type { TextColsProps } from "@/types/components/content/blocks" export default function TextCols({ text_cols }: TextColsProps) { return ( From e79f413003588c77cd70a34548a796aa91dd9156 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 17 Sep 2024 16:13:22 +0200 Subject: [PATCH 302/319] feat(SW-325): added pois to the list and dynamic map --- .../HotelPage/Map/DynamicMap/Content.tsx | 147 ------------------ .../HotelPage/Map/DynamicMap/Map/index.tsx | 137 ++++++++++++++++ .../Map/DynamicMap/Map/map.module.css | 108 +++++++++++++ .../Map/DynamicMap/Sidebar/index.tsx | 102 ++++++++++++ .../Map/DynamicMap/Sidebar/sidebar.module.css | 111 +++++++++++++ .../Map/DynamicMap/dynamicMap.module.css | 136 ---------------- .../HotelPage/Map/DynamicMap/index.tsx | 18 ++- .../HotelPage/Map/MapCard/index.tsx | 13 +- .../HotelPage/Map/MapCard/mapCard.module.css | 14 ++ .../MobileMapToggle/mobileToggle.module.css | 2 +- .../HotelPage/Map/StaticMap/index.tsx | 3 +- components/ContentType/HotelPage/index.tsx | 5 +- components/Icons/Cultural.tsx | 40 +++++ components/Icons/Museum.tsx | 36 +++++ components/Icons/Shopping.tsx | 40 +++++ components/Icons/StarFilled.tsx | 40 +++++ components/Icons/Train.tsx | 36 +++++ components/Icons/get-icon-by-icon-name.ts | 15 ++ components/Icons/index.tsx | 5 + components/Maps/Markers/Poi/index.tsx | 27 ++++ components/Maps/Markers/Poi/poi.module.css | 52 +++++++ components/Maps/Markers/Poi/variants.ts | 34 ++++ .../Map => Maps}/Markers/Scandic.tsx | 0 components/Maps/Markers/utils.ts | 21 +++ constants/poiCategories.ts | 19 +++ i18n/dictionaries/da.json | 18 ++- i18n/dictionaries/de.json | 18 ++- i18n/dictionaries/en.json | 18 ++- i18n/dictionaries/fi.json | 18 ++- i18n/dictionaries/no.json | 18 ++- i18n/dictionaries/sv.json | 18 ++- package-lock.json | 8 +- package.json | 2 +- server/routers/hotels/output.ts | 52 +++++-- server/routers/hotels/query.ts | 1 + server/routers/hotels/utils.ts | 18 +++ types/components/hotelPage/map/dynamicMap.ts | 2 + .../hotelPage/map/dynamicMapContent.ts | 6 - types/components/hotelPage/map/mapCard.ts | 3 + types/components/hotelPage/map/mapContent.ts | 9 ++ types/components/hotelPage/map/sidebar.ts | 8 + types/components/icon.ts | 5 + types/components/maps/poiMarker.ts | 8 + types/hotel.ts | 5 +- 44 files changed, 1078 insertions(+), 318 deletions(-) delete mode 100644 components/ContentType/HotelPage/Map/DynamicMap/Content.tsx create mode 100644 components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx create mode 100644 components/ContentType/HotelPage/Map/DynamicMap/Map/map.module.css create mode 100644 components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx create mode 100644 components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css create mode 100644 components/Icons/Cultural.tsx create mode 100644 components/Icons/Museum.tsx create mode 100644 components/Icons/Shopping.tsx create mode 100644 components/Icons/StarFilled.tsx create mode 100644 components/Icons/Train.tsx create mode 100644 components/Maps/Markers/Poi/index.tsx create mode 100644 components/Maps/Markers/Poi/poi.module.css create mode 100644 components/Maps/Markers/Poi/variants.ts rename components/{ContentType/HotelPage/Map => Maps}/Markers/Scandic.tsx (100%) create mode 100644 components/Maps/Markers/utils.ts create mode 100644 constants/poiCategories.ts create mode 100644 server/routers/hotels/utils.ts delete mode 100644 types/components/hotelPage/map/dynamicMapContent.ts create mode 100644 types/components/hotelPage/map/mapContent.ts create mode 100644 types/components/hotelPage/map/sidebar.ts create mode 100644 types/components/maps/poiMarker.ts diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Content.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Content.tsx deleted file mode 100644 index 451bb07e6..000000000 --- a/components/ContentType/HotelPage/Map/DynamicMap/Content.tsx +++ /dev/null @@ -1,147 +0,0 @@ -"use client" -import { - AdvancedMarker, - Map, - type MapProps, - useMap, -} from "@vis.gl/react-google-maps" -import { useState } from "react" -import { useIntl } from "react-intl" - -import useHotelPageStore from "@/stores/hotel-page" - -import { CloseIcon, MinusIcon, PlusIcon } from "@/components/Icons" -import Button from "@/components/TempDesignSystem/Button" -import Divider from "@/components/TempDesignSystem/Divider" -import Title from "@/components/TempDesignSystem/Text/Title" -import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" -import useLang from "@/hooks/useLang" - -import ScandicMarker from "../Markers/Scandic" - -import styles from "./dynamicMap.module.css" - -import type { DynamicMapContentProps } from "@/types/components/hotelPage/map/dynamicMapContent" - -export default function DynamicMapContent({ - hotelName, - coordinates, -}: DynamicMapContentProps) { - const intl = useIntl() - const lang = useLang() - const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore() - const [isFullScreenSidebar, setIsFullScreenSidebar] = useState(false) - const map = useMap() - - const mapOptions: MapProps = { - defaultZoom: 15, - defaultCenter: coordinates, - disableDefaultUI: true, - clickableIcons: false, - mapId: `${hotelName}-${lang}-map`, - // As reference for future styles when adding POIs - // styles: [ - // { - // featureType: "poi", - // elementType: "all", - // stylers: [{ visibility: "off" }], - // }, - // ], - } - - useHandleKeyUp((event: KeyboardEvent) => { - if (event.key === "Escape" && isDynamicMapOpen) { - closeDynamicMap() - } - }) - - function zoomIn() { - const currentZoom = map && map.getZoom() - if (currentZoom) { - map.setZoom(currentZoom + 1) - } - } - function zoomOut() { - const currentZoom = map && map.getZoom() - if (currentZoom) { - map.setZoom(currentZoom - 1) - } - } - - function toggleFullScreenSidebar() { - setIsFullScreenSidebar((prev) => !prev) - } - - return ( - <> -
    - -
    - - -
    -
    - -
    - - - - - -
    - - ) -} diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx new file mode 100644 index 000000000..196baff83 --- /dev/null +++ b/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx @@ -0,0 +1,137 @@ +"use client" +import { + AdvancedMarker, + AdvancedMarkerAnchorPoint, + Map, + type MapProps, + useMap, +} from "@vis.gl/react-google-maps" +import { useIntl } from "react-intl" + +import useHotelPageStore from "@/stores/hotel-page" + +import { MinusIcon, PlusIcon } from "@/components/Icons" +import CloseLargeIcon from "@/components/Icons/CloseLarge" +import PoiMarker from "@/components/Maps/Markers/Poi" +import ScandicMarker from "@/components/Maps/Markers/Scandic" +import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import styles from "./map.module.css" + +import type { MapContentProps } from "@/types/components/hotelPage/map/mapContent" + +export default function MapContent({ + coordinates, + pointsOfInterest, + activePoi, + onActivePoiChange, +}: MapContentProps) { + const intl = useIntl() + const { closeDynamicMap } = useHotelPageStore() + const map = useMap() + + const mapOptions: MapProps = { + defaultZoom: 14, + defaultCenter: coordinates, + disableDefaultUI: true, + clickableIcons: false, + mapId: "6b48ef228325ae84", + } + + function zoomIn() { + const currentZoom = map && map.getZoom() + if (currentZoom) { + map.setZoom(currentZoom + 1) + } + } + function zoomOut() { + const currentZoom = map && map.getZoom() + if (currentZoom) { + map.setZoom(currentZoom - 1) + } + } + + function toggleActivePoi(poiName: string) { + onActivePoiChange(activePoi === poiName ? null : poiName) + } + + return ( +
    + + + + + + {pointsOfInterest.map((poi) => ( + onActivePoiChange(poi.name)} + onMouseLeave={() => onActivePoiChange(null)} + onClick={() => toggleActivePoi(poi.name)} + > + + + + + {poi.name} + + {poi.distance} km + + + + + + ))} + +
    + +
    + + +
    +
    +
    + ) +} diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Map/map.module.css b/components/ContentType/HotelPage/Map/DynamicMap/Map/map.module.css new file mode 100644 index 000000000..557730bfe --- /dev/null +++ b/components/ContentType/HotelPage/Map/DynamicMap/Map/map.module.css @@ -0,0 +1,108 @@ +.mapContainer { + --button-box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1); + width: 100%; + position: relative; + z-index: 0; +} + +.mapContainer::after { + content: ""; + position: absolute; + top: 0; + right: 0; + background: linear-gradient( + 43deg, + rgba(172, 172, 172, 0) 57.66%, + rgba(0, 0, 0, 0.25) 92.45% + ); + width: 100%; + height: 100%; + pointer-events: none; +} + +.ctaButtons { + position: absolute; + top: var(--Spacing-x2); + right: var(--Spacing-x2); + z-index: 1; + display: flex; + flex-direction: column; + gap: var(--Spacing-x7); + align-items: flex-end; + pointer-events: none; +} + +.zoomButtons { + display: grid; + gap: var(--Spacing-x1); +} + +.closeButton { + pointer-events: initial; + box-shadow: var(--button-box-shadow); + gap: var(--Spacing-x-half); +} + +.zoomButton { + width: var(--Spacing-x5); + height: var(--Spacing-x5); + padding: 0; + pointer-events: initial; + box-shadow: var(--button-box-shadow); +} + +.advancedMarker { + height: var(--Spacing-x4); + width: var( + --Spacing-x4 + ) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ +} + +.advancedMarker.active { + height: var(--Spacing-x5); + width: var( + --Spacing-x5 + ) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ +} + +.poi { + position: absolute; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + padding: var(--Spacing-x-half); + border-radius: var(--Corner-radius-Rounded); + background-color: var(--Base-Surface-Primary-light-Normal); + box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1); + gap: var(--Spacing-x1); +} + +.poi.active { + padding-right: var(--Spacing-x-one-and-half); +} + +.poiLabel { + display: none; +} + +.poi.active .poiLabel { + display: flex; + align-items: center; + gap: var(--Spacing-x2); + text-wrap: nowrap; +} + +@media screen and (min-width: 768px) { + .ctaButtons { + top: var(--Spacing-x4); + right: var(--Spacing-x4); + bottom: var(--Spacing-x4); + justify-content: space-between; + } + + .zoomButtons { + display: flex; + } +} diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx new file mode 100644 index 000000000..217fa51bd --- /dev/null +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx @@ -0,0 +1,102 @@ +"use client" + +import { useState } from "react" +import { useIntl } from "react-intl" + +import PoiMarker from "@/components/Maps/Markers/Poi" +import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" +import Title from "@/components/TempDesignSystem/Text/Title" + +import styles from "./sidebar.module.css" + +import type { SidebarProps } from "@/types/components/hotelPage/map/sidebar" + +export default function Sidebar({ + activePoi, + hotelName, + pointsOfInterest, + onActivePoiChange, +}: SidebarProps) { + const intl = useIntl() + const [isFullScreenSidebar, setIsFullScreenSidebar] = useState(false) + const poiCategories = new Set( + pointsOfInterest.map(({ category }) => category) + ) + const poisInCategories = Array.from(poiCategories).map((category) => ({ + category, + pois: pointsOfInterest.filter((poi) => poi.category === category), + })) + + function toggleFullScreenSidebar() { + setIsFullScreenSidebar((prev) => !prev) + } + + return ( + + ) +} diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css new file mode 100644 index 000000000..11f26b822 --- /dev/null +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css @@ -0,0 +1,111 @@ +.sidebar { + --sidebar-max-width: 26.25rem; + --sidebar-mobile-toggle-height: 91px; + --sidebar-mobile-fullscreen-height: calc( + 100vh - var(--main-menu-mobile-height) - var(--sidebar-mobile-toggle-height) + ); + + position: absolute; + top: var(--sidebar-mobile-fullscreen-height); + height: 100%; + right: 0; + left: 0; + background-color: var(--Base-Surface-Primary-light-Normal); + z-index: 1; + transition: top 0.3s; +} + +.sidebar:not(.fullscreen) { + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; +} + +.sidebar.fullscreen { + top: 0; +} + +.sidebarToggle { + position: relative; + margin: var(--Spacing-x4) 0 var(--Spacing-x2); + width: 100%; +} + +.sidebarToggle::before { + content: ""; + position: absolute; + display: block; + top: -0.5rem; + width: 100px; + height: 3px; + background-color: var(--UI-Text-High-contrast); +} + +.sidebarContent { + display: grid; + gap: var(--Spacing-x5); + align-content: start; + padding: var(--Spacing-x3) var(--Spacing-x2); + height: var(--sidebar-mobile-fullscreen-height); + overflow-y: auto; +} + +.poiGroup { + display: grid; + gap: var(--Spacing-x2); +} + +.poiHeading { + display: flex; + align-items: center; + gap: var(--Spacing-x1); +} + +.poiList { + list-style: none; +} + +.poiItem { + padding: var(--Spacing-x1) 0; + border-bottom: 1px solid var(--Base-Border-Subtle); +} + +.poiButton { + background-color: var(--Base-Surface-Primary-light-Normal); + border-width: 0; + font-family: var(--typography-Body-Regular-fontFamily); + font-size: var(--typography-Body-Regular-fontSize); + font-weight: var(--typography-Body-Regular-fontWeight); + color: var(--UI-Text-High-contrast); + width: 100%; + display: grid; + grid-template-columns: 1fr max-content; + gap: var(--Spacing-x2); + align-items: center; + padding: var(--Spacing-x-half) var(--Spacing-x1); + border-radius: var(--Corner-radius-Medium); + cursor: pointer; + text-align: left; + transition: background-color 0.3s; +} +.poiButton.active { + background-color: var(--Base-Surface-Primary-light-Hover); +} + +@media screen and (min-width: 768px) { + .sidebar { + position: static; + width: 40vw; + min-width: 10rem; + max-width: var(--sidebar-max-width); + background-color: var(--Base-Surface-Primary-light-Normal); + } + + .sidebarToggle { + display: none; + } + + .sidebarContent { + padding: var(--Spacing-x4) var(--Spacing-x5); + height: 100%; + position: relative; + } +} diff --git a/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css b/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css index 303ad4c1f..2a3e4ff78 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css +++ b/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css @@ -1,5 +1,4 @@ .dynamicMap { - --sidebar-height: 88px; position: fixed; top: var(--main-menu-mobile-height); right: 0; @@ -10,143 +9,8 @@ background-color: var(--Base-Surface-Primary-light-Normal); } -.sidebar { - position: absolute; - top: calc(100vh - var(--main-menu-mobile-height) - var(--sidebar-height)); - height: 100%; - right: 0; - left: 0; - background-color: var(--Base-Surface-Primary-light-Normal); - z-index: 1; - transition: top 0.3s; -} - -.sidebar:not(.fullscreen) { - border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; -} - -.sidebar.fullscreen { - top: 0; -} - -.sidebarToggle { - display: grid; - gap: var(--Spacing-x1); - padding: var(--Spacing-x2); - height: var(--sidebar-height); -} - -.toggleButton { - position: relative; - display: flex; - justify-content: center; - background-color: transparent; - border-width: 0; - padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); - font-family: var(--typography-Body-Bold-fontFamily); - font-size: var(--typography-Body-Bold-fontSize); - font-weight: var(--typography-Body-Bold-fontWeight); - color: var(--UI-Text-Medium-contrast); - width: 100%; -} - -.toggleButton::before { - content: ""; - position: absolute; - display: block; - top: -0.5rem; - width: 100px; - height: 3px; - background-color: var(--UI-Text-High-contrast); -} - -.sidebarContent { - display: grid; - gap: var(--Spacing-x3); - align-content: start; - padding: var(--Spacing-x3) var(--Spacing-x2); - height: 100%; - overflow-y: auto; -} - -.mapContainer { - flex: 1; - position: relative; -} - -.mapContainer::after { - content: ""; - display: block; - position: absolute; - top: 0; - right: 0; - background: linear-gradient( - 43deg, - rgba(172, 172, 172, 0) 57.66%, - rgba(0, 0, 0, 0.25) 92.45% - ); - width: 100%; - height: 100%; - pointer-events: none; -} - -.ctaButtons { - position: absolute; - top: var(--Spacing-x2); - right: var(--Spacing-x2); - z-index: 1; - display: flex; - flex-direction: column; - gap: var(--Spacing-x7); - align-items: flex-end; - pointer-events: none; -} - -.zoomButtons { - display: grid; - gap: var(--Spacing-x1); -} - -.closeButton { - pointer-events: initial; - box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1); -} - -.zoomButton { - width: var(--Spacing-x5); - height: var(--Spacing-x5); - padding: 0; - pointer-events: initial; - box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1); -} - @media screen and (min-width: 768px) { .dynamicMap { top: var(--main-menu-desktop-height); } - .sidebar { - position: static; - width: 40vw; - min-width: 10rem; - max-width: 26.25rem; - background-color: var(--Base-Surface-Primary-light-Normal); - } - .sidebarToggle { - display: none; - } - - .sidebarContent { - padding: var(--Spacing-x4) var(--Spacing-x5); - } - - .ctaButtons { - top: var(--Spacing-x4); - right: var(--Spacing-x4); - bottom: var(--Spacing-x4); - justify-content: space-between; - } - - .zoomButtons { - display: flex; - } } diff --git a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx index 2eee0b6ae..28cf8cc9b 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx @@ -8,7 +8,8 @@ import useHotelPageStore from "@/stores/hotel-page" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" -import DynamicMapContent from "./Content" +import MapContent from "./Map" +import Sidebar from "./Sidebar" import styles from "./dynamicMap.module.css" @@ -18,11 +19,13 @@ export default function DynamicMap({ apiKey, hotelName, coordinates, + pointsOfInterest, }: DynamicMapProps) { const intl = useIntl() const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore() const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0) const hasMounted = useRef(false) + const [activePoi, setActivePoi] = useState(null) useHandleKeyUp((event: KeyboardEvent) => { if (event.key === "Escape" && isDynamicMapOpen) { @@ -58,7 +61,18 @@ export default function DynamicMap({ { hotelName } )} > - + + diff --git a/components/ContentType/HotelPage/Map/MapCard/index.tsx b/components/ContentType/HotelPage/Map/MapCard/index.tsx index c3caca9ed..8764870bc 100644 --- a/components/ContentType/HotelPage/Map/MapCard/index.tsx +++ b/components/ContentType/HotelPage/Map/MapCard/index.tsx @@ -4,7 +4,9 @@ import { useIntl } from "react-intl" import useHotelPageStore from "@/stores/hotel-page" +import PoiMarker from "@/components/Maps/Markers/Poi" import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" @@ -12,7 +14,7 @@ import styles from "./mapCard.module.css" import type { MapCardProps } from "@/types/components/hotelPage/map/mapCard" -export default function MapCard({ hotelName }: MapCardProps) { +export default function MapCard({ hotelName, pois }: MapCardProps) { const intl = useIntl() const { openDynamicMap } = useHotelPageStore() @@ -34,6 +36,15 @@ export default function MapCard({ hotelName }: MapCardProps) { > {hotelName} +
      + {pois.map((poi) => ( +
    • + + {poi.name} + {poi.distance} km +
    • + ))} +
diff --git a/components/Header/index.tsx b/components/Header/index.tsx index 026cc72f0..cb24d890e 100644 --- a/components/Header/index.tsx +++ b/components/Header/index.tsx @@ -5,7 +5,7 @@ import TopMenu from "./TopMenu" import styles from "./header.module.css" -export default function Header({}) { +export default function Header() { return (
diff --git a/components/MyPages/Sidebar/index.tsx b/components/MyPages/Sidebar/index.tsx index d91782b83..b6169639e 100644 --- a/components/MyPages/Sidebar/index.tsx +++ b/components/MyPages/Sidebar/index.tsx @@ -1,4 +1,4 @@ -import { Fragment, Suspense } from "react" +import { Fragment } from "react" import { logout } from "@/constants/routes/handleAuth" import { serverClient } from "@/lib/trpc/server" diff --git a/types/components/header/navigationMenuList.ts b/types/components/header/navigationMenuList.ts new file mode 100644 index 000000000..1b43d9a42 --- /dev/null +++ b/types/components/header/navigationMenuList.ts @@ -0,0 +1,6 @@ +import { MenuItems } from "@/types/header" + +export interface NavigationMenuListProps { + isMobile: boolean + items: MenuItems +} diff --git a/types/header.ts b/types/header.ts index cc425f618..b9d5863c4 100644 --- a/types/header.ts +++ b/types/header.ts @@ -10,3 +10,4 @@ export type HeaderRefResponse = z.input export type HeaderResponse = z.input export type Header = z.output export type MenuItem = z.output +export type MenuItems = z.infer["menuItems"] From 730f66d79a3f44469fea57131d272c5205cb8aaf Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Wed, 18 Sep 2024 15:21:58 +0200 Subject: [PATCH 304/319] feat(SW-325): added mapId as environment variables --- .env.local.example | 2 ++ .env.test | 4 ++++ .../ContentType/HotelPage/Map/DynamicMap/Map/index.tsx | 3 ++- components/ContentType/HotelPage/Map/DynamicMap/index.tsx | 2 ++ components/ContentType/HotelPage/Map/StaticMap/index.tsx | 4 ++++ components/ContentType/HotelPage/index.tsx | 4 +++- components/Maps/Markers/Poi/index.tsx | 2 +- components/Maps/Markers/utils.ts | 2 +- components/Maps/StaticMap/index.tsx | 6 ++++++ env/server.ts | 8 ++++++-- types/components/hotelPage/map/dynamicMap.ts | 1 + types/components/hotelPage/map/mapCard.ts | 2 +- types/components/hotelPage/map/mapContent.ts | 5 +++-- types/components/hotelPage/map/sidebar.ts | 2 +- types/components/hotelPage/roomCard.ts | 2 +- types/components/maps/poiMarker.ts | 4 ++-- types/components/maps/staticMap.ts | 3 ++- 17 files changed, 42 insertions(+), 14 deletions(-) diff --git a/.env.local.example b/.env.local.example index 6d6af70cc..c747c3e1e 100644 --- a/.env.local.example +++ b/.env.local.example @@ -46,3 +46,5 @@ NEXTAUTH_URL="$PUBLIC_URL/api/web/auth" GOOGLE_STATIC_MAP_KEY="" GOOGLE_STATIC_MAP_SIGNATURE_SECRET="" +GOOGLE_STATIC_MAP_ID="" +GOOGLE_DYNAMIC_MAP_ID="" diff --git a/.env.test b/.env.test index 801c4336b..d2b538cc2 100644 --- a/.env.test +++ b/.env.test @@ -37,3 +37,7 @@ SEAMLESS_LOGOUT_NO="test" SEAMLESS_LOGOUT_SV="test" WEBVIEW_ENCRYPTION_KEY="test" BOOKING_ENCRYPTION_KEY="test" +GOOGLE_STATIC_MAP_KEY="test" +GOOGLE_STATIC_MAP_SIGNATURE_SECRET="test" +GOOGLE_STATIC_MAP_ID="test" +GOOGLE_DYNAMIC_MAP_ID="test" diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx index 196baff83..a049e90c4 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx @@ -26,6 +26,7 @@ export default function MapContent({ coordinates, pointsOfInterest, activePoi, + mapId, onActivePoiChange, }: MapContentProps) { const intl = useIntl() @@ -37,7 +38,7 @@ export default function MapContent({ defaultCenter: coordinates, disableDefaultUI: true, clickableIcons: false, - mapId: "6b48ef228325ae84", + mapId, } function zoomIn() { diff --git a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx index 28cf8cc9b..785949be5 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx @@ -20,6 +20,7 @@ export default function DynamicMap({ hotelName, coordinates, pointsOfInterest, + mapId, }: DynamicMapProps) { const intl = useIntl() const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore() @@ -72,6 +73,7 @@ export default function DynamicMap({ pointsOfInterest={pointsOfInterest} activePoi={activePoi} onActivePoiChange={setActivePoi} + mapId={mapId} /> diff --git a/components/ContentType/HotelPage/Map/StaticMap/index.tsx b/components/ContentType/HotelPage/Map/StaticMap/index.tsx index 4a070c29c..4c9ebe04a 100644 --- a/components/ContentType/HotelPage/Map/StaticMap/index.tsx +++ b/components/ContentType/HotelPage/Map/StaticMap/index.tsx @@ -1,5 +1,7 @@ /* eslint-disable @next/next/no-img-element */ +import { env } from "@/env/server" + import ScandicMarker from "@/components/Maps/Markers/Scandic" import StaticMapComp from "@/components/Maps/StaticMap" import { getIntl } from "@/i18n" @@ -15,6 +17,7 @@ export default async function StaticMap({ zoomLevel = 14, }: StaticMapProps) { const intl = await getIntl() + const mapId = env.GOOGLE_STATIC_MAP_ID const mapHeight = 785 const markerHeight = 100 const mapLatitudeInPx = mapHeight * 0.2 @@ -31,6 +34,7 @@ export default async function StaticMap({ height={mapHeight} zoomLevel={zoomLevel} altText={intl.formatMessage({ id: "Map of HOTEL_NAME" }, { hotelName })} + mapId={mapId} /> ) : null} diff --git a/components/Maps/Markers/Poi/index.tsx b/components/Maps/Markers/Poi/index.tsx index b98e5c1cb..db67af80b 100644 --- a/components/Maps/Markers/Poi/index.tsx +++ b/components/Maps/Markers/Poi/index.tsx @@ -3,7 +3,7 @@ import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import { getCategoryIconName } from "../utils" import { poiVariants } from "./variants" -import { PoiMarkerProps } from "@/types/components/maps/poiMarker" +import type { PoiMarkerProps } from "@/types/components/maps/poiMarker" export default function PoiMarker({ category, diff --git a/components/Maps/Markers/utils.ts b/components/Maps/Markers/utils.ts index 934df976b..bf3e5ec38 100644 --- a/components/Maps/Markers/utils.ts +++ b/components/Maps/Markers/utils.ts @@ -1,5 +1,5 @@ import { IconName } from "@/types/components/icon" -import { PointOfInterestCategory } from "@/types/hotel" +import type { PointOfInterestCategory } from "@/types/hotel" /* 2024-09-18: At the moment, the icons for the different categories is unknown. This will be handled later. */ diff --git a/components/Maps/StaticMap/index.tsx b/components/Maps/StaticMap/index.tsx index 170865c0a..52f7dd7b3 100644 --- a/components/Maps/StaticMap/index.tsx +++ b/components/Maps/StaticMap/index.tsx @@ -13,6 +13,7 @@ export default function StaticMap({ zoomLevel = 14, mapType = "roadmap", altText, + mapId, }: StaticMapProps) { const key = env.GOOGLE_STATIC_MAP_KEY const secret = env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET @@ -27,6 +28,11 @@ export default function StaticMap({ const url = new URL( `${baseUrl}?center=${center}&zoom=${zoomLevel}&size=${width}x${height}&maptype=${mapType}&key=${key}` ) + + if (mapId) { + url.searchParams.append("map_id", mapId) + } + const src = getUrlWithSignature(url, secret) return {altText} diff --git a/env/server.ts b/env/server.ts index 58f98067b..663de1ad1 100644 --- a/env/server.ts +++ b/env/server.ts @@ -61,8 +61,10 @@ export const env = createEnv({ SEAMLESS_LOGOUT_SV: z.string(), WEBVIEW_ENCRYPTION_KEY: z.string(), BOOKING_ENCRYPTION_KEY: z.string(), - GOOGLE_STATIC_MAP_KEY: z.string().optional(), - GOOGLE_STATIC_MAP_SIGNATURE_SECRET: z.string().optional(), + GOOGLE_STATIC_MAP_KEY: z.string(), + GOOGLE_STATIC_MAP_SIGNATURE_SECRET: z.string(), + GOOGLE_DYNAMIC_MAP_ID: z.string(), + GOOGLE_STATIC_MAP_ID: z.string(), }, emptyStringAsUndefined: true, runtimeEnv: { @@ -113,5 +115,7 @@ export const env = createEnv({ GOOGLE_STATIC_MAP_KEY: process.env.GOOGLE_STATIC_MAP_KEY, GOOGLE_STATIC_MAP_SIGNATURE_SECRET: process.env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET, + GOOGLE_STATIC_MAP_ID: process.env.GOOGLE_STATIC_MAP_ID, + GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID, }, }) diff --git a/types/components/hotelPage/map/dynamicMap.ts b/types/components/hotelPage/map/dynamicMap.ts index 97803b424..7b8595cc9 100644 --- a/types/components/hotelPage/map/dynamicMap.ts +++ b/types/components/hotelPage/map/dynamicMap.ts @@ -6,4 +6,5 @@ export interface DynamicMapProps { hotelName: string coordinates: Coordinates pointsOfInterest: PointOfInterest[] + mapId: string } diff --git a/types/components/hotelPage/map/mapCard.ts b/types/components/hotelPage/map/mapCard.ts index d09b23833..1ead43544 100644 --- a/types/components/hotelPage/map/mapCard.ts +++ b/types/components/hotelPage/map/mapCard.ts @@ -1,4 +1,4 @@ -import { PointOfInterest } from "@/types/hotel" +import type { PointOfInterest } from "@/types/hotel" export interface MapCardProps { hotelName: string diff --git a/types/components/hotelPage/map/mapContent.ts b/types/components/hotelPage/map/mapContent.ts index cc484d081..98e7b4c81 100644 --- a/types/components/hotelPage/map/mapContent.ts +++ b/types/components/hotelPage/map/mapContent.ts @@ -1,9 +1,10 @@ -import { PointOfInterest } from "@/types/hotel" -import type { Coordinates } from "../../maps/coordinates" +import type { Coordinates } from "@/types/components/maps/coordinates" +import type { PointOfInterest } from "@/types/hotel" export interface MapContentProps { coordinates: Coordinates pointsOfInterest: PointOfInterest[] activePoi: PointOfInterest["name"] | null + mapId: string onActivePoiChange: (poi: PointOfInterest["name"] | null) => void } diff --git a/types/components/hotelPage/map/sidebar.ts b/types/components/hotelPage/map/sidebar.ts index 070ab5dd3..6bde9f7dc 100644 --- a/types/components/hotelPage/map/sidebar.ts +++ b/types/components/hotelPage/map/sidebar.ts @@ -1,4 +1,4 @@ -import { PointOfInterest } from "@/types/hotel" +import type { PointOfInterest } from "@/types/hotel" export interface SidebarProps { hotelName: string diff --git a/types/components/hotelPage/roomCard.ts b/types/components/hotelPage/roomCard.ts index 4a555c621..4d07f170c 100644 --- a/types/components/hotelPage/roomCard.ts +++ b/types/components/hotelPage/roomCard.ts @@ -1,4 +1,4 @@ -import { RoomData } from "@/types/hotel" +import type { RoomData } from "@/types/hotel" export interface RoomCardProps { id: string diff --git a/types/components/maps/poiMarker.ts b/types/components/maps/poiMarker.ts index 01a5669be..89932fb51 100644 --- a/types/components/maps/poiMarker.ts +++ b/types/components/maps/poiMarker.ts @@ -1,7 +1,7 @@ -import { VariantProps } from "class-variance-authority" - import { poiVariants } from "@/components/Maps/Markers/Poi/variants" +import type { VariantProps } from "class-variance-authority" + export interface PoiMarkerProps extends VariantProps { size?: number className?: string diff --git a/types/components/maps/staticMap.ts b/types/components/maps/staticMap.ts index 40a73776c..8519f4101 100644 --- a/types/components/maps/staticMap.ts +++ b/types/components/maps/staticMap.ts @@ -1,4 +1,4 @@ -import { Coordinates } from "./coordinates" +import type { Coordinates } from "./coordinates" export type StaticMapProps = { city?: string @@ -8,4 +8,5 @@ export type StaticMapProps = { zoomLevel?: number mapType?: "roadmap" | "satellite" | "terrain" | "hybrid" altText: string + mapId?: string } From ff7dc69f7e3de4f08a3ccaf07571e11629b1ab37 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Thu, 19 Sep 2024 09:07:51 +0200 Subject: [PATCH 305/319] More PR fixes --- .../Header/MainMenu/MobileMenu/index.tsx | 6 +++-- .../NavigationMenuList/index.tsx | 2 -- components/Header/MainMenu/index.tsx | 22 +++++++++---------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index e5acd7642..fdffa1ce7 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -1,6 +1,6 @@ "use client" -import { useEffect } from "react" +import { Suspense, useEffect } from "react" import { Dialog, Modal } from "react-aria-components" import { useIntl } from "react-intl" @@ -62,7 +62,9 @@ export default function MobileMenu({ languageUrls, topLink }: MobileMenuProps) { className={styles.dialog} aria-label={intl.formatMessage({ id: "Menu" })} > - + + +