diff --git a/app/[lang]/(live)/(protected)/my-pages/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/layout.tsx index e6152ae51..9677f014b 100644 --- a/app/[lang]/(live)/(protected)/my-pages/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/layout.tsx @@ -5,9 +5,9 @@ import styles from "./layout.module.css" export default async function MyPagesLayout({ breadcrumbs, children, -}: React.PropsWithChildren & { +}: React.PropsWithChildren<{ breadcrumbs: React.ReactNode -}) { +}>) { return (
{breadcrumbs} 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/app/[lang]/(live)/@bookingwidget/[...paths]/page.tsx b/app/[lang]/(live)/@bookingwidget/[...paths]/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx new file mode 100644 index 000000000..2ebaca014 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx @@ -0,0 +1 @@ +export { default } from "../../page" diff --git a/app/[lang]/(live)/@bookingwidget/default.tsx b/app/[lang]/(live)/@bookingwidget/default.tsx new file mode 100644 index 000000000..83ec2818e --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/default.tsx @@ -0,0 +1 @@ +export { default } from "./page" diff --git a/app/[lang]/(live)/@bookingwidget/my-pages/[...path]/page.tsx b/app/[lang]/(live)/@bookingwidget/my-pages/[...path]/page.tsx new file mode 100644 index 000000000..2ebaca014 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/my-pages/[...path]/page.tsx @@ -0,0 +1 @@ +export { default } from "../../page" diff --git a/app/[lang]/(live)/@bookingwidget/page.tsx b/app/[lang]/(live)/@bookingwidget/page.tsx new file mode 100644 index 000000000..9b83da1b8 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/page.tsx @@ -0,0 +1,18 @@ +import { serverClient } from "@/lib/trpc/server" + +import BookingWidget from "@/components/BookingWidget" +import { getLang } from "@/i18n/serverContext" + +export default async function BookingWidgetPage() { + // Get the booking widget show/hide status based on page specific settings + const bookingWidgetToggle = + await serverClient().contentstack.bookingwidget.getToggle() + + return ( + <> + {bookingWidgetToggle && bookingWidgetToggle.hideBookingWidget ? null : ( + + )} + + ) +} diff --git a/app/[lang]/(live)/@header/[...paths]/@languageSwitcher/error.tsx b/app/[lang]/(live)/@header/[...paths]/@languageSwitcher/error.tsx deleted file mode 100644 index 1501c40ab..000000000 --- a/app/[lang]/(live)/@header/[...paths]/@languageSwitcher/error.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client" - -import { baseUrls } from "@/constants/routes/baseUrls" - -import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher" - -export default function Error() { - return -} diff --git a/app/[lang]/(live)/@header/[...paths]/@languageSwitcher/page.tsx b/app/[lang]/(live)/@header/[...paths]/@languageSwitcher/page.tsx deleted file mode 100644 index 6758eaa7c..000000000 --- a/app/[lang]/(live)/@header/[...paths]/@languageSwitcher/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { serverClient } from "@/lib/trpc/server" - -import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher" -import { setLang } from "@/i18n/serverContext" - -import { LangParams, PageArgs } from "@/types/params" - -export default async function LanguageSwitcherRoute({ - params, -}: PageArgs) { - setLang(params.lang) - - const data = await serverClient().contentstack.languageSwitcher.get() - if (!data) { - return null - } - return -} diff --git a/app/[lang]/(live)/@header/[...paths]/@myPagesMobileDropdown/page.tsx b/app/[lang]/(live)/@header/[...paths]/@myPagesMobileDropdown/page.tsx deleted file mode 100644 index 8384a267c..000000000 --- a/app/[lang]/(live)/@header/[...paths]/@myPagesMobileDropdown/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { serverClient } from "@/lib/trpc/server" - -import MyPagesMobileDropdown from "@/components/Current/Header/MyPagesMobileDropdown" -import { setLang } from "@/i18n/serverContext" - -import { LangParams, PageArgs } from "@/types/params" - -export default async function MyPagesMobileDropdownPage({ - params, -}: PageArgs) { - setLang(params.lang) - const navigation = await serverClient().contentstack.myPages.navigation.get() - if (!navigation) return null - return -} diff --git a/app/[lang]/(live)/@header/[...paths]/layout.tsx b/app/[lang]/(live)/@header/[...paths]/layout.tsx deleted file mode 100644 index 3a56b7a6d..000000000 --- a/app/[lang]/(live)/@header/[...paths]/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Header from "@/components/Current/Header" -import { setLang } from "@/i18n/serverContext" - -import type { LangParams, LayoutArgs } from "@/types/params" - -export default function HeaderLayout({ - languageSwitcher, - myPagesMobileDropdown, - params, -}: LayoutArgs & { - languageSwitcher: React.ReactNode - myPagesMobileDropdown: React.ReactNode -}) { - setLang(params.lang) - return ( -
- ) -} diff --git a/app/[lang]/(live)/@header/[...paths]/page.tsx b/app/[lang]/(live)/@header/[...paths]/page.tsx index c662446a8..03a82e5f5 100644 --- a/app/[lang]/(live)/@header/[...paths]/page.tsx +++ b/app/[lang]/(live)/@header/[...paths]/page.tsx @@ -1,8 +1 @@ -import { setLang } from "@/i18n/serverContext" - -import type { LangParams, PageArgs } from "@/types/params" - -export default function EmptyHeaderPage({ params }: PageArgs) { - setLang(params.lang) - return null -} +export { default } from "../page" 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/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index 77911e187..3ee99194c 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -21,9 +21,11 @@ export default async function RootLayout({ children, params, header, + bookingwidget, }: React.PropsWithChildren< LayoutArgs & { header: React.ReactNode + bookingwidget: React.ReactNode } >) { setLang(params.lang) @@ -52,6 +54,7 @@ export default async function RootLayout({ {header} + {bookingwidget} {children}
diff --git a/app/[lang]/(live-current)/layout.tsx b/app/[lang]/(live-current)/layout.tsx index 7e45b121c..12578764e 100644 --- a/app/[lang]/(live-current)/layout.tsx +++ b/app/[lang]/(live-current)/layout.tsx @@ -4,6 +4,7 @@ import "@scandic-hotels/design-system/style.css" import Script from "next/script" import TokenRefresher from "@/components/Auth/TokenRefresher" +import BookingWidget from "@/components/BookingWidget" import AdobeScript from "@/components/Current/AdobeScript" import Footer from "@/components/Current/Footer" import Header from "@/components/Current/Header" @@ -71,6 +72,7 @@ export default async function RootLayout({ myPagesMobileDropdown={myPagesMobileDropdown} languageSwitcher={languageSwitcher} /> + {children}
diff --git a/app/api/web/revalidate/route.ts b/app/api/web/revalidate/route.ts index 5fd43a0b6..b1373eb5e 100644 --- a/app/api/web/revalidate/route.ts +++ b/app/api/web/revalidate/route.ts @@ -6,6 +6,7 @@ import { z } from "zod" import { Lang } from "@/constants/languages" import { env } from "@/env/server" import { internalServerError } from "@/server/errors/next" +import { affix as bookingwidgetAffix } from "@/server/routers/contentstack/bookingwidget/utils" import { affix as breadcrumbsAffix } from "@/server/routers/contentstack/breadcrumbs/utils" import { languageSwitcherAffix } from "@/server/routers/contentstack/languageSwitcher/utils" @@ -29,6 +30,11 @@ const validateJsonBody = z.object({ locale: z.nativeEnum(Lang), uid: z.string(), url: z.string().optional(), + page_settings: z + .object({ + hide_booking_widget: z.boolean(), + }) + .optional(), }), }), }) @@ -105,6 +111,17 @@ export async function POST(request: NextRequest) { revalidateTag(breadcrumbsTag) } + if (entry.page_settings?.hide_booking_widget) { + const bookingwidgetTag = generateTag( + entry.locale, + entry.uid, + bookingwidgetAffix + ) + + console.info(`Revalidating breadcrumbsTag: ${bookingwidgetTag}`) + revalidateTag(bookingwidgetTag) + } + return Response.json({ revalidated: true, now: Date.now() }) } catch (error) { console.error("Failed to revalidate tag(s)") diff --git a/app/globals.css b/app/globals.css index 6180a72f6..dcbbe972d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -97,10 +97,18 @@ } :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; + --current-mobile-site-header-height: 70.047px; + --max-width-navigation: 89.5rem; + + --main-menu-mobile-height: 75px; + + --header-z-index: 1; + --menu-overlay-z-index: 10; } * { diff --git a/components/BookingWidget/index.tsx b/components/BookingWidget/index.tsx index 5d4bc4dbd..d169f7dd3 100644 --- a/components/BookingWidget/index.tsx +++ b/components/BookingWidget/index.tsx @@ -1,8 +1,8 @@ -import Form from "../Forms/BookingWidget" +import Form from "@/components/Forms/BookingWidget" import styles from "./bookingWidget.module.css" -export function BookingWidget() { +export default async function BookingWidget() { return (
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 163f83aa9..b6fa4f7dc 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( @@ -80,7 +80,7 @@ export function Rooms({ rooms }: RoomsProps) {
+ + + +
+ + + {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..797b3b427 --- /dev/null +++ b/components/Header/MainMenu/MobileMenu/mobileMenu.module.css @@ -0,0 +1,103 @@ +@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.3s; + 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); +} + +.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; + z-index: var(--menu-overlay-z-index); +} + +.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); +} + +@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 new file mode 100644 index 000000000..8398ff840 --- /dev/null +++ b/components/Header/MainMenu/MyPagesMenu/index.tsx @@ -0,0 +1,60 @@ +"use client" + +import { useIntl } from "react-intl" + +import useDropdownStore from "@/stores/main-menu" + +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 MyPagesMenuContent from "../MyPagesMenuContent" + +import styles from "./myPagesMenu.module.css" + +import type { MyPagesMenuProps } from "@/types/components/header/myPagesMenu" + +export default function MyPagesMenu({ + membership, + navigation, + user, +}: MyPagesMenuProps) { + const intl = useIntl() + const lang = useLang() + const { toggleMyPagesMenu, isMyPagesMenuOpen } = useDropdownStore() + + useHandleKeyUp((event: KeyboardEvent) => { + if (event.key === "Escape" && isMyPagesMenuOpen) { + toggleMyPagesMenu() + } + }) + + return ( +
+ + + + {intl.formatMessage({ id: "Hi" })} {user.firstName}! + + + + {isMyPagesMenuOpen ? ( +
+ +
+ ) : null} +
+ ) +} diff --git a/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css new file mode 100644 index 000000000..39002b177 --- /dev/null +++ b/components/Header/MainMenu/MyPagesMenu/myPagesMenu.module.css @@ -0,0 +1,46 @@ +.myPagesMenu { + display: none; +} + +@media screen and (min-width: 768px) { + .myPagesMenu { + display: block; + position: relative; + } + + .chevron { + transition: transform 0.3s; + } + + .chevron.isExpanded { + transform: rotate(180deg); + } + + .dropdown { + position: absolute; + top: 2.875rem; /* 2.875rem is the height of the main menu + bottom padding */ + right: 0; + background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Large); + box-shadow: 0 0 0.875rem 0.375rem rgba(0, 0, 0, 0.1); + min-width: 20rem; + z-index: var(--menu-overlay-z-index); + } + + /* 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..8cf46be54 --- /dev/null +++ b/components/Header/MainMenu/MyPagesMenuContent/index.tsx @@ -0,0 +1,99 @@ +"use client" + +import Link from "next/link" +import { useIntl } from "react-intl" + +import { MembershipLevelEnum } from "@/constants/membershipLevels" +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 { getMembershipLevelObject } from "@/utils/membershipLevel" + +import styles from "./myPagesMenuContent.module.css" + +import type { MyPagesMenuContentProps } from "@/types/components/header/myPagesMenu" + +export default function MyPagesMenuContent({ + membership, + navigation, + toggleOpenStateFn, + user, +}: MyPagesMenuContentProps) { + const intl = useIntl() + const lang = useLang() + const myPagesMenuContentRef = useTrapFocus() + + const membershipLevel = membership?.membershipLevel + ? getMembershipLevelObject( + membership.membershipLevel as MembershipLevelEnum, + lang + ) + : null + const membershipPoints = membership?.currentPoints + const introClassName = + membershipLevel && membershipPoints + ? `${styles.intro}` + : `${styles.intro} ${styles.noMembership}` + 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..5e16801e3 --- /dev/null +++ b/components/Header/MainMenu/MyPagesMenuContent/myPagesMenuContent.module.css @@ -0,0 +1,75 @@ +.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(--Base-Text-High-contrast); + font-family: var(--typography-Body-Bold-fontFamily); + font-size: var(--typography-Body-Bold-fontSize); + font-weight: var(--typography-Body-Bold-fontWeight); + 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); + } + .intro.noMembership, + .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..29a8023a1 --- /dev/null +++ b/components/Header/MainMenu/MyPagesMobileMenu/index.tsx @@ -0,0 +1,58 @@ +"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 type { MyPagesMenuProps } from "@/types/components/header/myPagesMenu" + +export default function MyPagesMobileMenu({ + membership, + 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..014fc5cb2 --- /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); + z-index: var(--menu-overlay-z-index); +} + +.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: 768px) { + .myPagesMobileMenu, + .modal { + display: none; + } +} diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx new file mode 100644 index 000000000..086377dcd --- /dev/null +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx @@ -0,0 +1,46 @@ +"use client" + +import { useState } from "react" + +import { ChevronDownIcon, ChevronRightIcon } from "@/components/Icons" +import Link from "@/components/TempDesignSystem/Link" + +import MainMenuButton from "../../MainMenuButton" + +import styles from "./navigationMenuItem.module.css" + +import type { NavigationMenuItemProps } from "@/types/components/header/navigationMenuItem" + +export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) { + const { children, title, href, seeAllLinkText, infoCard } = item + const [isExpanded, setIsExpanded] = useState(false) + + 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 new file mode 100644 index 000000000..2c6c79180 --- /dev/null +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/navigationMenuItem.module.css @@ -0,0 +1,14 @@ +.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.3s; +} + +.chevron.isExpanded { + transform: rotate(180deg); +} diff --git a/components/Header/MainMenu/NavigationMenu/index.tsx b/components/Header/MainMenu/NavigationMenu/index.tsx new file mode 100644 index 000000000..c8b0aa96e --- /dev/null +++ b/components/Header/MainMenu/NavigationMenu/index.tsx @@ -0,0 +1,22 @@ +import NavigationMenuItem from "./NavigationMenuItem" + +import styles from "./navigationMenu.module.css" + +import type { NavigationMenuProps } from "@/types/components/header/navigationMenu" + +export default function NavigationMenu({ + items, + isMobile, +}: NavigationMenuProps) { + return ( +
    + {items.map((item) => ( +
  • + +
  • + ))} +
+ ) +} diff --git a/components/Header/MainMenu/NavigationMenu/navigationMenu.module.css b/components/Header/MainMenu/NavigationMenu/navigationMenu.module.css new file mode 100644 index 000000000..4b36fc1dc --- /dev/null +++ b/components/Header/MainMenu/NavigationMenu/navigationMenu.module.css @@ -0,0 +1,26 @@ +.navigationMenu { + list-style: none; + margin: 0; + 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.desktop { + display: flex; + } +} diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx new file mode 100644 index 000000000..7b5e2973d --- /dev/null +++ b/components/Header/MainMenu/index.tsx @@ -0,0 +1,81 @@ +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" + +import type { 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() + + const user = await serverClient().user.name() + const membership = await serverClient().user.membershipLevel() + + return ( +
+ +
+ ) +} diff --git a/components/Header/MainMenu/mainMenu.module.css b/components/Header/MainMenu/mainMenu.module.css new file mode 100644 index 000000000..9464318c7 --- /dev/null +++ b/components/Header/MainMenu/mainMenu.module.css @@ -0,0 +1,54 @@ +.mainMenu { + background-color: var(--Base-Surface-Primary-light-Normal); + padding: var(--Spacing-x2); + border-bottom: 1px solid var(--Base-Border-Subtle); +} + +.nav { + max-width: var(--max-width-navigation); + margin: 0 auto; + display: grid; + grid-template-columns: max-content 1fr; + align-items: center; + gap: var(--Spacing-x2); +} + +.menus { + display: flex; + justify-self: end; + align-items: center; + gap: var(--Spacing-x2); +} + +.logoLink { + display: inline-flex; + width: auto; +} + +.logo { + height: 1.375rem; +} + +.loginLink { + display: flex; + align-items: center; + gap: var(--Spacing-x1); +} + +.userName { + display: none; +} + +@media screen and (min-width: 768px) { + .nav { + display: flex; + justify-content: space-between; + gap: var(--Spacing-x3); + } + .menus { + display: contents; + } + .userName { + display: inline; + } +} diff --git a/components/Header/TopMenu/index.tsx b/components/Header/TopMenu/index.tsx new file mode 100644 index 000000000..898da8d17 --- /dev/null +++ b/components/Header/TopMenu/index.tsx @@ -0,0 +1,31 @@ +import { GiftIcon, SearchIcon } from "@/components/Icons" +import LanguageSwitcher from "@/components/LanguageSwitcher" +import { getIntl } from "@/i18n" + +import HeaderLink from "../HeaderLink" + +import styles from "./topMenu.module.css" + +import type { TopMenuProps } from "@/types/components/header/topMenu" + +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 new file mode 100644 index 000000000..17a739989 --- /dev/null +++ b/components/Header/TopMenu/topMenu.module.css @@ -0,0 +1,26 @@ +.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); +} + +.content { + max-width: var(--max-width-navigation); + margin: 0 auto; + display: flex; + justify-content: space-between; + gap: var(--Spacing-x3); +} + +.right { + display: flex; + gap: var(--Spacing-x2); + align-items: center; +} + +@media screen and (min-width: 768px) { + .topMenu { + display: block; + } +} diff --git a/components/Header/header.module.css b/components/Header/header.module.css new file mode 100644 index 000000000..f50c38a95 --- /dev/null +++ b/components/Header/header.module.css @@ -0,0 +1,7 @@ +.header { + position: relative; + font-family: var(--typography-Body-Regular-fontFamily); + color: var(--Base-Text-High-contrast); + box-shadow: 0 6px 10px rgba(0, 0, 0, 0.08); + z-index: var(--header-z-index); +} diff --git a/components/Header/index.tsx b/components/Header/index.tsx new file mode 100644 index 000000000..d4e01de0f --- /dev/null +++ b/components/Header/index.tsx @@ -0,0 +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/Header/tempHeaderData.ts b/components/Header/tempHeaderData.ts new file mode 100644 index 000000000..38810fc37 --- /dev/null +++ b/components/Header/tempHeaderData.ts @@ -0,0 +1,75 @@ +import type { MainNavigationItem } from "@/types/components/header/mainNavigationItem" + +export const navigationMenuItems: MainNavigationItem[] = [ + { + 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", + }, +] diff --git a/components/Icons/Check.tsx b/components/Icons/Check.tsx index 254dae82b..47c5067a0 100644 --- a/components/Icons/Check.tsx +++ b/components/Icons/Check.tsx @@ -7,27 +7,27 @@ export default function CheckIcon({ className, color, ...props }: IconProps) { 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/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/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..aa9f15e52 --- /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 SearchIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Service.tsx b/components/Icons/Service.tsx new file mode 100644 index 000000000..1f91f7cd8 --- /dev/null +++ b/components/Icons/Service.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ServiceIcon({ 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..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, @@ -25,6 +26,7 @@ import { ElectricBikeIcon, EmailIcon, FitnessIcon, + GiftIcon, GlobeIcon, HouseIcon, ImageIcon, @@ -39,6 +41,8 @@ import { PlusCircleIcon, RestaurantIcon, SaunaIcon, + SearchIcon, + ServiceIcon, TshirtWashIcon, WarningTriangle, WifiIcon, @@ -72,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: @@ -92,6 +98,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 +130,10 @@ export function getIconByIconName(icon?: IconName): FC | null { return RestaurantIcon case IconName.Sauna: 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 7f0259352..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" @@ -21,6 +22,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 +39,8 @@ 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 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/LanguageSwitcher/LanguageSwitcherContent/index.tsx b/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx new file mode 100644 index 000000000..7a20e3da1 --- /dev/null +++ b/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx @@ -0,0 +1,69 @@ +"use client" + +import { useIntl } from "react-intl" + +import { Lang, languages } from "@/constants/languages" +import useDropdownStore from "@/stores/main-menu" + +import { CheckIcon, ChevronLeftIcon } from "@/components/Icons" +import Link from "@/components/TempDesignSystem/Link" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import useLang from "@/hooks/useLang" +import { useTrapFocus } from "@/hooks/useTrapFocus" + +import styles from "./languageSwitcherContent.module.css" + +import type { LanguageSwitcherProps } from "@/types/components/languageSwitcher/languageSwitcher" + +export default function LanguageSwitcherContent({ + urls, + type, +}: LanguageSwitcherProps) { + const intl = useIntl() + const currentLanguage = useLang() + const { toggleLanguageSwitcher } = useDropdownStore() + const languageSwitcherRef = useTrapFocus() + const urlKeys = Object.keys(urls) as Lang[] + + return ( +
+ {type === "mobileHeader" ? ( +
+ +
+ ) : null} + +
+ + {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/LanguageSwitcherContent/languageSwitcherContent.module.css b/components/LanguageSwitcher/LanguageSwitcherContent/languageSwitcherContent.module.css new file mode 100644 index 000000000..465b39c23 --- /dev/null +++ b/components/LanguageSwitcher/LanguageSwitcherContent/languageSwitcherContent.module.css @@ -0,0 +1,76 @@ +.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); +} + +.list { + list-style: none; +} + +.link { + color: var(--Scandic-Brand-Burgundy); + font-family: var(--typography-Body-Regular-fontFamily); + font-size: var(--typography-Body-Regular-fontSize); + line-height: var(--typography-Body-Regular-lineHeight); + letter-spacing: var(--typography-Body-Regular-letterSpacing); + padding: var(--Spacing-x1); + border-radius: var(--Corner-radius-Medium); + display: flex; + gap: var(--Spacing-x1); + justify-content: space-between; + align-items: center; + text-decoration: none; + border-radius: var(--Corner-radius-Medium); +} + +.link.active, +.link:hover { + background-color: var(--Base-Surface-Primary-light-Hover-alt); + font-weight: var(--typography-Body-Bold-fontWeight); +} + +@media screen and (min-width: 768px) { + .backWrapper, + .backButton { + display: none; + } + + .languageWrapper { + padding: var(--Spacing-x2) var(--Spacing-x3); + } + + .subtitle { + display: none; + } + + .link.active:not(:hover) { + background-color: transparent; + } +} diff --git a/components/LanguageSwitcher/index.tsx b/components/LanguageSwitcher/index.tsx new file mode 100644 index 000000000..be8a2bebb --- /dev/null +++ b/components/LanguageSwitcher/index.tsx @@ -0,0 +1,63 @@ +"use client" + +import { useIntl } from "react-intl" + +import { languages } from "@/constants/languages" +import useDropdownStore from "@/stores/main-menu" + +import { ChevronDownIcon, GlobeIcon } from "@/components/Icons" +import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" +import useLang from "@/hooks/useLang" + +import LanguageSwitcherContent from "./LanguageSwitcherContent" + +import styles from "./languageSwitcher.module.css" + +import type { LanguageSwitcherProps } from "@/types/components/languageSwitcher/languageSwitcher" + +export default function LanguageSwitcher({ + urls, + type, +}: LanguageSwitcherProps) { + const intl = useIntl() + const currentLanguage = useLang() + const { toggleLanguageSwitcher, isLanguageSwitcherOpen } = useDropdownStore() + + useHandleKeyUp((event: KeyboardEvent) => { + if (event.key === "Escape" && isLanguageSwitcherOpen) { + toggleLanguageSwitcher() + } + }) + + return ( +
+ + +
+ {isLanguageSwitcherOpen ? ( + + ) : null} +
+
+ ) +} diff --git a/components/LanguageSwitcher/languageSwitcher.module.css b/components/LanguageSwitcher/languageSwitcher.module.css new file mode 100644 index 000000000..89ce55b56 --- /dev/null +++ b/components/LanguageSwitcher/languageSwitcher.module.css @@ -0,0 +1,76 @@ +.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: 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.3s; +} + +.chevron.isExpanded { + transform: rotate(180deg); +} + +.dropdown { + position: fixed; + top: var(--main-menu-mobile-height); + right: -100vw; + bottom: 0; + width: 100%; + background-color: var(--Base-Surface-Primary-light-Normal); + transition: right 0.3s; + z-index: var(--menu-overlay-z-index); +} + +.dropdown.isExpanded { + display: block; + right: 0; +} + +@media screen and (min-width: 768px) { + .languageSwitcher { + position: relative; + } + + .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); + } +} diff --git a/components/Lightbox/Lightbox.module.css b/components/Lightbox/Lightbox.module.css index 494368206..484eba1d4 100644 --- a/components/Lightbox/Lightbox.module.css +++ b/components/Lightbox/Lightbox.module.css @@ -3,7 +3,7 @@ } .mobileGallery { - margin-top: var(--mobile-site-header-height); + margin-top: var(--current-mobile-site-header-height); height: 100%; position: relative; display: flex; @@ -112,7 +112,7 @@ } .fullViewContainer { - margin-top: var(--mobile-site-header-height); + margin-top: var(--current-mobile-site-header-height); background-color: var(--UI-Text-High-contrast); height: 100%; padding: var(--Spacing-x2); 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/MaxWidth/maxWidth.module.css b/components/MaxWidth/maxWidth.module.css index 563ae5721..666bcd4cf 100644 --- a/components/MaxWidth/maxWidth.module.css +++ b/components/MaxWidth/maxWidth.module.css @@ -1,4 +1,4 @@ .container { - max-width: var(--max-width, 1140px); + max-width: var(--current-max-width, 1140px); position: relative; } 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..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 { @@ -131,7 +123,7 @@ } .peach80 { - color: var(--Primary-Light-On-Surface-Accent); + color: var(--Base-Text-Medium-contrast); } .red { @@ -140,12 +132,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/SidePeek/sidePeek.module.css b/components/TempDesignSystem/SidePeek/sidePeek.module.css index 0297f9d04..0ed9304c5 100644 --- a/components/TempDesignSystem/SidePeek/sidePeek.module.css +++ b/components/TempDesignSystem/SidePeek/sidePeek.module.css @@ -1,10 +1,10 @@ .sidePeek { position: fixed; - top: var(--mobile-site-header-height); + top: var(--current-mobile-site-header-height); right: auto; bottom: 0; width: 100%; - height: calc(100vh - var(--mobile-site-header-height)); + 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); @@ -24,7 +24,7 @@ .overlay { position: absolute; - top: var(--mobile-site-header-height); + top: var(--current-mobile-site-header-height); bottom: 0; left: 0; right: 0; @@ -46,7 +46,7 @@ } to { - top: var(--mobile-site-header-height); + top: var(--current-mobile-site-header-height); } } 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/cypress/support/component.ts b/cypress/support/component.ts index d308632d2..853685310 100644 --- a/cypress/support/component.ts +++ b/cypress/support/component.ts @@ -18,7 +18,6 @@ import "./commands" // Alternatively you can use CommonJS syntax: // require('./commands') - import { mount } from "cypress/react18" // Augment the Cypress namespace to include type definitions for 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..b7d1311c8 --- /dev/null +++ b/hooks/useTrapFocus.ts @@ -0,0 +1,83 @@ +"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/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index a50f0448a..3d5ddcf19 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -15,6 +15,7 @@ "As our Close Friend": "Som vores nære ven", "At latest": "Senest", "At the hotel": "På hotellet", + "Back to scandichotels.com": "Tilbage til scandichotels.com", "Bed type": "Seng type", "Book": "Book", "Book reward night": "Book bonusnat", @@ -34,6 +35,9 @@ "City/State": "By/Stat", "Click here to log in": "Klik her for at logge ind", "Close": "Tæt", + "Close language menu": "Luk sprogmenu", + "Close menu": "Luk menu", + "Close my pages menu": "Luk mine sider menu", "Coming up": "Er lige om hjørnet", "Compare all levels": "Sammenlign alle niveauer", "Contact us": "Kontakt os", @@ -43,6 +47,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", @@ -64,6 +69,7 @@ "from your member profile?": "fra din medlemsprofil?", "Get inspired": "Bliv inspireret", "Go back to overview": "Gå tilbage til oversigten", + "Hi": "Hei", "Highest level": "Højeste niveau", "Hotel facilities": "Hotel faciliteter", "Hotel surroundings": "Hotel omgivelser", @@ -82,6 +88,7 @@ "Level up to unlock": "Stig i niveau for at låse op", "Log in": "Log på", "Log in here": "Log ind her", + "Log in/Join": "Log på/Tilmeld dig", "Log out": "Log ud", "Manage preferences": "Administrer præferencer", "Meetings & Conferences": "Møder & Konferencer", @@ -91,12 +98,14 @@ "Members": "Medlemmer", "Membership cards": "Medlemskort", "Membership ID": "Medlems-id", + "Menu": "Menu", "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 pages menu": "Mine sider menu", + "My payment cards": "Mine betalingskort", "My wishes": "Mine ønsker", "New password": "Nyt kodeord", "Next": "Næste", @@ -114,6 +123,9 @@ "number": "nummer", "On your journey": "På din rejse", "Open": "Åben", + "Open language menu": "Åbn sprogmenuen", + "Open menu": "Åbn menuen", + "Open my pages menu": "Åbn mine sider menuen", "or": "eller", "Overview": "Oversigt", "Password": "Adgangskode", @@ -125,7 +137,7 @@ "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.", @@ -152,6 +164,7 @@ "Select country of residence": "Vælg bopælsland", "Select date of birth": "Vælg fødselsdato", "Select language": "Vælg sprog", + "Select your language": "Vælg dit sprog", "Show all amenities": "Vis alle faciliteter", "Show less": "Vis mindre", "Show map": "Vis kort", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 10a8e7fcf..d365e7945 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -14,6 +14,7 @@ "As our Close Friend": "Als unser enger Freund", "At latest": "Spätestens", "At the hotel": "Im Hotel", + "Back to scandichotels.com": "Zurück zu scandichotels.com", "Bed type": "Bettentyp", "Book": "Buchen", "Book reward night": "Bonusnacht buchen", @@ -33,6 +34,9 @@ "City/State": "Stadt/Zustand", "Click here to log in": "Klicken Sie hier, um sich einzuloggen", "Close": "Schließen", + "Close language menu": "Sprachmenü schließen", + "Close menu": "Menü schließen", + "Close my pages menu": "Meine Seiten Menü schließen", "Coming up": "Demnächst", "Compare all levels": "Vergleichen Sie alle Levels", "Contact us": "Kontaktieren Sie uns", @@ -42,6 +46,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", @@ -63,6 +68,7 @@ "from your member profile?": "wirklich aus Ihrem Mitgliedsprofil entfernen?", "Get inspired": "Lassen Sie sich inspieren", "Go back to overview": "Zurück zur Übersicht", + "Hi": "Hallo", "Highest level": "Höchstes Level", "Hotel facilities": "Hotel-Infos", "Hotel surroundings": "Umgebung des Hotels", @@ -81,6 +87,7 @@ "Level up to unlock": "Zum Freischalten aufsteigen", "Log in": "Anmeldung", "Log in here": "Hier einloggen", + "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", @@ -89,12 +96,14 @@ "Members": "Mitglieder", "Membership cards": "Mitgliedskarten", "Membership ID": "Mitglieds-ID", + "Menu": "Menu", "Modify": "Ändern", "Month": "Monat", "My communication preferences": "Meine Kommunikationseinstellungen", - "My credit cards": "Meine Kreditkarten", "My membership cards": "Meine Mitgliedskarten", "My pages": "Meine Seiten", + "My pages menu": "Meine Seite Menü", + "My payment cards": "Meine Zahlungskarten", "My wishes": "Meine Wünsche", "New password": "Neues Kennwort", "Next": "Nächste", @@ -112,6 +121,9 @@ "number": "nummer", "On your journey": "Auf deiner Reise", "Open": "Offen", + "Open language menu": "Sprachmenü öffnen", + "Open menu": "Menü öffnen", + "Open my pages menu": "Meine Seiten Menü öffnen", "or": "oder", "Password": "Passwort", "Pay later": "Später bezahlen", @@ -147,6 +159,7 @@ "Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus", "Select date of birth": "Geburtsdatum auswählen", "Select language": "Sprache auswählen", + "Select your language": "Wählen Sie Ihre Sprache", "Show all amenities": "Alle Annehmlichkeiten anzeigen", "Show less": "Weniger anzeigen", "Show map": "Karte anzeigen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 8d6ce6f44..4ced7fc92 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -15,6 +15,7 @@ "As our Close Friend": "As our Close Friend", "At latest": "At latest", "At the hotel": "At the hotel", + "Back to scandichotels.com": "Back to scandichotels.com", "Bed type": "Bed type", "Book": "Book", "Book reward night": "Book reward night", @@ -34,6 +35,9 @@ "City/State": "City/State", "Click here to log in": "Click here to log in", "Close": "Close", + "Close language menu": "Close language menu", + "Close menu": "Close menu", + "Close my pages menu": "Close my pages menu", "Coming up": "Coming up", "Compare all levels": "Compare all levels", "Contact us": "Contact us", @@ -43,6 +47,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", @@ -66,6 +71,7 @@ "from your member profile?": "from your member profile?", "Get inspired": "Get inspired", "Go back to overview": "Go back to overview", + "Hi": "Hi", "Highest level": "Highest level", "Hotel facilities": "Hotel facilities", "Hotel surroundings": "Hotel surroundings", @@ -87,6 +93,7 @@ "Level up to unlock": "Level up to unlock", "Log in": "Log in", "Log in here": "Log in here", + "Log in/Join": "Log in/Join", "Log out": "Log out", "Manage preferences": "Manage preferences", "Meetings & Conferences": "Meetings & Conferences", @@ -96,12 +103,14 @@ "Members": "Members", "Membership cards": "Membership cards", "Membership ID": "Membership ID", + "Menu": "Menu", "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 pages menu": "My pages menu", + "My payment cards": "My payment cards", "My wishes": "My wishes", "New password": "New password", "Next": "Next", @@ -119,6 +128,9 @@ "number": "number", "On your journey": "On your journey", "Open": "Open", + "Open language menu": "Open language menu", + "Open menu": "Open menu", + "Open my pages menu": "Open my pages menu", "or": "or", "Overview": "Overview", "Password": "Password", @@ -130,7 +142,7 @@ "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.", @@ -158,6 +170,7 @@ "Select country of residence": "Select country of residence", "Select date of birth": "Select date of birth", "Select language": "Select language", + "Select your language": "Select your language", "Show all amenities": "Show all amenities", "Show less": "Show less", "Show map": "Show map", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 3c33b7848..b54da3963 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -15,6 +15,7 @@ "As our Close Friend": "Läheisenä ystävänämme", "At latest": "Viimeistään", "At the hotel": "Hotellissa", + "Back to scandichotels.com": "Takaisin scandichotels.com", "Bed type": "Vuodetyyppi", "Book": "Varaa", "Book reward night": "Kirjapalkinto-ilta", @@ -34,6 +35,9 @@ "City/State": "Kaupunki/Osavaltio", "Click here to log in": "Napsauta tästä kirjautuaksesi sisään", "Close": "Kiinni", + "Close language menu": "Sulje kielivalikko", + "Close menu": "Sulje valikko", + "Close my pages menu": "Sulje omat sivut -valikko", "Coming up": "Tulossa", "Compare all levels": "Vertaa kaikkia tasoja", "Contact us": "Ota meihin yhteyttä", @@ -43,6 +47,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", @@ -64,6 +69,7 @@ "from your member profile?": "jäsenprofiilistasi?", "Get inspired": "Inspiroidu", "Go back to overview": "Palaa yleiskatsaukseen", + "Hi": "Hi", "Highest level": "Korkein taso", "Hotel facilities": "Hotellin palvelut", "Hotel surroundings": "Hotellin ympäristö", @@ -79,9 +85,9 @@ "Level 5": "Taso 5", "Level 6": "Taso 6", "Level 7": "Taso 7", - "Level up to unlock": "Nouse seuraavalle tasolle ja avaat seuraavan edun", "Log in": "Kirjaudu sisään", "Log in here": "Kirjaudu sisään", + "Log in/Join": "Kirjaudu sisään/Liittyä", "Log out": "Kirjaudu ulos", "Manage preferences": "Asetusten hallinta", "Meetings & Conferences": "Kokoukset & Konferenssit", @@ -91,12 +97,14 @@ "Members": "Jäsenet", "Membership cards": "Jäsenkortit", "Membership ID": "Jäsentunnus", + "Menu": "Valikko", "Modify": "Muokkaa", "Month": "Kuukausi", "My communication preferences": "Viestintämieltymykseni", - "My credit cards": "Luottokorttini", "My membership cards": "Jäsenkorttini", "My pages": "Omat sivut", + "My pages menu": "Omat sivut -valikko", + "My payment cards": "Minun maksukortit", "My wishes": "Toiveeni", "New password": "Uusi salasana", "Next": "Seuraava", @@ -114,6 +122,9 @@ "number": "määrä", "On your journey": "Matkallasi", "Open": "Avata", + "Open language menu": "Avaa kielivalikko", + "Open menu": "Avaa valikko", + "Open my pages menu": "Avaa omat sivut -valikko", "or": "tai", "Overview": "Yleiskatsaus", "Password": "Salasana", @@ -152,6 +163,7 @@ "Select country of residence": "Valitse asuinmaa", "Select date of birth": "Valitse syntymäaika", "Select language": "Valitse kieli", + "Select your language": "Valitse kieli", "Show all amenities": "Näytä kaikki mukavuudet", "Show less": "Näytä vähemmän", "Show map": "Näytä kartta", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 7cebd9aac..57ea705a0 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -15,6 +15,7 @@ "As our Close Friend": "Som vår nære venn", "At latest": "Senest", "At the hotel": "På hotellet", + "Back to scandichotels.com": "Tilbake til scandichotels.com", "Bed type": "Seng type", "Book": "Bestill", "Book reward night": "Bestill belønningskveld", @@ -34,6 +35,9 @@ "City/State": "By/Stat", "Click here to log in": "Klikk her for å logge inn", "Close": "Lukk", + "Close language menu": "Lukk språkmeny", + "Close menu": "Lukk meny", + "Close my pages menu": "Lukk mine sidermenyn", "Coming up": "Kommer opp", "Compare all levels": "Sammenlign alle nivåer", "Contact us": "Kontakt oss", @@ -43,6 +47,7 @@ "Country code": "Landskode", "Credit card deleted successfully": "Kredittkort slettet", "Current password": "Nåværende passord", + "Customer service": "Kundeservice", "Date of Birth": "Fødselsdato", "Day": "Dag", "Description": "Beskrivelse", @@ -64,6 +69,7 @@ "from your member profile?": "fra medlemsprofilen din?", "Get inspired": "Bli inspirert", "Go back to overview": "Gå tilbake til oversikten", + "Hi": "Hei", "Highest level": "Høyeste nivå", "Hotel facilities": "Hotelfaciliteter", "Hotel surroundings": "Hotellomgivelser", @@ -82,6 +88,7 @@ "Level up to unlock": "Nivå opp for å låse opp", "Log in": "Logg Inn", "Log in here": "Logg inn her", + "Log in/Join": "Logg på/Bli med", "Log out": "Logg ut", "Manage preferences": "Administrer preferanser", "Meetings & Conferences": "Møter & Konferanser", @@ -91,12 +98,14 @@ "Members": "Medlemmer", "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", + "Menu": "Menu", "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 pages menu": "Mine sider-menyen", + "My payment cards": "Mine betalingskort", "My wishes": "Mine ønsker", "New password": "Nytt passord", "Next": "Neste", @@ -114,6 +123,9 @@ "number": "antall", "On your journey": "På reisen din", "Open": "Åpen", + "Open language menu": "Åpne språkmenyen", + "Open menu": "Åpne menyen", + "Open my pages menu": "Åpne mine sider menyen", "or": "eller", "Overview": "Oversikt", "Password": "Passord", @@ -124,7 +136,7 @@ "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", @@ -152,6 +164,7 @@ "Select country of residence": "Velg bostedsland", "Select date of birth": "Velg fødselsdato", "Select language": "Velg språk", + "Select your language": "Velg språk", "Show all amenities": "Vis alle fasiliteter", "Show less": "Vis mindre", "Show map": "Vis kart", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index e08e9afe3..9634b8b2d 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -15,6 +15,7 @@ "As our Close Friend": "Som vår nära vän", "At latest": "Senast", "At the hotel": "På hotellet", + "Back to scandichotels.com": "Tillbaka till scandichotels.com", "Bed type": "Sängtyp", "Book": "Boka", "Book reward night": "Boka frinatt", @@ -34,6 +35,9 @@ "City/State": "Ort", "Click here to log in": "Klicka här för att logga in", "Close": "Stäng", + "Close language menu": "Stäng språkmenyn", + "Close menu": "Stäng menyn", + "Close my pages menu": "Stäng mina sidor menyn", "Coming up": "Kommer härnäst", "Compare all levels": "Jämför alla nivåer", "Contact us": "Kontakta oss", @@ -43,6 +47,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", @@ -64,6 +69,7 @@ "from your member profile?": "från din medlemsprofil?", "Get inspired": "Bli inspirerad", "Go back to overview": "Gå tillbaka till översikten", + "Hi": "Hej", "Highest level": "Högsta nivå", "Hotel facilities": "Hotellfaciliteter", "Hotel surroundings": "Hotellomgivning", @@ -84,6 +90,7 @@ "Level up to unlock": "Levla upp för att låsa upp", "Log in": "Logga in", "Log in here": "Logga in här", + "Log in/Join": "Logga in/Gå med", "Log out": "Logga ut", "Manage preferences": "Hantera inställningar", "Meetings & Conferences": "Möten & Konferenser", @@ -93,12 +100,14 @@ "Members": "Medlemmar", "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", + "Menu": "Meny", "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 pages menu": "Mina sidor meny", + "My payment cards": "Mina betalningskort", "My wishes": "Mina önskningar", "New password": "Nytt lösenord", "Next": "Nästa", @@ -116,6 +125,9 @@ "number": "nummer", "On your journey": "På din resa", "Open": "Öppna", + "Open language menu": "Öppna språkmenyn", + "Open menu": "Öppna menyn", + "Open my pages menu": "Öppna mina sidor menyn", "or": "eller", "Overview": "Översikt", "Password": "Lösenord", @@ -155,6 +167,7 @@ "Select country of residence": "Välj bosättningsland", "Select date of birth": "Välj födelsedatum", "Select language": "Välj språk", + "Select your language": "Välj ditt språk", "Show all amenities": "Visa alla bekvämligheter", "Show less": "Visa mindre", "Show map": "Visa karta", @@ -183,7 +196,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/lib/graphql/Query/BookingWidgetToggle.graphql b/lib/graphql/Query/BookingWidgetToggle.graphql new file mode 100644 index 000000000..fa6f73fff --- /dev/null +++ b/lib/graphql/Query/BookingWidgetToggle.graphql @@ -0,0 +1,39 @@ +query GetAccountPageSettings($uid: String!, $locale: String!) { + account_page(uid: $uid, locale: $locale) { + page_settings { + hide_booking_widget + } + } +} + +query GetLoyaltyPageSettings($uid: String!, $locale: String!) { + loyalty_page(uid: $uid, locale: $locale) { + page_settings { + hide_booking_widget + } + } +} + +query GetContentPageSettings($uid: String!, $locale: String!) { + content_page(uid: $uid, locale: $locale) { + page_settings { + hide_booking_widget + } + } +} + +query GetHotelPageSettings($uid: String!, $locale: String!) { + hotel_page(uid: $uid, locale: $locale) { + page_settings { + hide_booking_widget + } + } +} + +query GetCurrentBlocksPageSettings($uid: String!, $locale: String!) { + current_blocks_page(uid: $uid, locale: $locale) { + page_settings { + hide_booking_widget + } + } +} 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/server/routers/contentstack/base/query.ts b/server/routers/contentstack/base/query.ts index 12b1fa6d6..e8570e192 100644 --- a/server/routers/contentstack/base/query.ts +++ b/server/routers/contentstack/base/query.ts @@ -13,11 +13,7 @@ import { request } from "@/lib/graphql/request" import { notFound } from "@/server/errors/trpc" import { contentstackBaseProcedure, router } from "@/server/trpc" -import { - generateRefsResponseTag, - generateRefTag, - generateTag, -} from "@/utils/generateTag" +import { generateRefsResponseTag, generateTag } from "@/utils/generateTag" import { langInput } from "./input" import { diff --git a/server/routers/contentstack/bookingwidget/index.ts b/server/routers/contentstack/bookingwidget/index.ts new file mode 100644 index 000000000..8aeebe7ae --- /dev/null +++ b/server/routers/contentstack/bookingwidget/index.ts @@ -0,0 +1,5 @@ +import { mergeRouters } from "@/server/trpc" + +import { bookingwidgetQueryRouter } from "./query" + +export const bookingwidgetRouter = mergeRouters(bookingwidgetQueryRouter) diff --git a/server/routers/contentstack/bookingwidget/output.ts b/server/routers/contentstack/bookingwidget/output.ts new file mode 100644 index 000000000..e113bae95 --- /dev/null +++ b/server/routers/contentstack/bookingwidget/output.ts @@ -0,0 +1,21 @@ +import { z } from "zod" + +const bookingWidgetToggleSchema = z + .object({ + page_settings: z.object({ + hide_booking_widget: z.boolean(), + }), + }) + .optional() + +export const validateBookingWidgetToggleSchema = z.object({ + account_page: bookingWidgetToggleSchema, + loyalty_page: bookingWidgetToggleSchema, + content_page: bookingWidgetToggleSchema, + hotel_page: bookingWidgetToggleSchema, + current_blocks_page: bookingWidgetToggleSchema, +}) + +export type ValidateBookingWidgetToggleType = z.infer< + typeof validateBookingWidgetToggleSchema +> diff --git a/server/routers/contentstack/bookingwidget/query.ts b/server/routers/contentstack/bookingwidget/query.ts new file mode 100644 index 000000000..89f8d68b6 --- /dev/null +++ b/server/routers/contentstack/bookingwidget/query.ts @@ -0,0 +1,93 @@ +import { ValueOf } from "next/dist/shared/lib/constants" + +import { + GetAccountPageSettings, + GetContentPageSettings, + GetCurrentBlocksPageSettings, + GetHotelPageSettings, + GetLoyaltyPageSettings, +} from "@/lib/graphql/Query/BookingWidgetToggle.graphql" +import { request } from "@/lib/graphql/request" +import { contentstackExtendedProcedureUID, router } from "@/server/trpc" + +import { generateTag } from "@/utils/generateTag" + +import { + validateBookingWidgetToggleSchema, + ValidateBookingWidgetToggleType, +} from "./output" +import { affix as bookingwidgetAffix } from "./utils" + +import { ContentTypeEnum } from "@/types/requests/contentType" + +export const bookingwidgetQueryRouter = router({ + getToggle: contentstackExtendedProcedureUID.query(async ({ ctx }) => { + const failedResponse = { hideBookingWidget: false } + const { contentType, uid, lang } = ctx + + // This condition is to handle 404 page case + if (!contentType || !uid) { + console.log("No proper params defined: ", contentType, uid) + return failedResponse + } + let GetPageSettings = "" + const contentTypeCMS = >( + contentType.replaceAll("-", "_") + ) + + switch (contentTypeCMS) { + case ContentTypeEnum.accountPage: + GetPageSettings = GetAccountPageSettings + break + case ContentTypeEnum.loyaltyPage: + GetPageSettings = GetLoyaltyPageSettings + break + case ContentTypeEnum.contentPage: + GetPageSettings = GetContentPageSettings + break + case ContentTypeEnum.hotelPage: + GetPageSettings = GetHotelPageSettings + break + case ContentTypeEnum.currentBlocksPage: + GetPageSettings = GetCurrentBlocksPageSettings + break + } + + if (!GetPageSettings) { + console.error("No proper Content type defined: ", contentType) + return failedResponse + } + + const response = await request( + GetPageSettings, + { + uid: uid, + locale: lang, + }, + { + next: { + tags: [generateTag(lang, uid, bookingwidgetAffix)], + }, + } + ) + + const bookingWidgetToggleData = validateBookingWidgetToggleSchema.safeParse( + response.data + ) + if (!bookingWidgetToggleData.success) { + console.error( + "Flag hide_booking_widget fetch error: ", + bookingWidgetToggleData.error + ) + return failedResponse + } + + const hideBookingWidget = + bookingWidgetToggleData.data[contentTypeCMS]?.page_settings + ?.hide_booking_widget + + return { + hideBookingWidget, + } + }), +}) diff --git a/server/routers/contentstack/bookingwidget/utils.ts b/server/routers/contentstack/bookingwidget/utils.ts new file mode 100644 index 000000000..af79363f0 --- /dev/null +++ b/server/routers/contentstack/bookingwidget/utils.ts @@ -0,0 +1 @@ +export const affix = "bookingwidget" diff --git a/server/routers/contentstack/index.ts b/server/routers/contentstack/index.ts index c04dc867d..d409f06ae 100644 --- a/server/routers/contentstack/index.ts +++ b/server/routers/contentstack/index.ts @@ -2,6 +2,7 @@ import { router } from "@/server/trpc" import { accountPageRouter } from "./accountPage" import { baseRouter } from "./base" +import { bookingwidgetRouter } from "./bookingwidget" import { breadcrumbsRouter } from "./breadcrumbs" import { contentPageRouter } from "./contentPage" import { hotelPageRouter } from "./hotelPage" @@ -13,6 +14,7 @@ import { myPagesRouter } from "./myPages" export const contentstackRouter = router({ accountPage: accountPageRouter, base: baseRouter, + bookingwidget: bookingwidgetRouter, breadcrumbs: breadcrumbsRouter, hotelPage: hotelPageRouter, languageSwitcher: languageSwitcherRouter, diff --git a/server/routers/contentstack/myPages/index.ts b/server/routers/contentstack/myPages/index.ts index 8b5cdcd71..2b1e07f4e 100644 --- a/server/routers/contentstack/myPages/index.ts +++ b/server/routers/contentstack/myPages/index.ts @@ -1,5 +1,6 @@ -import { router } from "@/server/trpc"; -import { navigationRouter } from "./navigation"; +import { router } from "@/server/trpc" + +import { navigationRouter } from "./navigation" export const myPagesRouter = router({ navigation: navigationRouter, diff --git a/server/transformer.ts b/server/transformer.ts index 0d8d72aeb..b30080513 100644 --- a/server/transformer.ts +++ b/server/transformer.ts @@ -1,2 +1,3 @@ import superjson from "superjson" + export const transformer = superjson diff --git a/stores/main-menu.ts b/stores/main-menu.ts index cbd6f1004..6818d4cdb 100644 --- a/stores/main-menu.ts +++ b/stores/main-menu.ts @@ -1,31 +1,103 @@ 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 - if (!state.isHamburgerMenuOpen && state.isMyPagesMobileMenuOpen) { - set({ isMyPagesMobileMenuOpen: false }) + set( + ({ isHamburgerMenuOpen, isMyPagesMenuOpen, isMyPagesMobileMenuOpen }) => { + // Close the other dropdowns if they're open + if (!isHamburgerMenuOpen) { + if (isMyPagesMenuOpen) { + set({ isMyPagesMenuOpen: false }) + } + if (isMyPagesMobileMenuOpen) { + set({ isMyPagesMobileMenuOpen: false }) + } + } + return { isHamburgerMenuOpen: !isHamburgerMenuOpen } } - return { isHamburgerMenuOpen: !state.isHamburgerMenuOpen } - }), + ), toggleMyPagesMobileMenu: () => - set((state) => { - // Close the other dropdown if it's open - if (!state.isMyPagesMobileMenuOpen && state.isHamburgerMenuOpen) { - set({ isHamburgerMenuOpen: false }) + set( + ({ + isMyPagesMenuOpen, + isMyPagesMobileMenuOpen, + isHamburgerMenuOpen, + isLanguageSwitcherOpen, + }) => { + // Close the other dropdowns if they're open + if (!isMyPagesMobileMenuOpen) { + if (isMyPagesMenuOpen) { + set({ isMyPagesMenuOpen: false }) + } + if (isHamburgerMenuOpen) { + set({ isHamburgerMenuOpen: false }) + } + if (isLanguageSwitcherOpen) { + set({ isLanguageSwitcherOpen: false }) + } + } + return { isMyPagesMobileMenuOpen: !isMyPagesMobileMenuOpen } } - return { isMyPagesMobileMenuOpen: !state.isMyPagesMobileMenuOpen } - }), + ), + toggleMyPagesMenu: () => + set( + ({ + isHamburgerMenuOpen, + isLanguageSwitcherOpen, + isMyPagesMenuOpen, + isMyPagesMobileMenuOpen, + }) => { + // Close the other dropdowns if they're open + if (!isMyPagesMenuOpen) { + if (isHamburgerMenuOpen) { + set({ isHamburgerMenuOpen: false }) + } + if (isMyPagesMobileMenuOpen) { + set({ isMyPagesMobileMenuOpen: false }) + } + if (isLanguageSwitcherOpen) { + set({ isLanguageSwitcherOpen: 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 }) + } + } + return { isLanguageSwitcherOpen: !isLanguageSwitcherOpen } + } + ), })) export default useDropdownStore diff --git a/types/components/current/asides/puffs.ts b/types/components/current/asides/puffs.ts index 9a1691b91..07198d912 100644 --- a/types/components/current/asides/puffs.ts +++ b/types/components/current/asides/puffs.ts @@ -1,5 +1,5 @@ -import type { Node } from "@/types/requests/utils/edges" import type { Puff } from "@/types/requests/puff" +import type { Node } from "@/types/requests/utils/edges" export type PuffsProps = { puffs: Node[] diff --git a/types/components/current/languageSwitcher.ts b/types/components/current/languageSwitcher.ts index 0f409d36e..44b49fff6 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 = { diff --git a/types/components/header/avatar.ts b/types/components/header/avatar.ts new file mode 100644 index 000000000..a06a4d2f8 --- /dev/null +++ b/types/components/header/avatar.ts @@ -0,0 +1,6 @@ +import type { ImageProps } from "next/image" + +export interface AvatarProps { + image?: ImageProps + initials?: string | null +} diff --git a/types/components/header/headerLink.ts b/types/components/header/headerLink.ts new file mode 100644 index 000000000..deb1c71ab --- /dev/null +++ b/types/components/header/headerLink.ts @@ -0,0 +1,3 @@ +import type { 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..34906bbd5 --- /dev/null +++ b/types/components/header/mainMenu.ts @@ -0,0 +1,5 @@ +import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" + +export interface MainMenuProps { + languageUrls: LanguageSwitcherData +} diff --git a/types/components/header/mainMenuButton.ts b/types/components/header/mainMenuButton.ts new file mode 100644 index 000000000..4d88a6b38 --- /dev/null +++ b/types/components/header/mainMenuButton.ts @@ -0,0 +1,2 @@ +export interface MainMenuButtonProps + extends React.ButtonHTMLAttributes {} diff --git a/types/components/header/mainNavigationItem.ts b/types/components/header/mainNavigationItem.ts new file mode 100644 index 000000000..3eff9ab37 --- /dev/null +++ b/types/components/header/mainNavigationItem.ts @@ -0,0 +1,20 @@ +export interface MainNavigationItem { + 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 + } +} diff --git a/types/components/header/mobileMenu.ts b/types/components/header/mobileMenu.ts new file mode 100644 index 000000000..aa43f1ab0 --- /dev/null +++ b/types/components/header/mobileMenu.ts @@ -0,0 +1,8 @@ +import { MainNavigationItem } from "./mainNavigationItem" + +import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" + +export interface MobileMenuProps { + languageUrls: LanguageSwitcherData + mainNavigation: MainNavigationItem[] +} diff --git a/types/components/header/myPagesMenu.ts b/types/components/header/myPagesMenu.ts new file mode 100644 index 000000000..dcad1887d --- /dev/null +++ b/types/components/header/myPagesMenu.ts @@ -0,0 +1,19 @@ +import { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query" + +import { MembershipLevel } from "@/utils/user" + +import type { User } from "@/types/user" + +type MyPagesNavigation = Awaited< + ReturnType<(typeof navigationQueryRouter)["get"]> +> + +export interface MyPagesMenuProps { + navigation: MyPagesNavigation + user: Pick + membership?: MembershipLevel | null +} + +export interface MyPagesMenuContentProps extends MyPagesMenuProps { + toggleOpenStateFn: () => void +} diff --git a/types/components/header/navigationMenu.ts b/types/components/header/navigationMenu.ts new file mode 100644 index 000000000..ceaed9247 --- /dev/null +++ b/types/components/header/navigationMenu.ts @@ -0,0 +1,6 @@ +import type { MainNavigationItem } from "@/types/components/header/mainNavigationItem" + +export interface NavigationMenuProps { + items: MainNavigationItem[] + isMobile: boolean +} diff --git a/types/components/header/navigationMenuItem.ts b/types/components/header/navigationMenuItem.ts new file mode 100644 index 000000000..c529e0dcf --- /dev/null +++ b/types/components/header/navigationMenuItem.ts @@ -0,0 +1,6 @@ +import type { MainNavigationItem } from "@/types/components/header/mainNavigationItem" + +export interface NavigationMenuItemProps { + item: MainNavigationItem + isMobile: boolean +} diff --git a/types/components/header/topMenu.ts b/types/components/header/topMenu.ts new file mode 100644 index 000000000..b30cf64a0 --- /dev/null +++ b/types/components/header/topMenu.ts @@ -0,0 +1,5 @@ +import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" + +export interface TopMenuProps { + languageUrls: LanguageSwitcherData +} diff --git a/types/components/header/topMenuButton.ts b/types/components/header/topMenuButton.ts new file mode 100644 index 000000000..20589bc4c --- /dev/null +++ b/types/components/header/topMenuButton.ts @@ -0,0 +1,2 @@ +export interface TopMenuButtonProps + extends React.ButtonHTMLAttributes {} diff --git a/types/components/icon.ts b/types/components/icon.ts index d965a4e78..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", @@ -29,6 +30,7 @@ export enum IconName { Email = "Email", Facebook = "Facebook", Fitness = "Fitness", + Gift = "Gift", Globe = "Globe", House = "House", Image = "Image", @@ -44,6 +46,8 @@ export enum IconName { PlusCircle = "PlusCircle", Restaurant = "Restaurant", Sauna = "Sauna", + Search = "Search", + Service = "Service", Tripadvisor = "Tripadvisor", TshirtWash = "TshirtWash", Wifi = "Wifi", diff --git a/types/components/languageSwitcher/languageSwitcher.ts b/types/components/languageSwitcher/languageSwitcher.ts new file mode 100644 index 000000000..fbadfab5d --- /dev/null +++ b/types/components/languageSwitcher/languageSwitcher.ts @@ -0,0 +1,6 @@ +import { LanguageSwitcherData } from "@/types/requests/languageSwitcher" + +export interface LanguageSwitcherProps { + type: "mobileHeader" | "mobileFooter" | "desktopHeader" | "desktopFooter" + urls: LanguageSwitcherData +} 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 diff --git a/types/requests/contentType.ts b/types/requests/contentType.ts new file mode 100644 index 000000000..605c96c9b --- /dev/null +++ b/types/requests/contentType.ts @@ -0,0 +1,7 @@ +export enum ContentTypeEnum { + accountPage = "account_page", + loyaltyPage = "loyalty_page", + hotelPage = "hotel_page", + contentPage = "content_page", + currentBlocksPage = "current_blocks_page", +} diff --git a/utils/tabbable.ts b/utils/tabbable.ts new file mode 100644 index 000000000..37dafb131 --- /dev/null +++ b/utils/tabbable.ts @@ -0,0 +1,63 @@ +/*! + * 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 ( + style.getPropertyValue("display") === "none" || + (zeroSize && style.getPropertyValue("overflow") !== "visible") + ) +} + +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) +}