diff --git a/.env.local.example b/.env.local.example index 6d842230e..ab0b7ee72 100644 --- a/.env.local.example +++ b/.env.local.example @@ -19,6 +19,8 @@ DESIGN_SYSTEM_ACCESS_TOKEN="" NEXTAUTH_REDIRECT_PROXY_URL="http://localhost:3000/api/web/auth" NEXTAUTH_SECRET="" REVALIDATE_SECRET="" +SALESFORCE_PREFERENCE_BASE_URL="https://cloud.emails.scandichotels.com/preference-center" + SEAMLESS_LOGIN_DA="http://www.example.dk/updatelogin" SEAMLESS_LOGIN_DE="http://www.example.de/updatelogin" SEAMLESS_LOGIN_EN="http://www.example.com/updatelogin" diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx index fb15f24cc..1cc5c648f 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx @@ -1,4 +1,5 @@ import { ArrowRightIcon } from "@/components/Icons" +import ManagePreferencesButton from "@/components/Profile/ManagePreferencesButton" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -27,12 +28,7 @@ export default async function CommunicationSlot({ })} - - - - {formatMessage({ id: "Manage preferences" })} - - + ) } 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 a37b0047b..4dbfc1ab4 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx @@ -1,4 +1,3 @@ -import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" import AddCreditCardButton from "@/components/Profile/AddCreditCardButton" @@ -17,8 +16,6 @@ export default async function CreditCardSlot({ params }: PageArgs) { const { formatMessage } = await getIntl() const creditCards = await serverClient().user.creditCards() - const { lang } = params - return (
diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx index cdf46a35d..73496fe4e 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx @@ -15,8 +15,7 @@ export default function ProfileLayout({ {profile} {creditCards} - {/* TODO: Implement communication preferences flow. Hidden until decided on where to send user. */} - {/* {communication} */} + {communication}
) diff --git a/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx index 3ebb491e5..d2f2a5450 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx @@ -3,10 +3,10 @@ import { notFound } from "next/navigation" import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" +import BedType from "@/components/HotelReservation/EnterDetails/BedType" +import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" import Details from "@/components/HotelReservation/EnterDetails/Details" import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" -import BedSelection from "@/components/HotelReservation/SelectRate/BedSelection" -import BreakfastSelection from "@/components/HotelReservation/SelectRate/BreakfastSelection" import Payment from "@/components/HotelReservation/SelectRate/Payment" import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" import SectionAccordion from "@/components/HotelReservation/SelectRate/SectionAccordion" @@ -155,24 +155,14 @@ export default async function SectionsPage({ selection={selectedBed} path={`select-bed?${currentSearchParams}`} > - {params.section === "select-bed" && ( - - )} + {params.section === "select-bed" ? : null} - {params.section === "breakfast" && ( - - )} + {params.section === "breakfast" ? : null} + + + ) +} diff --git a/components/ContentType/HotelPage/AmenitiesList/index.tsx b/components/ContentType/HotelPage/AmenitiesList/index.tsx index 20bad025d..27448827d 100644 --- a/components/ContentType/HotelPage/AmenitiesList/index.tsx +++ b/components/ContentType/HotelPage/AmenitiesList/index.tsx @@ -10,13 +10,11 @@ import { getLang } from "@/i18n/serverContext" import styles from "./amenitiesList.module.css" -import { HotelData } from "@/types/hotel" +import type { AmenitiesListProps } from "@/types/components/hotelPage/amenities" export default async function AmenitiesList({ detailedFacilities, -}: { - detailedFacilities: HotelData["data"]["attributes"]["detailedFacilities"] -}) { +}: AmenitiesListProps) { const intl = await getIntl() const sortedAmenities = detailedFacilities .sort((a, b) => b.sortOrder - a.sortOrder) diff --git a/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx b/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx new file mode 100644 index 000000000..c1bf12c31 --- /dev/null +++ b/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx @@ -0,0 +1,46 @@ +import { activities } from "@/constants/routes/hotelPageParams" + +import Card from "@/components/TempDesignSystem/Card" +import CardImage from "@/components/TempDesignSystem/Card/CardImage" +import Grids from "@/components/TempDesignSystem/Grids" +import { getLang } from "@/i18n/serverContext" + +import styles from "./cardGrid.module.css" + +import type { ActivityCard } from "@/types/trpc/routers/contentstack/hotelPage" +import type { CardProps } from "@/components/TempDesignSystem/Card/card" + +export default function ActivitiesCardGrid(activitiesCard: ActivityCard) { + const lang = getLang() + const hasImage = activitiesCard.backgroundImage + + const updatedCard: CardProps = { + ...activitiesCard, + id: activities[lang], + theme: hasImage ? "image" : "primaryDark", + primaryButton: hasImage + ? { + href: activitiesCard.contentPage.href, + title: activitiesCard.ctaText, + isExternal: false, + } + : undefined, + secondaryButton: hasImage + ? undefined + : { + href: activitiesCard.contentPage.href, + title: activitiesCard.ctaText, + isExternal: false, + }, + } + return ( +
+ + + + + + +
+ ) +} diff --git a/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css b/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css index c205f68fe..7c97fb3bd 100644 --- a/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css +++ b/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css @@ -1,31 +1,32 @@ -.one { +.spanOne { grid-column: span 1; } -.two { +.spanTwo { grid-column: span 2; } -.three { - grid-column: 1/-1; +.spanThree { + grid-column: span 3; } -.desktopGrid { +section .desktopGrid { display: none; } -.mobileGrid { +section .mobileGrid { display: grid; gap: var(--Spacing-x-quarter); } @media screen and (min-width: 768px) { - .desktopGrid { + section .desktopGrid { display: grid; gap: var(--Spacing-x1); + grid-template-columns: repeat(3, 1fr); } - .mobileGrid { + section .mobileGrid { display: none; } } diff --git a/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx b/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx index 8d98c689d..8bb1dff40 100644 --- a/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx +++ b/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx @@ -1,29 +1,35 @@ import Card from "@/components/TempDesignSystem/Card" import CardImage from "@/components/TempDesignSystem/Card/CardImage" import Grids from "@/components/TempDesignSystem/Grids" -import { sortCards } from "@/utils/imageCard" +import { filterFacilityCards, isFacilityCard } from "@/utils/facilityCards" import styles from "./cardGrid.module.css" -import type { CardGridProps } from "@/types/components/hotelPage/facilities" +import type { + CardGridProps, + FacilityCardType, +} from "@/types/components/hotelPage/facilities" + +export default function FacilitiesCardGrid({ + facilitiesCardGrid, +}: CardGridProps) { + const imageCard = filterFacilityCards(facilitiesCardGrid) + const nrCards = facilitiesCardGrid.length + + function getCardClassName(card: FacilityCardType): string { + if (nrCards === 1) { + return styles.spanThree + } else if (nrCards === 2 && !isFacilityCard(card)) { + return styles.spanTwo + } + return styles.spanOne + } -export default async function CardGrid({ facility }: CardGridProps) { - const imageCard = sortCards(facility) return ( -
+
- {facility.map((card: any, idx: number) => ( - + {facilitiesCardGrid.map((card: FacilityCardType) => ( + ))} diff --git a/components/ContentType/HotelPage/Facilities/index.tsx b/components/ContentType/HotelPage/Facilities/index.tsx index 8775d0de2..3f43cc328 100644 --- a/components/ContentType/HotelPage/Facilities/index.tsx +++ b/components/ContentType/HotelPage/Facilities/index.tsx @@ -1,17 +1,56 @@ import SectionContainer from "@/components/Section/Container" +import { getIntl } from "@/i18n" +import { isFacilityCard, setFacilityCardGrids } from "@/utils/facilityCards" -import CardGrid from "./CardGrid" +import ActivitiesCardGrid from "./CardGrid/ActivitiesCardGrid" +import FacilitiesCardGrid from "./CardGrid" import styles from "./facilities.module.css" -import type { FacilityProps } from "@/types/components/hotelPage/facilities" +import type { + Facilities, + FacilitiesProps, + FacilityCardType, + FacilityGrid, +} from "@/types/components/hotelPage/facilities" + +export default async function Facilities({ + facilities, + activitiesCard, +}: FacilitiesProps) { + const intl = await getIntl() + + const facilityCardGrids = setFacilityCardGrids(facilities) + + const translatedFacilityGrids: Facilities = facilityCardGrids.map( + (cardGrid: FacilityGrid) => { + return cardGrid.map((card: FacilityCardType) => { + if (isFacilityCard(card)) { + return { + ...card, + heading: intl.formatMessage({ id: card.heading }), + secondaryButton: { + ...card.secondaryButton, + title: intl.formatMessage({ + id: card.secondaryButton.title, + }), + }, + } + } + return card + }) + } + ) -export default async function Facilities({ facilities }: FacilityProps) { return ( - {facilities.map((facility: any, idx: number) => ( - + {translatedFacilityGrids.map((cardGrid: FacilityGrid) => ( + ))} + {activitiesCard && } ) } diff --git a/components/ContentType/HotelPage/Facilities/mockData.ts b/components/ContentType/HotelPage/Facilities/mockData.ts deleted file mode 100644 index dfea0a74b..000000000 --- a/components/ContentType/HotelPage/Facilities/mockData.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { - activities, - meetingsAndConferences, - restaurantAndBar, - wellnessAndExercise, -} from "@/constants/routes/hotelPageParams" - -import { getLang } from "@/i18n/serverContext" - -import type { Facilities } from "@/types/components/hotelPage/facilities" - -const lang = getLang() -/* -Most of this will be available from the api. Some will need to come from Contentstack. "Activities" will most likely come from Contentstack, which is prepped for. - */ -export const MOCK_FACILITIES: Facilities = [ - [ - { - id: "restaurant-and-bar", - theme: "primaryDark", - scriptedTopTitle: "Restaurant & Bar", - heading: "Enjoy relaxed restaurant experience", - secondaryButton: { - href: `?s=${restaurantAndBar[lang]}`, - title: "Read more & book a table", - isExternal: false, - }, - columnSpan: "one", - }, - { - backgroundImage: { - url: "https://imagevault.scandichotels.com/publishedmedia/79xttlmnum0kjbwhyh18/scandic-helsinki-hub-restaurant-food-tuna.jpg", - title: "scandic-helsinki-hub-restaurant-food-tuna.jpg", - meta: { - alt: "food in restaurant at scandic helsinki hub", - caption: "food in restaurant at scandic helsinki hub", - }, - id: 81751, - dimensions: { - width: 5935, - height: 3957, - aspectRatio: 1.499873641647713, - }, - }, - columnSpan: "one", - }, - { - backgroundImage: { - url: "https://imagevault.scandichotels.com/publishedmedia/48sb3eyhhzj727l2j1af/Scandic-helsinki-hub-II-centro-41.jpg", - meta: { - alt: "restaurant il centro at scandic helsinki hu", - caption: "restaurant il centro at scandic helsinki hub", - }, - id: 82457, - title: "Scandic-helsinki-hub-II-centro-41.jpg", - dimensions: { - width: 4200, - height: 2800, - aspectRatio: 1.5, - }, - }, - columnSpan: "one", - }, - ], - [ - { - backgroundImage: { - url: "https://imagevault.scandichotels.com/publishedmedia/csef06n329hjfiet1avj/Scandic-spectrum-8.jpg", - meta: { - alt: "man with a laptop", - caption: "man with a laptop", - }, - id: 82713, - title: "Scandic-spectrum-8.jpg", - dimensions: { - width: 7499, - height: 4999, - aspectRatio: 1.500100020004001, - }, - }, - columnSpan: "two", - }, - { - id: "meetings-and-conferences", - theme: "primaryDim", - scriptedTopTitle: "Meetings & Conferences", - heading: "Events that make an impression", - secondaryButton: { - href: `?s=${meetingsAndConferences[lang]}`, - title: "About meetings & conferences", - isExternal: false, - }, - columnSpan: "one", - }, - ], - [ - { - id: "wellness-and-exercise", - theme: "one", - scriptedTopTitle: "Wellness & Exercise", - heading: "Sauna and gym", - secondaryButton: { - href: `?s=${wellnessAndExercise[lang]}`, - title: "Read more about wellness & exercise", - isExternal: false, - }, - columnSpan: "one", - }, - { - backgroundImage: { - url: "https://imagevault.scandichotels.com/publishedmedia/69acct5i3pk5be7d6ub0/scandic-helsinki-hub-sauna.jpg", - meta: { - alt: "sauna at scandic helsinki hub", - caption: "sauna at scandic helsinki hub", - }, - id: 81814, - title: "scandic-helsinki-hub-sauna.jpg", - dimensions: { - width: 4000, - height: 2667, - aspectRatio: 1.4998125234345707, - }, - }, - columnSpan: "one", - }, - { - backgroundImage: { - url: "https://imagevault.scandichotels.com/publishedmedia/eu70o6z85idy24r92ysf/Scandic-Helsinki-Hub-gym-22.jpg", - meta: { - alt: "Gym at hotel Scandic Helsinki Hub", - caption: "Gym at hotel Scandic Helsinki Hub", - }, - id: 81867, - title: "Scandic-Helsinki-Hub-gym-22.jpg", - dimensions: { - width: 4000, - height: 2667, - aspectRatio: 1.4998125234345707, - }, - }, - columnSpan: "one", - }, - ], -] diff --git a/components/ContentType/HotelPage/Facilities/utils.ts b/components/ContentType/HotelPage/Facilities/utils.ts deleted file mode 100644 index 1086e7430..000000000 --- a/components/ContentType/HotelPage/Facilities/utils.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Facility } from "@/types/components/hotelPage/facilities" -import type { ActivityCard } from "@/types/trpc/routers/contentstack/hotelPage" - -export function setActivityCard(activitiesCard: ActivityCard): Facility { - const hasImage = !!activitiesCard.background_image - return [ - { - id: "activities", - theme: hasImage ? "image" : "primaryDark", - scriptedTopTitle: activitiesCard.scripted_title, - heading: activitiesCard.heading, - bodyText: activitiesCard.body_text, - backgroundImage: hasImage ? activitiesCard.background_image : undefined, - primaryButton: hasImage - ? { - href: activitiesCard.contentPage.href, - title: activitiesCard.cta_text, - isExternal: false, - } - : undefined, - secondaryButton: hasImage - ? undefined - : { - href: activitiesCard.contentPage.href, - title: activitiesCard.cta_text, - isExternal: false, - }, - columnSpan: "three", - }, - ] -} - -export function getCardTheme() { - // TODO -} diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx index 4e6c6624d..967580157 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx +++ b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx @@ -10,7 +10,7 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./roomCard.module.css" -import { RoomCardProps } from "@/types/components/hotelPage/roomCard" +import type { RoomCardProps } from "@/types/components/hotelPage/roomCard" export function RoomCard({ badgeTextTransKey, diff --git a/components/ContentType/HotelPage/Rooms/index.tsx b/components/ContentType/HotelPage/Rooms/index.tsx index 58cc2ef68..b6fd241f5 100644 --- a/components/ContentType/HotelPage/Rooms/index.tsx +++ b/components/ContentType/HotelPage/Rooms/index.tsx @@ -10,10 +10,11 @@ import Button from "@/components/TempDesignSystem/Button" import Grids from "@/components/TempDesignSystem/Grids" import { RoomCard } from "./RoomCard" -import { RoomsProps } from "./types" import styles from "./rooms.module.css" +import type { RoomsProps } from "./types" + export function Rooms({ rooms }: RoomsProps) { const intl = useIntl() const [allRoomsVisible, setAllRoomsVisible] = useState(false) diff --git a/components/ContentType/HotelPage/TabNavigation/index.tsx b/components/ContentType/HotelPage/TabNavigation/index.tsx index e594d757c..346db0521 100644 --- a/components/ContentType/HotelPage/TabNavigation/index.tsx +++ b/components/ContentType/HotelPage/TabNavigation/index.tsx @@ -6,21 +6,38 @@ import useHash from "@/hooks/useHash" import styles from "./tabNavigation.module.css" -import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation" +import { + HotelHashValues, + type TabNavigationProps, +} from "@/types/components/hotelPage/tabNavigation" -export default function TabNavigation() { +export default function TabNavigation({ restaurantTitle }: TabNavigationProps) { const hash = useHash() const intl = useIntl() - const hotelTabLinks: { href: HotelHashValues; text: string }[] = [ - // TODO these titles will need to reflect the facility card titles, which will vary between hotels - { href: HotelHashValues.overview, text: "Overview" }, - { href: HotelHashValues.rooms, text: "Rooms" }, - { href: HotelHashValues.restaurant, text: "Restaurant & Bar" }, - { href: HotelHashValues.meetings, text: "Meetings & Conferences" }, - { href: HotelHashValues.wellness, text: "Wellness & Exercise" }, - { href: HotelHashValues.activities, text: "Activities" }, - { href: HotelHashValues.faq, text: "FAQ" }, + const hotelTabLinks: { href: HotelHashValues | string; text: string }[] = [ + { + href: HotelHashValues.overview, + text: intl.formatMessage({ id: "Overview" }), + }, + { href: HotelHashValues.rooms, text: intl.formatMessage({ id: "Rooms" }) }, + { + href: HotelHashValues.restaurant, + text: intl.formatMessage({ id: restaurantTitle }, { count: 1 }), + }, + { + href: HotelHashValues.meetings, + text: intl.formatMessage({ id: "Meetings & Conferences" }), + }, + { + href: HotelHashValues.wellness, + text: intl.formatMessage({ id: "Wellness & Exercise" }), + }, + { + href: HotelHashValues.activities, + text: intl.formatMessage({ id: "Activities" }), + }, + { href: HotelHashValues.faq, text: intl.formatMessage({ id: "FAQ" }) }, ] return ( diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index fe0a20caa..04a56a865 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -6,9 +6,8 @@ import SidePeekProvider from "@/components/SidePeekProvider" import SidePeek from "@/components/TempDesignSystem/SidePeek" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import { getRestaurantHeading } from "@/utils/facilityCards" -import { MOCK_FACILITIES } from "./Facilities/mockData" -import { setActivityCard } from "./Facilities/utils" import DynamicMap from "./Map/DynamicMap" import MapCard from "./Map/MapCard" import MobileMapToggle from "./Map/MobileMapToggle" @@ -45,10 +44,9 @@ export default async function HotelPage() { roomCategories, activitiesCard, pointsOfInterest, + facilities, } = hotelData - const facilities = [...MOCK_FACILITIES] - activitiesCard && facilities.push(setActivityCard(activitiesCard)) const topThreePois = pointsOfInterest.slice(0, 3) const coordinates = { @@ -61,7 +59,9 @@ export default async function HotelPage() {
- +
- +
{googleMapsApiKey ? ( <> diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index 08c7a2d11..1f2660770 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -9,6 +9,7 @@ import useDropdownStore from "@/stores/main-menu" import { GiftIcon, SearchIcon, ServiceIcon } from "@/components/Icons" import LanguageSwitcher from "@/components/LanguageSwitcher" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" +import useMediaQuery from "@/hooks/useMediaQuery" import HeaderLink from "../../HeaderLink" @@ -37,6 +38,13 @@ export default function MobileMenu({ isHeaderLanguageSwitcherMobileOpen || isFooterLanguageSwitcherOpen + const isAboveMobile = useMediaQuery("(min-width: 768px)") + useEffect(() => { + if (isAboveMobile && isHamburgerMenuOpen) { + toggleDropdown(DropdownTypeEnum.HamburgerMenu) + } + }, [isAboveMobile, isHamburgerMenuOpen, toggleDropdown]) + useHandleKeyUp((event: KeyboardEvent) => { if (event.key === "Escape" && isHamburgerMenuOpen) { toggleDropdown(DropdownTypeEnum.HamburgerMenu) diff --git a/components/Header/MainMenu/MobileMenu/mobileMenu.module.css b/components/Header/MainMenu/MobileMenu/mobileMenu.module.css index 797b3b427..0d4be7cbc 100644 --- a/components/Header/MainMenu/MobileMenu/mobileMenu.module.css +++ b/components/Header/MainMenu/MobileMenu/mobileMenu.module.css @@ -97,7 +97,8 @@ } @media screen and (min-width: 768px) { - .hamburger { + .hamburger, + .modal { display: none; } } diff --git a/components/Header/MainMenu/MyPagesMenu/index.tsx b/components/Header/MainMenu/MyPagesMenu/index.tsx index 6f01ae2ff..fb739399c 100644 --- a/components/Header/MainMenu/MyPagesMenu/index.tsx +++ b/components/Header/MainMenu/MyPagesMenu/index.tsx @@ -1,11 +1,13 @@ "use client" +import { useRef } from "react" import { useIntl } from "react-intl" import useDropdownStore from "@/stores/main-menu" import { ChevronDownIcon } from "@/components/Icons" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import useClickOutside from "@/hooks/useClickOutside" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import { getInitials } from "@/utils/user" @@ -22,8 +24,10 @@ export default function MyPagesMenu({ membership, navigation, user, + membershipLevel, }: MyPagesMenuProps) { const intl = useIntl() + const myPagesMenuRef = useRef(null) const { toggleDropdown, isMyPagesMenuOpen } = useDropdownStore() @@ -33,8 +37,12 @@ export default function MyPagesMenu({ } }) + useClickOutside(myPagesMenuRef, isMyPagesMenuOpen, () => { + toggleDropdown(DropdownTypeEnum.MyPagesMenu) + }) + return ( -
+
toggleDropdown(DropdownTypeEnum.MyPagesMenu)} > @@ -50,6 +58,7 @@ export default function MyPagesMenu({ {isMyPagesMenuOpen ? (
{user ? ( <> { + if (isAboveMobile && isMyPagesMobileMenuOpen) { + toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu) + } + }, [isAboveMobile, isMyPagesMobileMenuOpen, toggleDropdown]) + // Making sure the menu is always opened at the top of the page, just below the header. useEffect(() => { if (isMyPagesMobileMenuOpen) { @@ -54,6 +63,7 @@ export default function MyPagesMobileMenu({ aria-label={intl.formatMessage({ id: "My pages menu" })} >
diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx index 222eb21e9..c57064b2a 100644 --- a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx @@ -1,9 +1,12 @@ "use client" +import { useRef } from "react" + import useDropdownStore from "@/stores/main-menu" import { ChevronDownIcon, ChevronRightIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" +import useClickOutside from "@/hooks/useClickOutside" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import MainMenuButton from "../../MainMenuButton" @@ -15,8 +18,10 @@ import type { NavigationMenuItemProps } from "@/types/components/header/navigati export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) { const { openMegaMenu, toggleMegaMenu } = useDropdownStore() + const megaMenuRef = useRef(null) const { submenu, title, link, seeAllLink, card } = item - const isMegaMenuOpen = openMegaMenu === title + const megaMenuTitle = `${title}-${isMobile ? "mobile" : "desktop"}` + const isMegaMenuOpen = openMegaMenu === megaMenuTitle useHandleKeyUp((event: KeyboardEvent) => { if (event.key === "Escape" && isMegaMenuOpen) { @@ -24,10 +29,14 @@ export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) { } }) + useClickOutside(megaMenuRef, isMegaMenuOpen && !isMobile, () => { + toggleMegaMenu(false) + }) + return submenu.length ? ( <> toggleMegaMenu(title)} + onClick={() => toggleMegaMenu(megaMenuTitle)} className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : styles.desktop}`} > {title} @@ -41,6 +50,7 @@ export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) { )}
{isMegaMenuOpen ? ( diff --git a/components/HotelReservation/EnterDetails/BedType/bedOptions.module.css b/components/HotelReservation/EnterDetails/BedType/bedOptions.module.css new file mode 100644 index 000000000..81fd223b9 --- /dev/null +++ b/components/HotelReservation/EnterDetails/BedType/bedOptions.module.css @@ -0,0 +1,7 @@ +.form { + display: grid; + gap: var(--Spacing-x2); + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); + padding-bottom: var(--Spacing-x3); + width: min(600px, 100%); +} diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx new file mode 100644 index 000000000..61633cdcf --- /dev/null +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -0,0 +1,72 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { KingBedIcon } from "@/components/Icons" +import RadioCard from "@/components/TempDesignSystem/Form/Card/Radio" + +import { bedTypeSchema } from "./schema" + +import styles from "./bedOptions.module.css" + +import type { BedTypeSchema } from "@/types/components/enterDetails/bedType" +import { bedTypeEnum } from "@/types/enums/bedType" + +export default function BedType() { + const intl = useIntl() + + const methods = useForm({ + criteriaMode: "all", + mode: "all", + resolver: zodResolver(bedTypeSchema), + reValidateMode: "onChange", + }) + + // @ts-expect-error - Types mismatch docs as this is + // a pattern that is allowed https://formatjs.io/docs/react-intl/api#usage + const text = intl.formatMessage( + { id: "Included (based on availability)" }, + { b: (str) => {str} } + ) + + return ( + +
+ + + +
+ ) +} diff --git a/components/HotelReservation/EnterDetails/BedType/schema.ts b/components/HotelReservation/EnterDetails/BedType/schema.ts new file mode 100644 index 000000000..bd819b986 --- /dev/null +++ b/components/HotelReservation/EnterDetails/BedType/schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod" + +import { bedTypeEnum } from "@/types/enums/bedType" + +export const bedTypeSchema = z.object({ + bed: z.nativeEnum(bedTypeEnum), +}) diff --git a/components/HotelReservation/EnterDetails/Breakfast/breakfast.module.css b/components/HotelReservation/EnterDetails/Breakfast/breakfast.module.css new file mode 100644 index 000000000..81fd223b9 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Breakfast/breakfast.module.css @@ -0,0 +1,7 @@ +.form { + display: grid; + gap: var(--Spacing-x2); + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); + padding-bottom: var(--Spacing-x3); + width: min(600px, 100%); +} diff --git a/components/HotelReservation/EnterDetails/Breakfast/index.tsx b/components/HotelReservation/EnterDetails/Breakfast/index.tsx new file mode 100644 index 000000000..b8f00ec83 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Breakfast/index.tsx @@ -0,0 +1,70 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { BreakfastIcon, NoBreakfastIcon } from "@/components/Icons" +import RadioCard from "@/components/TempDesignSystem/Form/Card/Radio" + +import { breakfastSchema } from "./schema" + +import styles from "./breakfast.module.css" + +import type { BreakfastSchema } from "@/types/components/enterDetails/breakfast" +import { breakfastEnum } from "@/types/enums/breakfast" + +export default function Breakfast() { + const intl = useIntl() + + const methods = useForm({ + criteriaMode: "all", + mode: "all", + resolver: zodResolver(breakfastSchema), + reValidateMode: "onChange", + }) + + return ( + +
+ {amount} {currency}/night per adult" }, + { + amount: "150", + b: (str) => {str}, + currency: "SEK", + } + )} + text={intl.formatMessage({ + id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", + })} + title={intl.formatMessage({ id: "Breakfast buffet" })} + value={breakfastEnum.BREAKFAST} + /> + + +
+ ) +} diff --git a/components/HotelReservation/EnterDetails/Breakfast/schema.ts b/components/HotelReservation/EnterDetails/Breakfast/schema.ts new file mode 100644 index 000000000..34cc5efca --- /dev/null +++ b/components/HotelReservation/EnterDetails/Breakfast/schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod" + +import { breakfastEnum } from "@/types/enums/breakfast" + +export const breakfastSchema = z.object({ + breakfast: z.nativeEnum(breakfastEnum), +}) diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index 731e3b205..2dd1b1cdb 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -4,7 +4,7 @@ import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import Button from "@/components/TempDesignSystem/Button" -import CheckboxCard from "@/components/TempDesignSystem/Form/Checkbox/Card" +import CheckboxCard from "@/components/TempDesignSystem/Form/Card/Checkbox" import CountrySelect from "@/components/TempDesignSystem/Form/Country" import Input from "@/components/TempDesignSystem/Form/Input" import Phone from "@/components/TempDesignSystem/Form/Phone" @@ -36,6 +36,7 @@ export default function Details({ user }: DetailsProps) { lastname: user?.lastName ?? "", phoneNumber: user?.phoneNumber ?? "", }, + criteriaMode: "all", mode: "all", resolver: zodResolver(user ? signedInDetailsSchema : detailsSchema), reValidateMode: "onChange", diff --git a/components/Icons/Breakfast.tsx b/components/Icons/Breakfast.tsx new file mode 100644 index 000000000..dccfc0c39 --- /dev/null +++ b/components/Icons/Breakfast.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function BreakfastIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/KingBed.tsx b/components/Icons/KingBed.tsx new file mode 100644 index 000000000..d4df0f225 --- /dev/null +++ b/components/Icons/KingBed.tsx @@ -0,0 +1,27 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function KingBedIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + ) +} diff --git a/components/Icons/NoBreakfast.tsx b/components/Icons/NoBreakfast.tsx new file mode 100644 index 000000000..c09af6616 --- /dev/null +++ b/components/Icons/NoBreakfast.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function NoBreakfastIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 04f167061..f26933255 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -4,6 +4,7 @@ export { default as AirplaneIcon } from "./Airplane" export { default as ArrowRightIcon } from "./ArrowRight" export { default as BarIcon } from "./Bar" export { default as BikingIcon } from "./Biking" +export { default as BreakfastIcon } from "./Breakfast" export { default as BusinessIcon } from "./Business" export { default as CalendarIcon } from "./Calendar" export { default as CameraIcon } from "./Camera" @@ -34,11 +35,13 @@ export { default as HeartIcon } from "./Heart" export { default as HouseIcon } from "./House" export { default as ImageIcon } from "./Image" export { default as InfoCircleIcon } from "./InfoCircle" +export { default as KingBedIcon } from "./KingBed" export { default as LocationIcon } from "./Location" export { default as LockIcon } from "./Lock" export { default as MapIcon } from "./Map" export { default as MinusIcon } from "./Minus" export { default as MuseumIcon } from "./Museum" +export { default as NoBreakfastIcon } from "./NoBreakfast" export { default as ParkingIcon } from "./Parking" export { default as People2Icon } from "./People2" export { default as PersonIcon } from "./Person" diff --git a/components/LanguageSwitcher/index.tsx b/components/LanguageSwitcher/index.tsx index b5cb2f25e..7ed5b4d75 100644 --- a/components/LanguageSwitcher/index.tsx +++ b/components/LanguageSwitcher/index.tsx @@ -1,12 +1,13 @@ "use client" -import { useEffect, useRef } from "react" +import { useRef } from "react" import { useIntl } from "react-intl" import { languages } from "@/constants/languages" import useDropdownStore from "@/stores/main-menu" import { ChevronDownIcon, GlobeIcon } from "@/components/Icons" +import useClickOutside from "@/hooks/useClickOutside" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import useLang from "@/hooks/useLang" @@ -28,18 +29,13 @@ export default function LanguageSwitcher({ }: LanguageSwitcherProps) { const intl = useIntl() const currentLanguage = useLang() - const toggleDropdown = useDropdownStore((state) => state.toggleDropdown) + const { + toggleDropdown, + isFooterLanguageSwitcherOpen, + isHeaderLanguageSwitcherMobileOpen, + isHeaderLanguageSwitcherOpen, + } = useDropdownStore() const languageSwitcherRef = useRef(null) - const isFooterLanguageSwitcherOpen = useDropdownStore( - (state) => state.isFooterLanguageSwitcherOpen - ) - const isHeaderLanguageSwitcherOpen = useDropdownStore( - (state) => state.isHeaderLanguageSwitcherOpen - ) - const isHeaderLanguageSwitcherMobileOpen = useDropdownStore( - (state) => state.isHeaderLanguageSwitcherMobileOpen - ) - const isFooter = type === LanguageSwitcherTypesEnum.Footer const isHeader = !isFooter @@ -71,33 +67,11 @@ export default function LanguageSwitcher({ window.scrollTo(0, scrollPosition) }) } - - useEffect(() => { - function handleClickOutside(evt: Event) { - const target = evt.target as HTMLElement - if ( - languageSwitcherRef.current && - target && - !languageSwitcherRef.current.contains(target) && - isLanguageSwitcherOpen && - !isHeaderLanguageSwitcherMobileOpen - ) { - toggleDropdown(dropdownType) - } - } - - if (languageSwitcherRef.current) { - document.addEventListener("click", handleClickOutside) - } - return () => { - document.removeEventListener("click", handleClickOutside) - } - }, [ - dropdownType, - toggleDropdown, - isLanguageSwitcherOpen, - isHeaderLanguageSwitcherMobileOpen, - ]) + useClickOutside( + languageSwitcherRef, + isLanguageSwitcherOpen && !isHeaderLanguageSwitcherMobileOpen, + () => toggleDropdown(dropdownType) + ) const classNames = languageSwitcherVariants({ color, position }) diff --git a/components/Profile/ManagePreferencesButton/index.tsx b/components/Profile/ManagePreferencesButton/index.tsx new file mode 100644 index 000000000..8741888d0 --- /dev/null +++ b/components/Profile/ManagePreferencesButton/index.tsx @@ -0,0 +1,51 @@ +"use client" + +import { useIntl } from "react-intl" + +import { trpc } from "@/lib/trpc/client" + +import ArrowRight from "@/components/Icons/ArrowRight" +import Button from "@/components/TempDesignSystem/Button" +import { toast } from "@/components/TempDesignSystem/Toasts" + +import styles from "./managePreferencesButton.module.css" + +export default function ManagePreferencesButton() { + const intl = useIntl() + const generatePreferencesLink = trpc.user.generatePreferencesLink.useMutation( + { + onSuccess: (preferencesLink) => { + if (preferencesLink) { + window.open(preferencesLink, "_blank") + } else { + toast.error( + intl.formatMessage({ + id: "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.", + }) + ) + } + }, + onError: (e) => { + toast.error( + intl.formatMessage({ + id: "An error occurred trying to manage your preferences, please try again later.", + }) + ) + }, + } + ) + + return ( + + ) +} diff --git a/components/Profile/ManagePreferencesButton/managePreferencesButton.module.css b/components/Profile/ManagePreferencesButton/managePreferencesButton.module.css new file mode 100644 index 000000000..f786a669f --- /dev/null +++ b/components/Profile/ManagePreferencesButton/managePreferencesButton.module.css @@ -0,0 +1,3 @@ +.managePreferencesButton { + justify-self: flex-start; +} diff --git a/components/TempDesignSystem/Card/CardImage/index.tsx b/components/TempDesignSystem/Card/CardImage/index.tsx index dc9c4f7f9..9c3233e87 100644 --- a/components/TempDesignSystem/Card/CardImage/index.tsx +++ b/components/TempDesignSystem/Card/CardImage/index.tsx @@ -14,7 +14,7 @@ export default function CardImage({ return (
- {imageCards.map( + {imageCards?.map( ({ backgroundImage }) => backgroundImage && ( void onSecondaryButtonClick?: () => void + backgroundImage?: ImageVaultAsset | ApiImage } diff --git a/components/TempDesignSystem/Card/index.tsx b/components/TempDesignSystem/Card/index.tsx index 94cc08773..547ab5bea 100644 --- a/components/TempDesignSystem/Card/index.tsx +++ b/components/TempDesignSystem/Card/index.tsx @@ -24,15 +24,17 @@ export default function Card({ backgroundImage, imageHeight, imageWidth, + imageGradient, onPrimaryButtonClick, onSecondaryButtonClick, }: CardProps) { const buttonTheme = getTheme(theme) imageHeight = imageHeight || 320 + imageWidth = imageWidth || - (backgroundImage + (backgroundImage && "dimensions" in backgroundImage ? backgroundImage.dimensions.aspectRatio * imageHeight : 420) @@ -44,7 +46,7 @@ export default function Card({ })} > {backgroundImage && ( -
+
+} diff --git a/components/TempDesignSystem/Form/Card/Radio.tsx b/components/TempDesignSystem/Form/Card/Radio.tsx new file mode 100644 index 000000000..c1de94782 --- /dev/null +++ b/components/TempDesignSystem/Form/Card/Radio.tsx @@ -0,0 +1,7 @@ +import Card from "." + +import type { RadioProps } from "./card" + +export default function RadioCard(props: RadioProps) { + return +} diff --git a/components/TempDesignSystem/Form/Checkbox/Card/card.module.css b/components/TempDesignSystem/Form/Card/card.module.css similarity index 50% rename from components/TempDesignSystem/Form/Checkbox/Card/card.module.css rename to components/TempDesignSystem/Form/Card/card.module.css index bd44b7b45..1044596f6 100644 --- a/components/TempDesignSystem/Form/Checkbox/Card/card.module.css +++ b/components/TempDesignSystem/Form/Card/card.module.css @@ -1,70 +1,72 @@ -.checkbox { +.label { + align-self: flex-start; background-color: var(--Base-Surface-Primary-light-Normal); border: 1px solid var(--Base-Border-Subtle); border-radius: var(--Corner-radius-Large); cursor: pointer; - display: flex; - flex-direction: column; - gap: var(--Spacing-x1); + display: grid; + grid-template-columns: 1fr auto; padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); transition: all 200ms ease; width: min(100%, 600px); } -.checkbox:hover { +.label:hover { background-color: var(--Base-Surface-Secondary-light-Hover); } -.checkbox:has(:checked) { +.label:has(:checked) { background-color: var(--Primary-Light-Surface-Normal); border-color: var(--Base-Border-Hover); } -.header { - align-items: center; - display: grid; - gap: 0px var(--Spacing-x1); - grid-template-areas: - "title icon" - "subtitle icon"; -} - .icon { - grid-area: icon; + align-self: center; + grid-column: 2/3; + grid-row: 1/3; justify-self: flex-end; transition: fill 200ms ease; } -.checkbox:hover .icon, -.checkbox:hover .icon *, -.checkbox:has(:checked) .icon, -.checkbox:has(:checked) .icon * { +.label:hover .icon, +.label:hover .icon *, +.label:has(:checked) .icon, +.label:has(:checked) .icon * { fill: var(--Base-Text-Medium-contrast); } -.checkbox[data-declined="true"]:hover .icon, -.checkbox[data-declined="true"]:hover .icon *, -.checkbox[data-declined="true"]:has(:checked) .icon, -.checkbox[data-declined="true"]:has(:checked) .icon * { +.label[data-declined="true"]:hover .icon, +.label[data-declined="true"]:hover .icon *, +.label[data-declined="true"]:has(:checked) .icon, +.label[data-declined="true"]:has(:checked) .icon * { fill: var(--Base-Text-Disabled); } .subtitle { - grid-area: subtitle; + grid-column: 1 / 2; + grid-row: 2; } .title { - grid-area: title; + grid-column: 1 / 2; } -.list { - list-style: none; - margin: 0; - padding: 0; +.label .text { + margin-top: var(--Spacing-x1); + grid-column: 1/-1; } .listItem { align-items: center; display: flex; gap: var(--Spacing-x-quarter); + grid-column: 1/-1; +} + +.listItem:first-of-type { + margin-top: var(--Spacing-x1); +} + +.listItem:nth-of-type(n + 2) { + margin-top: var(--Spacing-x-quarter); } diff --git a/components/TempDesignSystem/Form/Card/card.ts b/components/TempDesignSystem/Form/Card/card.ts new file mode 100644 index 000000000..167595164 --- /dev/null +++ b/components/TempDesignSystem/Form/Card/card.ts @@ -0,0 +1,35 @@ +import type { IconProps } from "@/types/components/icon" + +interface BaseCardProps extends React.LabelHTMLAttributes { + Icon?: (props: IconProps) => JSX.Element + declined?: boolean + iconHeight?: number + iconWidth?: number + name?: string + saving?: boolean + subtitle?: string + title: string + type: "checkbox" | "radio" + value?: string +} + +interface ListCardProps extends BaseCardProps { + list: { + title: string + }[] + text?: never +} + +interface TextCardProps extends BaseCardProps { + list?: never + text: string +} + +export type CardProps = ListCardProps | TextCardProps + +export type CheckboxProps = + | Omit + | Omit +export type RadioProps = + | Omit + | Omit diff --git a/components/TempDesignSystem/Form/Card/index.tsx b/components/TempDesignSystem/Form/Card/index.tsx new file mode 100644 index 000000000..82f99e80d --- /dev/null +++ b/components/TempDesignSystem/Form/Card/index.tsx @@ -0,0 +1,77 @@ +"use client" + +import { CheckIcon, CloseIcon, HeartIcon } from "@/components/Icons" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" + +import styles from "./card.module.css" + +import type { CardProps } from "./card" + +export default function Card({ + Icon = HeartIcon, + iconHeight = 32, + iconWidth = 32, + declined = false, + id, + list, + name = "join", + saving = false, + subtitle, + text, + title, + type, + value, +}: CardProps) { + return ( + + ) +} diff --git a/components/TempDesignSystem/Form/Checkbox/Card/card.ts b/components/TempDesignSystem/Form/Checkbox/Card/card.ts deleted file mode 100644 index 438c9b729..000000000 --- a/components/TempDesignSystem/Form/Checkbox/Card/card.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IconProps } from "@/types/components/icon" - -export interface CheckboxCardProps { - Icon?: (props: IconProps) => JSX.Element - declined?: boolean - list?: { - title: string - }[] - name?: string - saving?: boolean - subtitle?: string - text?: string - title: string -} diff --git a/components/TempDesignSystem/Form/Checkbox/Card/index.tsx b/components/TempDesignSystem/Form/Checkbox/Card/index.tsx deleted file mode 100644 index 77158c531..000000000 --- a/components/TempDesignSystem/Form/Checkbox/Card/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -"use client" - -import { CheckIcon, CloseIcon, HeartIcon } from "@/components/Icons" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Footnote from "@/components/TempDesignSystem/Text/Footnote" - -import styles from "./card.module.css" - -import type { CheckboxCardProps } from "./card" - -export default function CheckboxCard({ - Icon = HeartIcon, - declined = false, - list, - name = "join", - saving = false, - subtitle, - text, - title, -}: CheckboxCardProps) { - return ( - - ) -} diff --git a/constants/routes/hotelPageParams.js b/constants/routes/hotelPageParams.js index 9eadf2996..5ee71737d 100644 --- a/constants/routes/hotelPageParams.js +++ b/constants/routes/hotelPageParams.js @@ -45,13 +45,40 @@ export const meetingsAndConferences = { export const restaurantAndBar = { en: "restaurant-and-bar", - sv: "restaurant-och-bar", + sv: "restaurang-och-bar", no: "restaurant-og-bar", da: "restaurant-og-bar", fi: "ravintola-ja-baari", de: "Restaurant-und-Bar", } +/*export const restaurant = { + en: "restaurant", + sv: "restaurang", + no: "restaurant", + da: "restaurant", + fi: "ravintola", + de: "Restaurant", +} + +export const bar = { + en: "bar", + sv: "bar", + no: "bar", + da: "bar", + fi: "baari", + de: "Bar", +} + +export const breakfastRestaurant = { + en: "breakfast-restaurant", + sv: "frukostrestaurang", + no: "frokostrestaurant", + da: "morgenmadsrestaurant", + fi: "aamiaisravintola", + de: "Frühstücksrestaurant", +} +*/ const params = { about, amenities, @@ -59,6 +86,9 @@ const params = { activities, meetingsAndConferences, restaurantAndBar, + /*bar, + restaurant, + breakfastRestaurant,*/ } export default params diff --git a/env/server.ts b/env/server.ts index 751143e62..b32150a1d 100644 --- a/env/server.ts +++ b/env/server.ts @@ -47,6 +47,7 @@ export const env = createEnv({ .default("false"), PUBLIC_URL: z.string().optional(), REVALIDATE_SECRET: z.string(), + SALESFORCE_PREFERENCE_BASE_URL: z.string(), SEAMLESS_LOGIN_DA: z.string(), SEAMLESS_LOGIN_DE: z.string(), SEAMLESS_LOGIN_EN: z.string(), @@ -104,6 +105,7 @@ export const env = createEnv({ PRINT_QUERY: process.env.PRINT_QUERY, PUBLIC_URL: process.env.PUBLIC_URL, REVALIDATE_SECRET: process.env.REVALIDATE_SECRET, + SALESFORCE_PREFERENCE_BASE_URL: process.env.SALESFORCE_PREFERENCE_BASE_URL, SEAMLESS_LOGIN_DA: process.env.SEAMLESS_LOGIN_DA, SEAMLESS_LOGIN_DE: process.env.SEAMLESS_LOGIN_DE, SEAMLESS_LOGIN_EN: process.env.SEAMLESS_LOGIN_EN, diff --git a/hooks/useClickOutside.ts b/hooks/useClickOutside.ts new file mode 100644 index 000000000..b9862c059 --- /dev/null +++ b/hooks/useClickOutside.ts @@ -0,0 +1,24 @@ +import { useEffect } from "react" + +export default function useClickOutside( + ref: React.RefObject, + isOpen: boolean, + callback: () => void +) { + useEffect(() => { + function handleClickOutside(evt: Event) { + const target = evt.target as HTMLElement + if (ref.current && target && !ref.current.contains(target) && isOpen) { + callback() + } + } + + if (isOpen) { + document.addEventListener("click", handleClickOutside) + } + + return () => { + document.removeEventListener("click", handleClickOutside) + } + }, [ref, isOpen, callback]) +} diff --git a/hooks/useMediaQuery.ts b/hooks/useMediaQuery.ts new file mode 100644 index 000000000..bbb9eabac --- /dev/null +++ b/hooks/useMediaQuery.ts @@ -0,0 +1,21 @@ +import { useEffect, useState } from "react" + +function useMediaQuery(query: string) { + const [isMatch, setIsMatch] = useState(false) + + useEffect(() => { + const media = window.matchMedia(query) + if (media.matches !== isMatch) { + setIsMatch(media.matches) + } + + const listener = () => setIsMatch(media.matches) + media.addEventListener("change", listener) + + return () => media.removeEventListener("change", listener) + }, [isMatch, query]) + + return isMatch +} + +export default useMediaQuery diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index e78161f87..26897f291 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -1,15 +1,20 @@ { + "Included (based on availability)": "Inkluderet (baseret på tilgængelighed)", + "{amount} {currency}/night per adult": "{amount} {currency}/nat pr. voksen", "A destination or hotel name is needed to be able to search for a hotel room.": "Et destinations- eller hotelnavn er nødvendigt for at kunne søge efter et hotelværelse.", "A photo of the room": "Et foto af værelset", + "About meetings & conferences": "About meetings & conferences", "Activities": "Aktiviteter", "Add code": "Tilføj kode", "Add new card": "Tilføj nyt kort", "Address": "Adresse", "Airport": "Lufthavn", + "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle vores morgenmadsbuffeter tilbyder glutenfrie, veganske og allergivenlige muligheder.", "Already a friend?": "Allerede en ven?", "Amenities": "Faciliteter", "Amusement park": "Forlystelsespark", "An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.", + "An error occurred trying to manage your preferences, please try again later.": "Der opstod en fejl under forsøget på at administrere dine præferencer. Prøv venligst igen senere.", "An error occurred when trying to update profile.": "Der opstod en fejl under forsøg på at opdatere profilen.", "Any changes you've made will be lost.": "Alle ændringer, du har foretaget, går tabt.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på, at du vil fjerne kortet, der slutter me {lastFourDigits} fra din medlemsprofil?", @@ -21,15 +26,18 @@ "At the hotel": "På hotellet", "Attraction": "Attraktion", "Back to scandichotels.com": "Tilbage til scandichotels.com", + "Bar": "Bar", "Bed type": "Seng type", "Book": "Book", "Book reward night": "Book bonusnat", "Booking number": "Bookingnummer", "Breakfast": "Morgenmad", + "Breakfast buffet": "Morgenbuffet", "Breakfast excluded": "Morgenmad ikke inkluderet", "Breakfast included": "Morgenmad inkluderet", "Bus terminal": "Busstation", "Business": "Forretning", + "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Afbestille", "Check in": "Check ind", "Check out": "Check ud", @@ -61,6 +69,7 @@ "Date of Birth": "Fødselsdato", "Day": "Dag", "Description": "Beskrivelse", + "Destination": "Destination", "Destinations & hotels": "Destinationer & hoteller", "Disabled booking options header": "Vi beklager", "Disabled booking options text": "Koder, checks og bonusnætter er endnu ikke tilgængelige på den nye hjemmeside.", @@ -73,7 +82,10 @@ "Edit": "Redigere", "Edit profile": "Rediger profil", "Email": "E-mail", + "Email address": "E-mailadresse", "Enter destination or hotel": "Indtast destination eller hotel", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", + "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Udforsk alle niveauer og fordele", "Explore nearby": "Udforsk i nærheden", "Extras to your booking": "Tillæg til din booking", @@ -82,6 +94,7 @@ "Fair": "Messe", "Find booking": "Find booking", "Find hotels": "Find hotel", + "Firstname": "Fornavn", "Flexibility": "Fleksibilitet", "Former Scandic Hotel": "Tidligere Scandic Hotel", "Free cancellation": "Gratis afbestilling", @@ -91,6 +104,7 @@ "Get member benefits & offers": "Få medlemsfordele og tilbud", "Go back to edit": "Gå tilbage til redigering", "Go back to overview": "Gå tilbage til oversigten", + "Guest information": "Gæsteinformation", "Guests & Rooms": "Gæster & værelser", "Hi": "Hei", "Highest level": "Højeste niveau", @@ -102,10 +116,13 @@ "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det virker", "Image gallery": "Billedgalleri", + "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.", "Join Scandic Friends": "Tilmeld dig Scandic Friends", "Join at no cost": "Tilmeld dig uden omkostninger", + "King bed": "Kingsize-seng", "km to city center": "km til byens centrum", "Language": "Sprog", + "Lastname": "Efternavn", "Latest searches": "Seneste søgninger", "Level": "Niveau", "Level 1": "Niveau 1", @@ -149,6 +166,7 @@ "New password": "Nyt kodeord", "Next": "Næste", "Nights needed to level up": "Nætter nødvendige for at komme i niveau", + "No breakfast": "Ingen morgenmad", "No content published": "Intet indhold offentliggjort", "No matching location found": "Der blev ikke fundet nogen matchende placering", "No results": "Ingen resultater", @@ -181,17 +199,24 @@ "Points needed to level up": "Point nødvendige for at stige i niveau", "Points needed to stay on level": "Point nødvendige for at holde sig på niveau", "Previous victories": "Tidligere sejre", + "Proceed to payment method": "Fortsæt til betalingsmetode", "Public price from": "Offentlig pris fra", "Public transport": "Offentlig transport", + "Queen bed": "Queensize-seng", "Read more": "Læs mere", + "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Læs mere om hotellet", + "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", - "Restaurant": "Restaurant", + "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", + "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Gentag den nye adgangskode", "Room & Terms": "Værelse & Vilkår", "Room facilities": "Værelsesfaciliteter", "Rooms": "Værelser", + "Rooms & Guests": "Værelser & gæster", + "Sauna and gym": "Sauna and gym", "Save": "Gemme", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", @@ -203,6 +228,7 @@ "Select a country": "Vælg et land", "Select country of residence": "Vælg bopælsland", "Select date of birth": "Vælg fødselsdato", + "Select dates": "Vælg datoer", "Select language": "Vælg sprog", "Select your language": "Vælg dit sprog", "Shopping": "Shopping", @@ -258,6 +284,7 @@ "Year": "År", "Yes, discard changes": "Ja, kasser ændringer", "Yes, remove my card": "Ja, fjern mit kort", + "You can always change your mind later and add breakfast at the hotel.": "Du kan altid ombestemme dig senere og tilføje morgenmad på hotellet.", "You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.", "You have no previous stays.": "Du har ingen tidligere ophold.", "You have no upcoming stays.": "Du har ingen kommende ophold.", @@ -273,7 +300,9 @@ "Zoom in": "Zoom ind", "Zoom out": "Zoom ud", "as of today": "fra idag", + "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", "booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}", + "booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}", "by": "inden", "characters": "tegn", "hotelPages.rooms.roomCard.person": "person", @@ -289,5 +318,8 @@ "special character": "speciel karakter", "spendable points expiring by": "{points} Brugbare point udløber den {date}", "to": "til", - "uppercase letter": "stort bogstav" + "uppercase letter": "stort bogstav", + "{amount} {currency}": "{amount} {currency}", + "{difference}{amount} {currency}": "{difference}{amount} {currency}", + "{width} cm × {length} cm": "{width} cm × {length} cm" } diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 614ca2f46..13438d8c3 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -1,39 +1,43 @@ { + "Included (based on availability)": "Inbegriffen (je nach Verfügbarkeit)", + "{amount} {currency}/night per adult": "{amount} {currency}/Nacht pro Erwachsener", "A destination or hotel name is needed to be able to search for a hotel room.": "Ein Reiseziel oder Hotelname wird benötigt, um nach einem Hotelzimmer suchen zu können.", "A photo of the room": "Ein Foto des Zimmers", + "About meetings & conferences": "About meetings & conferences", "Activities": "Aktivitäten", "Add code": "Code hinzufügen", "Add new card": "Neue Karte hinzufügen", "Address": "Adresse", "Airport": "Flughafen", + "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle unsere Frühstücksbuffets bieten glutenfreie, vegane und allergikerfreundliche Speisen.", "Already a friend?": "Sind wir schon Freunde?", "Amenities": "Annehmlichkeiten", "Amusement park": "Vergnügungspark", + "An error occurred trying to manage your preferences, please try again later.": "Beim Versuch, Ihre Einstellungen zu verwalten, ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.", "An error occurred when adding a credit card, please try again later.": "Beim Hinzufügen einer Kreditkarte ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.", "An error occurred when trying to update profile.": "Beim Versuch, das Profil zu aktualisieren, ist ein Fehler aufgetreten.", "Any changes you've made will be lost.": "Alle Änderungen, die Sie vorgenommen haben, gehen verloren.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Möchten Sie die Karte mit der Endung {lastFourDigits} wirklich aus Ihrem Mitgliedsprofil entfernen?", "Arrival date": "Ankunftsdatum", - "as of today": "Stand heute", "As our": "Als unser {level}", "As our Close Friend": "Als unser enger Freund", "At latest": "Spätestens", "At the hotel": "Im Hotel", "Attraction": "Attraktion", "Back to scandichotels.com": "Zurück zu scandichotels.com", + "Bar": "Bar", "Bed type": "Bettentyp", "Book": "Buchen", "Book reward night": "Bonusnacht buchen", "Booking number": "Buchungsnummer", - "booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}", "Breakfast": "Frühstück", + "Breakfast buffet": "Frühstücksbuffet", "Breakfast excluded": "Frühstück nicht inbegriffen", "Breakfast included": "Frühstück inbegriffen", + "Breakfast restaurant": "Breakfast restaurant", "Bus terminal": "Busbahnhof", "Business": "Geschäft", - "by": "bis", "Cancel": "Stornieren", - "characters": "figuren", "Check in": "Einchecken", "Check out": "Auschecken", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sehen Sie sich die in Ihrem Profil gespeicherten Kreditkarten an. Bezahlen Sie mit einer gespeicherten Karte, wenn Sie angemeldet sind, für ein reibungsloseres Web-Erlebnis.", @@ -64,6 +68,7 @@ "Date of Birth": "Geburtsdatum", "Day": "Tag", "Description": "Beschreibung", + "Destination": "Bestimmungsort", "Destinations & hotels": "Reiseziele & Hotels", "Disabled booking options header": "Es tut uns leid", "Disabled booking options text": "Codes, Schecks und Bonusnächte sind auf der neuen Website noch nicht verfügbar.", @@ -72,26 +77,33 @@ "Distance to city centre": "{number}km zum Stadtzentrum", "Do you want to start the day with Scandics famous breakfast buffé?": "Möchten Sie den Tag mit Scandics berühmtem Frühstücksbuffet beginnen?", "Download the Scandic app": "Laden Sie die Scandic-App herunter", + "Earn bonus nights & points": "Sammeln Sie Bonusnächte und -punkte", "Edit": "Bearbeiten", "Edit profile": "Profil bearbeiten", "Email": "Email", + "Email address": "E-Mail-Adresse", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Enter destination or hotel": "Reiseziel oder Hotel eingeben", + "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile", "Explore nearby": "Erkunden Sie die Umgebung", "Extras to your booking": "Extras zu Ihrer Buchung", + "FAQ": "Häufig gestellte Fragen", "Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.", "Fair": "Messe", - "FAQ": "Häufig gestellte Fragen", "Find booking": "Buchung finden", "Find hotels": "Hotels finden", + "Firstname": "Vorname", "Flexibility": "Flexibilität", "Former Scandic Hotel": "Ehemaliges Scandic Hotel", "Free cancellation": "Kostenlose Stornierung", "Free rebooking": "Kostenlose Umbuchung", "From": "Fromm", "Get inspired": "Lassen Sie sich inspieren", + "Get member benefits & offers": "Holen Sie sich Vorteile und Angebote für Mitglieder", "Go back to edit": "Zurück zum Bearbeiten", "Go back to overview": "Zurück zur Übersicht", + "Guest information": "Informationen für Gäste", "Guests & Rooms": "Gäste & Zimmer", "Hi": "Hallo", "Highest level": "Höchstes Level", @@ -99,16 +111,16 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel-Infos", "Hotel surroundings": "Umgebung des Hotels", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personen", - "hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen", "Hotels": "Hotels", "How do you want to sleep?": "Wie möchtest du schlafen?", "How it works": "Wie es funktioniert", "Image gallery": "Bildergalerie", + "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.", "Join Scandic Friends": "Treten Sie Scandic Friends bei", - "km to city center": "km bis zum Stadtzentrum", + "Join at no cost": "Kostenlos beitreten", + "King bed": "Kingsize-Bett", "Language": "Sprache", + "Lastname": "Nachname", "Latest searches": "Letzte Suchanfragen", "Level": "Level", "Level 1": "Level 1", @@ -134,9 +146,9 @@ "Member price": "Mitgliederpreis", "Member price from": "Mitgliederpreis ab", "Members": "Mitglieder", - "Membership cards": "Mitgliedskarten", "Membership ID": "Mitglieds-ID", "Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert", + "Membership cards": "Mitgliedskarten", "Menu": "Menu", "Modify": "Ändern", "Month": "Monat", @@ -151,10 +163,8 @@ "Nearby companies": "Nahe gelegene Unternehmen", "New password": "Neues Kennwort", "Next": "Nächste", - "next level:": "Nächstes Level:", - "night": "nacht", - "nights": "Nächte", "Nights needed to level up": "Nächte, die zum Levelaufstieg benötigt werden", + "No breakfast": "Kein Frühstück", "No content published": "Kein Inhalt veröffentlicht", "No matching location found": "Kein passender Standort gefunden", "No results": "Keine Ergebnisse", @@ -164,16 +174,15 @@ "Non-refundable": "Nicht erstattungsfähig", "Not found": "Nicht gefunden", "Nr night, nr adult": "{nights, number} Nacht, {adults, number} Erwachsener", - "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", "Overview": "Übersicht", "Parking": "Parken", "Parking / Garage": "Parken / Garage", + "Password": "Passwort", "Pay later": "Später bezahlen", "Pay now": "Jetzt bezahlen", "Payment info": "Zahlungsinformationen", @@ -181,7 +190,6 @@ "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", - "points": "Punkte", "Points": "Punkte", "Points being calculated": "Punkte werden berechnet", "Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021", @@ -189,17 +197,24 @@ "Points needed to level up": "Punkte, die zum Levelaufstieg benötigt werden", "Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben", "Previous victories": "Bisherige Siege", + "Proceed to payment method": "Weiter zur Zahlungsmethode", "Public price from": "Öffentlicher Preis ab", "Public transport": "Öffentliche Verkehrsmittel", + "Queen bed": "Queensize-Bett", "Read more": "Mehr lesen", + "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Lesen Sie mehr über das Hotel", + "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen", - "Restaurant": "Restaurant", + "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", + "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Neues Passwort erneut eingeben", "Room & Terms": "Zimmer & Bedingungen", "Room facilities": "Zimmerausstattung", "Rooms": "Räume", + "Rooms & Guests": "Zimmer & Gäste", + "Sauna and gym": "Sauna and gym", "Save": "Speichern", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", @@ -211,6 +226,7 @@ "Select a country": "Wähle ein Land", "Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus", "Select date of birth": "Geburtsdatum auswählen", + "Select dates": "Datum auswählen", "Select language": "Sprache auswählen", "Select your language": "Wählen Sie Ihre Sprache", "Shopping": "Einkaufen", @@ -224,29 +240,25 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.", "Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.", "Something went wrong!": "Etwas ist schief gelaufen!", - "special character": "sonderzeichen", - "spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}", "Sports": "Sport", "Standard price": "Standardpreis", "Street": "Straße", "Successfully updated profile!": "Profil erfolgreich aktualisiert!", "Summary": "Zusammenfassung", + "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Teilen Sie uns mit, welche Informationen und Updates Sie wie erhalten möchten, indem Sie auf den unten stehenden Link klicken.", "Thank you": "Danke", "Theatre": "Theater", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", - "to": "zu", "Total Points": "Gesamtpunktzahl", "Tourist": "Tourist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktionen", "Transportations": "Transportmittel", "Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)", - "TUI Points": "TUI Points", "Type of bed": "Bettentyp", "Type of room": "Zimmerart", - "uppercase letter": "großbuchstabe", "Use bonus cheque": "Bonusscheck nutzen", "Use code/voucher": "Code/Gutschein nutzen", "User information": "Nutzerinformation", @@ -270,21 +282,42 @@ "Year": "Jahr", "Yes, discard changes": "Ja, Änderungen verwerfen", "Yes, remove my card": "Ja, meine Karte entfernen", + "You can always change your mind later and add breakfast at the hotel.": "Sie können es sich später jederzeit anders überlegen und das Frühstück im Hotel hinzufügen.", "You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.", "You have no previous stays.": "Sie haben keine vorherigen Aufenthalte.", "You have no upcoming stays.": "Sie haben keine bevorstehenden Aufenthalte.", + "Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!", "Your card was successfully removed!": "Ihre Karte wurde erfolgreich entfernt!", "Your card was successfully saved!": "Ihre Karte wurde erfolgreich gespeichert!", - "Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!", "Your current level": "Ihr aktuelles Level", "Your details": "Ihre Angaben", "Your level": "Dein level", "Your points to spend": "Meine Punkte", "Zip code": "PLZ", "Zoo": "Zoo", - "Earn bonus nights & points": "Sammeln Sie Bonusnächte und -punkte", - "Get member benefits & offers": "Holen Sie sich Vorteile und Angebote für Mitglieder", - "Join at no cost": "Kostenlos beitreten", "Zoom in": "Vergrößern", - "Zoom out": "Verkleinern" + "Zoom out": "Verkleinern", + "as of today": "Stand heute", + "booking.adults": "{totalAdults, plural, one {# erwachsene} other {# erwachsene}}", + "booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}", + "booking.rooms": "{totalRooms, plural, one {# zimmer} other {# räume}}", + "by": "bis", + "characters": "figuren", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "personen", + "hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen", + "km to city center": "km bis zum Stadtzentrum", + "next level:": "Nächstes Level:", + "night": "nacht", + "nights": "Nächte", + "number": "nummer", + "or": "oder", + "points": "Punkte", + "special character": "sonderzeichen", + "spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}", + "to": "zu", + "uppercase letter": "großbuchstabe", + "{amount} {currency}": "{amount} {currency}", + "{difference}{amount} {currency}": "{difference}{amount} {currency}", + "{width} cm × {length} cm": "{width} cm × {length} cm" } diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 1fbad9201..fa0e27a61 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -1,14 +1,19 @@ { + "Included (based on availability)": "Included (based on availability)", + "{amount} {currency}/night per adult": "{amount} {currency}/night per adult", "A destination or hotel name is needed to be able to search for a hotel room.": "A destination or hotel name is needed to be able to search for a hotel room.", "A photo of the room": "A photo of the room", + "About meetings & conferences": "About meetings & conferences", "Activities": "Activities", "Add code": "Add code", "Add new card": "Add new card", "Address": "Address", "Airport": "Airport", + "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", "Already a friend?": "Already a friend?", "Amenities": "Amenities", "Amusement park": "Amusement park", + "An error occurred trying to manage your preferences, please try again later.": "An error occurred trying to manage your preferences, please try again later.", "An error occurred when adding a credit card, please try again later.": "An error occurred when adding a credit card, please try again later.", "An error occurred when trying to update profile.": "An error occurred when trying to update profile.", "Any changes you've made will be lost.": "Any changes you've made will be lost.", @@ -20,13 +25,16 @@ "At the hotel": "At the hotel", "Attractions": "Attractions", "Back to scandichotels.com": "Back to scandichotels.com", + "Bar": "Bar", "Bed type": "Bed type", "Book": "Book", "Book reward night": "Book reward night", "Booking number": "Booking number", "Breakfast": "Breakfast", + "Breakfast buffet": "Breakfast buffet", "Breakfast excluded": "Breakfast excluded", "Breakfast included": "Breakfast included", + "Breakfast restaurant": "Breakfast restaurant", "Bus terminal": "Bus terminal", "Business": "Business", "Cancel": "Cancel", @@ -74,7 +82,9 @@ "Edit profile": "Edit profile", "Email": "Email", "Email address": "Email address", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Enter destination or hotel": "Enter destination or hotel", + "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Explore all levels and benefits", "Explore nearby": "Explore nearby", "Extras to your booking": "Extras to your booking", @@ -105,8 +115,10 @@ "How do you want to sleep?": "How do you want to sleep?", "How it works": "How it works", "Image gallery": "Image gallery", + "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.", "Join Scandic Friends": "Join Scandic Friends", "Join at no cost": "Join at no cost", + "King bed": "King bed", "km to city center": "km to city center", "Language": "Language", "Lastname": "Lastname", @@ -153,6 +165,7 @@ "New password": "New password", "Next": "Next", "Nights needed to level up": "Nights needed to level up", + "No breakfast": "No breakfast", "No content published": "No content published", "No matching location found": "No matching location found", "No results": "No results", @@ -188,15 +201,21 @@ "Proceed to payment method": "Proceed to payment method", "Public price from": "Public price from", "Public transport": "Public transport", + "Queen bed": "Queen bed", "Read more": "Read more", + "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Read more about the hotel", + "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Remove card from member profile", - "Restaurant": "Restaurant", + "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", + "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Retype new password", "Room & Terms": "Room & Terms", "Room facilities": "Room facilities", "Rooms": "Rooms", + "Rooms & Guests": "Rooms & Guests", + "Sauna and gym": "Sauna and gym", "Save": "Save", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", @@ -250,7 +269,7 @@ "Visiting address": "Visiting address", "We could not add a card right now, please try again later.": "We could not add a card right now, please try again later.", "We couldn't find a matching location for your search.": "We couldn't find a matching location for your search.", - "We have sent a detailed confirmation of your booking to your email: ": "We have sent a detailed confirmation of your booking to your email: ", + "We have sent a detailed confirmation of your booking to your email:": "We have sent a detailed confirmation of your booking to your email: ", "We look forward to your visit!": "We look forward to your visit!", "Weekdays": "Weekdays", "Weekends": "Weekends", @@ -264,6 +283,7 @@ "Year": "Year", "Yes, discard changes": "Yes, discard changes", "Yes, remove my card": "Yes, remove my card", + "You can always change your mind later and add breakfast at the hotel.": "You can always change your mind later and add breakfast at the hotel.", "You canceled adding a new credit card.": "You canceled adding a new credit card.", "You have no previous stays.": "You have no previous stays.", "You have no upcoming stays.": "You have no upcoming stays.", @@ -298,5 +318,7 @@ "spendable points expiring by": "{points} spendable points expiring by {date}", "to": "to", "uppercase letter": "uppercase letter", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{amount} {currency}": "{amount} {currency}", + "{difference}{amount} {currency}": "{difference}{amount} {currency}", + "{width} cm × {length} cm": "{width} cm × {length} cm" } diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index c67f3f109..a00928ad3 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -1,15 +1,20 @@ { + "Included (based on availability)": "Sisältyy (saatavuuden mukaan)", + "{amount} {currency}/night per adult": "{amount} {currency}/yö per aikuinen", "A destination or hotel name is needed to be able to search for a hotel room.": "Kohteen tai hotellin nimi tarvitaan, jotta hotellihuonetta voidaan hakea.", "A photo of the room": "Kuva huoneesta", + "About meetings & conferences": "About meetings & conferences", "Activities": "Aktiviteetit", "Add code": "Lisää koodi", "Add new card": "Lisää uusi kortti", "Address": "Osoite", "Airport": "Lentokenttä", + "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Kaikki aamiaisbuffettimme tarjoavat gluteenittomia, vegaanisia ja allergiaystävällisiä vaihtoehtoja.", "Already a friend?": "Oletko jo ystävä?", "Amenities": "Mukavuudet", "Amusement park": "Huvipuisto", "An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.", + "An error occurred trying to manage your preferences, please try again later.": "Asetusten hallinnassa tapahtui virhe. Yritä myöhemmin uudelleen.", "An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.", "Any changes you've made will be lost.": "Kaikki tekemäsi muutokset menetetään.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Haluatko varmasti poistaa kortin, joka päättyy numeroon {lastFourDigits} jäsenprofiilistasi?", @@ -20,15 +25,18 @@ "At the hotel": "Hotellissa", "Attractions": "Nähtävyydet", "Back to scandichotels.com": "Takaisin scandichotels.com", + "Bar": "Bar", "Bed type": "Vuodetyyppi", "Book": "Varaa", "Book reward night": "Kirjapalkinto-ilta", "Booking number": "Varausnumero", "Breakfast": "Aamiainen", + "Breakfast buffet": "Aamiaisbuffet", "Breakfast excluded": "Aamiainen ei sisälly", "Breakfast included": "Aamiainen sisältyy", "Bus terminal": "Bussiasema", "Business": "Business", + "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Peruuttaa", "Check in": "Sisäänkirjautuminen", "Check out": "Uloskirjautuminen", @@ -60,6 +68,7 @@ "Date of Birth": "Syntymäaika", "Day": "Päivä", "Description": "Kuvaus", + "Destination": "Kohde", "Destinations & hotels": "Kohteet ja hotellit", "Disabled booking options header": "Olemme pahoillamme", "Disabled booking options text": "Koodit, sekit ja palkintoillat eivät ole vielä saatavilla uudella verkkosivustolla.", @@ -72,7 +81,10 @@ "Edit": "Muokata", "Edit profile": "Muokkaa profiilia", "Email": "Sähköposti", + "Email address": "Sähköpostiosoite", "Enter destination or hotel": "Anna kohde tai hotelli", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", + "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", "Explore nearby": "Tutustu lähialueeseen", "Extras to your booking": "Varauksessa lisäpalveluita", @@ -81,6 +93,7 @@ "Fair": "Messukeskus", "Find booking": "Etsi varaus", "Find hotels": "Etsi hotelleja", + "Firstname": "Etunimi", "Flexibility": "Joustavuus", "Former Scandic Hotel": "Entinen Scandic-hotelli", "Free cancellation": "Ilmainen peruutus", @@ -90,6 +103,7 @@ "Get member benefits & offers": "Hanki jäsenetuja ja -tarjouksia", "Go back to edit": "Palaa muokkaamaan", "Go back to overview": "Palaa yleiskatsaukseen", + "Guest information": "Vieraan tiedot", "Guests & Rooms": "Vieraat & Huoneet", "Hi": "Hi", "Highest level": "Korkein taso", @@ -101,10 +115,13 @@ "How do you want to sleep?": "Kuinka haluat nukkua?", "How it works": "Kuinka se toimii", "Image gallery": "Kuvagalleria", + "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.", "Join Scandic Friends": "Liity jäseneksi", "Join at no cost": "Liity maksutta", + "King bed": "King-vuode", "km to city center": "km keskustaan", "Language": "Kieli", + "Lastname": "Sukunimi", "Latest searches": "Viimeisimmät haut", "Level": "Level", "Level 1": "Taso 1", @@ -148,6 +165,7 @@ "New password": "Uusi salasana", "Next": "Seuraava", "Nights needed to level up": "Yöt, joita tarvitaan tasolle", + "No breakfast": "Ei aamiaista", "No content published": "Ei julkaistua sisältöä", "No matching location found": "Vastaavaa sijaintia ei löytynyt", "No results": "Ei tuloksia", @@ -180,17 +198,25 @@ "Points needed to level up": "Tarvitset vielä", "Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet", "Previous victories": "Edelliset voitot", + "Proceed to payment method": "Siirry maksutavalle", "Public price from": "Julkinen hinta alkaen", "Public transport": "Julkinen liikenne", + "Queen bed": "Queen-vuode", "Read more": "Lue lisää", + "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Lue lisää hotellista", + "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Poista kortti jäsenprofiilista", - "Restaurant": "Ravintola", + "Restaurant": "{count, plural, one {#Ravintola} other {#Restaurants}}", "Restaurant & Bar": "Ravintola & Baari", + "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Kirjoita uusi salasana uudelleen", "Room & Terms": "Huone & Ehdot", "Room facilities": "Huoneen varustelu", "Rooms": "Huoneet", + "Rooms & Guests": "Huoneet & Vieraat", + "Rooms & Guestss": "Huoneet & Vieraat", + "Sauna and gym": "Sauna and gym", "Save": "Tallenna", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", @@ -202,6 +228,7 @@ "Select a country": "Valitse maa", "Select country of residence": "Valitse asuinmaa", "Select date of birth": "Valitse syntymäaika", + "Select dates": "Valitse päivämäärät", "Select language": "Valitse kieli", "Select your language": "Valitse kieli", "Shopping": "Ostokset", @@ -257,6 +284,7 @@ "Year": "Vuosi", "Yes, discard changes": "Kyllä, hylkää muutokset", "Yes, remove my card": "Kyllä, poista korttini", + "You can always change your mind later and add breakfast at the hotel.": "Voit aina muuttaa mieltäsi myöhemmin ja lisätä aamiaisen hotelliin.", "You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.", "You have no previous stays.": "Sinulla ei ole aiempia majoituksia.", "You have no upcoming stays.": "Sinulla ei ole tulevia majoituksia.", @@ -272,7 +300,9 @@ "Zoom in": "Lähennä", "Zoom out": "Loitonna", "as of today": "tänään", + "booking.adults": "{totalAdults, plural, one {# aikuinen} other {# aikuiset}}", "booking.nights": "{totalNights, plural, one {# yö} other {# yötä}}", + "booking.rooms": "{totalRooms, plural, one {# huone} other {# sviitti}}", "by": "mennessä", "characters": "hahmoja", "hotelPages.rooms.roomCard.person": "henkilö", @@ -288,5 +318,8 @@ "special character": "erikoishahmo", "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "to": "to", - "uppercase letter": "iso kirjain" + "uppercase letter": "iso kirjain", + "{amount} {currency}": "{amount} {currency}", + "{difference}{amount} {currency}": "{difference}{amount} {currency}", + "{width} cm × {length} cm": "{width} cm × {length} cm" } diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index fb3a94b6e..9a28e6f69 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -1,15 +1,20 @@ { + "Included (based on availability)": "Inkludert (basert på tilgjengelighet)", + "{amount} {currency}/night per adult": "{amount} {currency}/natt per voksen", "A destination or hotel name is needed to be able to search for a hotel room.": "Et reisemål eller hotellnavn er nødvendig for å kunne søke etter et hotellrom.", "A photo of the room": "Et bilde av rommet", + "About meetings & conferences": "About meetings & conferences", "Activities": "Aktiviteter", "Add code": "Legg til kode", "Add new card": "Legg til nytt kort", "Address": "Adresse", "Airport": "Flyplass", + "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.", "Already a friend?": "Allerede Friend?", "Amenities": "Fasiliteter", "Amusement park": "Tivoli", "An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.", + "An error occurred trying to manage your preferences, please try again later.": "Det oppstod en feil under forsøket på å administrere innstillingene dine. Prøv igjen senere.", "An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.", "Any changes you've made will be lost.": "Eventuelle endringer du har gjort, går tapt.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på at du vil fjerne kortet som slutter på {lastFourDigits} fra medlemsprofilen din?", @@ -21,11 +26,13 @@ "At the hotel": "På hotellet", "Attractions": "Attraksjoner", "Back to scandichotels.com": "Tilbake til scandichotels.com", + "Bar": "Bar", "Bed type": "Seng type", "Book": "Bestill", "Book reward night": "Bestill belønningskveld", "Booking number": "Bestillingsnummer", "Breakfast": "Frokost", + "Breakfast buffet": "Breakfast buffet", "Breakfast excluded": "Frokost ekskludert", "Breakfast included": "Frokost inkludert", "Bus terminal": "Bussterminal", @@ -61,6 +68,7 @@ "Date of Birth": "Fødselsdato", "Day": "Dag", "Description": "Beskrivelse", + "Destination": "Destinasjon", "Destinations & hotels": "Destinasjoner og hoteller", "Disabled booking options header": "Vi beklager", "Disabled booking options text": "Koder, checks og belønningsnætter er enda ikke tilgjengelige på den nye nettsiden.", @@ -73,15 +81,19 @@ "Edit": "Redigere", "Edit profile": "Rediger profil", "Email": "E-post", + "Email address": "E-postadresse", "Enter destination or hotel": "Skriv inn destinasjon eller hotell", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", + "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Utforsk alle nivåer og fordeler", "Explore nearby": "Utforsk i nærheten", "Extras to your booking": "Tilvalg til bestillingen din", - "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.", "Fair": "Messe", + "FAQ": "FAQ", "Find booking": "Finn booking", "Find hotels": "Finn hotell", + "Firstname": "Fornavn", "Flexibility": "Fleksibilitet", "Former Scandic Hotel": "Tidligere Scandic-hotell", "Free cancellation": "Gratis avbestilling", @@ -91,6 +103,7 @@ "Get member benefits & offers": "Få medlemsfordeler og tilbud", "Go back to edit": "Gå tilbake til redigering", "Go back to overview": "Gå tilbake til oversikten", + "Guest information": "Informasjon til gjester", "Guests & Rooms": "Gjester & rom", "Hi": "Hei", "Highest level": "Høyeste nivå", @@ -102,10 +115,13 @@ "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det fungerer", "Image gallery": "Bildegalleri", + "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.", "Join Scandic Friends": "Bli med i Scandic Friends", "Join at no cost": "Bli med uten kostnad", + "King bed": "King-size-seng", "km to city center": "km til sentrum", "Language": "Språk", + "Lastname": "Etternavn", "Latest searches": "Siste søk", "Level": "Nivå", "Level 1": "Nivå 1", @@ -149,6 +165,7 @@ "New password": "Nytt passord", "Next": "Neste", "Nights needed to level up": "Netter som trengs for å komme opp i nivå", + "No breakfast": "Ingen frokost", "No content published": "Ingen innhold publisert", "No matching location found": "Fant ingen samsvarende plassering", "No results": "Ingen resultater", @@ -181,17 +198,24 @@ "Points needed to level up": "Poeng som trengs for å komme opp i nivå", "Points needed to stay on level": "Poeng som trengs for å holde seg på nivå", "Previous victories": "Tidligere seire", + "Proceed to payment method": "Fortsett til betalingsmetode", "Public price from": "Offentlig pris fra", "Public transport": "Offentlig transport", + "Queen bed": "Queen-size-seng", "Read more": "Les mer", + "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Les mer om hotellet", + "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", - "Restaurant": "Restaurant", + "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", + "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Skriv inn nytt passord på nytt", "Room & Terms": "Rom & Vilkår", "Room facilities": "Romfasiliteter", "Rooms": "Rom", + "Rooms & Guests": "Rom og gjester", + "Sauna and gym": "Sauna and gym", "Save": "Lagre", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", @@ -203,6 +227,7 @@ "Select a country": "Velg et land", "Select country of residence": "Velg bostedsland", "Select date of birth": "Velg fødselsdato", + "Select dates": "Velg datoer", "Select language": "Velg språk", "Select your language": "Velg språk", "Shopping": "Shopping", @@ -258,6 +283,7 @@ "Year": "År", "Yes, discard changes": "Ja, forkast endringer", "Yes, remove my card": "Ja, fjern kortet mitt", + "You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ombestemme deg senere og legge til frokost på hotellet.", "You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.", "You have no previous stays.": "Du har ingen tidligere opphold.", "You have no upcoming stays.": "Du har ingen kommende opphold.", @@ -273,7 +299,9 @@ "Zoom in": "Zoom inn", "Zoom out": "Zoom ut", "as of today": "per idag", + "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", + "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}", "by": "innen", "characters": "tegn", "hotelPages.rooms.roomCard.person": "person", @@ -289,5 +317,8 @@ "special character": "spesiell karakter", "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", "to": "til", - "uppercase letter": "stor bokstav" + "uppercase letter": "stor bokstav", + "{amount} {currency}": "{amount} {currency}", + "{difference}{amount} {currency}": "{difference}{amount} {currency}", + "{width} cm × {length} cm": "{width} cm × {length} cm" } diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 9dc2fb264..dc6a082c8 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -1,15 +1,20 @@ { + "Included (based on availability)": "Ingår (baserat på tillgänglighet)", + "{amount} {currency}/night per adult": "{amount} {currency}/natt per vuxen", "A destination or hotel name is needed to be able to search for a hotel room.": "Ett destinations- eller hotellnamn behövs för att kunna söka efter ett hotellrum.", "A photo of the room": "Ett foto av rummet", + "About meetings & conferences": "About meetings & conferences", "Activities": "Aktiviteter", "Add code": "Lägg till kod", "Add new card": "Lägg till nytt kort", "Address": "Adress", "Airport": "Flygplats", + "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alla våra frukostbufféer erbjuder glutenfria, veganska och allergivänliga alternativ.", "Already a friend?": "Är du redan en vän?", "Amenities": "Bekvämligheter", "Amusement park": "Nöjespark", "An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.", + "An error occurred trying to manage your preferences, please try again later.": "Ett fel uppstod när du försökte hantera dina inställningar, försök igen senare.", "An error occurred when trying to update profile.": "Ett fel uppstod när du försökte uppdatera profilen.", "Any changes you've made will be lost.": "Alla ändringar du har gjort kommer att gå förlorade.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Är du säker på att du vill ta bort kortet som slutar med {lastFourDigits} från din medlemsprofil?", @@ -21,15 +26,18 @@ "At the hotel": "På hotellet", "Attractions": "Sevärdheter", "Back to scandichotels.com": "Tillbaka till scandichotels.com", + "Bar": "Bar", "Bed type": "Sängtyp", "Book": "Boka", "Book reward night": "Boka frinatt", "Booking number": "Bokningsnummer", "Breakfast": "Frukost", + "Breakfast buffet": "Frukostbuffé", "Breakfast excluded": "Frukost ingår ej", "Breakfast included": "Frukost ingår", "Bus terminal": "Bussterminal", "Business": "Business", + "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Avbryt", "Check in": "Checka in", "Check out": "Checka ut", @@ -61,6 +69,7 @@ "Date of Birth": "Födelsedatum", "Day": "Dag", "Description": "Beskrivning", + "Destination": "Destination", "Destinations & hotels": "Destinationer & hotell", "Disabled booking options header": "Vi beklagar", "Disabled booking options text": "Koder, bonuscheckar och belöningsnätter är inte tillgängliga på den nya webbplatsen än.", @@ -73,7 +82,10 @@ "Edit": "Redigera", "Edit profile": "Redigera profil", "Email": "E-post", + "Email address": "E-postadress", "Enter destination or hotel": "Ange destination eller hotell", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", + "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Utforska alla nivåer och fördelar", "Explore nearby": "Utforska i närheten", "Extras to your booking": "Extra tillval till din bokning", @@ -82,6 +94,7 @@ "Fair": "Mässa", "Find booking": "Hitta bokning", "Find hotels": "Hitta hotell", + "Firstname": "Förnamn", "Flexibility": "Flexibilitet", "Former Scandic Hotel": "Tidigare Scandichotell", "Free cancellation": "Fri avbokning", @@ -91,6 +104,7 @@ "Get member benefits & offers": "Ta del av medlemsförmåner och erbjudanden", "Go back to edit": "Gå tillbaka till redigeringen", "Go back to overview": "Gå tillbaka till översikten", + "Guest information": "Information till gästerna", "Guests & Rooms": "Gäster & rum", "Hi": "Hej", "Highest level": "Högsta nivå", @@ -102,10 +116,14 @@ "How do you want to sleep?": "Hur vill du sova?", "How it works": "Hur det fungerar", "Image gallery": "Bildgalleri", + "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.", + "Join Scandic Friends": "Gå med i Scandic Friends", "Join at no cost": "Gå med utan kostnad", + "King bed": "King size-säng", "km to city center": "km till stadens centrum", "Language": "Språk", + "Lastname": "Efternamn", "Latest searches": "Senaste sökningarna", "Level": "Nivå", "Level 1": "Nivå 1", @@ -149,6 +167,7 @@ "New password": "Nytt lösenord", "Next": "Nästa", "Nights needed to level up": "Nätter som behövs för att gå upp i nivå", + "No breakfast": "Ingen frukost", "No content published": "Inget innehåll publicerat", "No matching location found": "Ingen matchande plats hittades", "No results": "Inga resultat", @@ -181,17 +200,24 @@ "Points needed to level up": "Poäng som behövs för att gå upp i nivå", "Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå", "Previous victories": "Tidigare segrar", + "Proceed to payment method": "Gå vidare till betalningsmetod", "Public price from": "Offentligt pris från", "Public transport": "Kollektivtrafik", + "Queen bed": "Queen size-säng", "Read more": "Läs mer", + "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Läs mer om hotellet", + "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Ta bort kortet från medlemsprofilen", - "Restaurant": "Restaurang", + "Restaurant": "{count, plural, one {#Restaurang} other {#Restauranger}}", "Restaurant & Bar": "Restaurang & Bar", + "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Upprepa nytt lösenord", "Room & Terms": "Rum & Villkor", "Room facilities": "Rumfaciliteter", "Rooms": "Rum", + "Rooms & Guests": "Rum och gäster", + "Sauna and gym": "Sauna and gym", "Save": "Spara", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", @@ -203,6 +229,7 @@ "Select a country": "Välj ett land", "Select country of residence": "Välj bosättningsland", "Select date of birth": "Välj födelsedatum", + "Select dates": "Välj datum", "Select language": "Välj språk", "Select your language": "Välj ditt språk", "Shopping": "Shopping", @@ -259,6 +286,7 @@ "Year": "År", "Yes, discard changes": "Ja, ignorera ändringar", "Yes, remove my card": "Ja, ta bort mitt kort", + "You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ändra dig senare och lägga till frukost på hotellet.", "You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.", "You have no previous stays.": "Du har inga tidigare vistelser.", "You have no upcoming stays.": "Du har inga planerade resor.", @@ -274,7 +302,9 @@ "Zoom in": "Zooma in", "Zoom out": "Zooma ut", "as of today": "från och med idag", + "booking.adults": "{totalAdults, plural, one {# vuxen} other {# vuxna}}", "booking.nights": "{totalNights, plural, one {# natt} other {# nätter}}", + "booking.rooms": "{totalRooms, plural, one {# rum} other {# rum}}", "by": "innan", "characters": "tecken", "hotelPages.rooms.roomCard.person": "person", @@ -290,5 +320,8 @@ "special character": "speciell karaktär", "spendable points expiring by": "{points} poäng förfaller {date}", "to": "till", - "uppercase letter": "stor bokstav" + "uppercase letter": "stor bokstav", + "{amount} {currency}": "{amount} {currency}", + "{difference}{amount} {currency}": "{difference}{amount} {currency}", + "{width} cm × {length} cm": "{width} cm × {length} cm" } diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 213e40b37..a21017d6a 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -21,6 +21,7 @@ export namespace endpoints { upcomingStays = "booking/v1/Stays/future", rewards = `${profile}/reward`, tierRewards = `${profile}/TierRewards`, + subscriberId = `${profile}/SubscriberId`, } } diff --git a/lib/graphql/Query/HotelPage/HotelPage.graphql b/lib/graphql/Query/HotelPage/HotelPage.graphql index dee41c6a8..9e90f069c 100644 --- a/lib/graphql/Query/HotelPage/HotelPage.graphql +++ b/lib/graphql/Query/HotelPage/HotelPage.graphql @@ -1,11 +1,13 @@ +#import "../../Fragments/PageLink/ContentPageLink.graphql" + query GetHotelPage($locale: String!, $uid: String!) { hotel_page(locale: $locale, uid: $uid) { hotel_page_id title url content { + __typename ... on HotelPageContentUpcomingActivitiesCard { - __typename upcoming_activities_card { background_image cta_text @@ -16,15 +18,8 @@ query GetHotelPage($locale: String!, $uid: String!) { hotel_page_activities_content_pageConnection { edges { node { - ... on ContentPage { - url - web { - original_url - } - system { - locale - } - } + __typename + ...ContentPageLink } } } diff --git a/public/_static/icons/UI - Enter details/bed king.svg b/public/_static/icons/UI - Enter details/bed king.svg new file mode 100644 index 000000000..0a8804fef --- /dev/null +++ b/public/_static/icons/UI - Enter details/bed king.svg @@ -0,0 +1,3 @@ + + + diff --git a/server/routers/contentstack/loyaltyLevel/query.ts b/server/routers/contentstack/loyaltyLevel/query.ts index 58882dde7..c531fbebb 100644 --- a/server/routers/contentstack/loyaltyLevel/query.ts +++ b/server/routers/contentstack/loyaltyLevel/query.ts @@ -31,6 +31,17 @@ const getAllLoyaltyLevelFailCounter = meter.createCounter( "trpc.contentstack.loyaltyLevel.all-fail" ) +const getByLevelLoyaltyLevelCounter = meter.createCounter( + "trpc.contentstack.loyaltyLevel.byLevel" +) + +const getByLevelLoyaltyLevelSuccessCounter = meter.createCounter( + "trpc.contentstack.loyaltyLevel.byLevel-success" +) +const getByLevelLoyaltyLevelFailCounter = meter.createCounter( + "trpc.contentstack.loyaltyLevel.byLevel-fail" +) + export async function getAllLoyaltyLevels(ctx: Context) { getAllLoyaltyLevelCounter.add(1) @@ -87,7 +98,9 @@ export async function getAllLoyaltyLevels(ctx: Context) { } export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) { - getAllLoyaltyLevelCounter.add(1) + getByLevelLoyaltyLevelCounter.add(1, { + query: JSON.stringify({ lang: ctx.lang, level_id }), + }) const loyaltyLevelsConfigResponse = await request( GetLoyaltyLevel, @@ -103,10 +116,10 @@ export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) { !loyaltyLevelsConfigResponse.data || !loyaltyLevelsConfigResponse.data.all_loyalty_level.items.length ) { - getAllLoyaltyLevelFailCounter.add(1) + getByLevelLoyaltyLevelFailCounter.add(1) const notFoundError = notFound(loyaltyLevelsConfigResponse) console.error( - "contentstack.loyaltyLevels not found error", + "contentstack.loyaltyLevel not found error", JSON.stringify({ query: { lang: ctx.lang, level_id }, error: { code: notFoundError.code }, @@ -119,10 +132,10 @@ export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) { loyaltyLevelsConfigResponse.data ) if (!validatedLoyaltyLevels.success) { - getAllLoyaltyLevelFailCounter.add(1) + getByLevelLoyaltyLevelFailCounter.add(1) console.error(validatedLoyaltyLevels.error) console.error( - "contentstack.rewards validation error", + "contentstack.loyaltyLevel validation error", JSON.stringify({ query: { lang: ctx.lang, level_id }, error: validatedLoyaltyLevels.error, @@ -131,7 +144,7 @@ export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) { return null } - getAllLoyaltyLevelSuccessCounter.add(1) + getByLevelLoyaltyLevelSuccessCounter.add(1) return validatedLoyaltyLevels.data[0] } diff --git a/server/routers/contentstack/schemas/blocks/activitiesCard.ts b/server/routers/contentstack/schemas/blocks/activitiesCard.ts index d98e985a7..0d4fba2b1 100644 --- a/server/routers/contentstack/schemas/blocks/activitiesCard.ts +++ b/server/routers/contentstack/schemas/blocks/activitiesCard.ts @@ -47,13 +47,13 @@ export const activitiesCard = z.object({ } } return { - background_image: data.background_image, - body_text: data.body_text, + backgroundImage: data.background_image, + bodyText: data.body_text, contentPage, - cta_text: data.cta_text, + ctaText: data.cta_text, heading: data.heading, - open_in_new_tab: !!data.open_in_new_tab, - scripted_title: data.scripted_title, + openInNewTab: !!data.open_in_new_tab, + scriptedTopTitle: data.scripted_title, } }), }) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index d1fb2dfe8..0d3e75ce3 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -164,6 +164,16 @@ const detailedFacilitySchema = z.object({ filter: z.string().optional(), }) +export const facilitySchema = z.object({ + headingText: z.string(), + heroImages: z.array( + z.object({ + metaData: imageMetaDataSchema, + imageSizes: imageSizesSchema, + }) + ), +}) + const healthFacilitySchema = z.object({ type: z.string(), content: z.object({ @@ -497,6 +507,9 @@ export const getHotelDataSchema = z.object({ socialMedia: socialMediaSchema, meta: metaSchema.optional(), isActive: z.boolean(), + conferencesAndMeetings: facilitySchema.optional(), + healthAndWellness: facilitySchema.optional(), + restaurantImages: facilitySchema.optional(), }), relationships: relationshipsSchema, }), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 9c1042dbd..2f30ae397 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -40,8 +40,10 @@ import { TWENTYFOUR_HOURS, } from "./utils" +import { FacilityEnum } from "@/types/components/hotelPage/facilities" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import type { RequestOptionsWithOutBody } from "@/types/fetch" +import type { Facility } from "@/types/hotel" import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage" const meter = metrics.getMeter("trpc.hotels") @@ -173,7 +175,6 @@ export const hotelQueryRouter = router({ const included = validatedHotelData.data.included || [] const hotelAttributes = validatedHotelData.data.data.attributes - const images = extractHotelImages(hotelAttributes) const roomCategories = included @@ -212,6 +213,21 @@ export const hotelQueryRouter = router({ ? contentstackData?.content[0] : null + const facilities: Facility[] = [ + { + ...apiJson.data.attributes.restaurantImages, + id: FacilityEnum.restaurant, + }, + { + ...apiJson.data.attributes.conferencesAndMeetings, + id: FacilityEnum.conference, + }, + { + ...apiJson.data.attributes.healthAndWellness, + id: FacilityEnum.wellness, + }, + ] + getHotelSuccessCounter.add(1, { hotelId, lang, include }) console.info( "api.hotels.hotel success", @@ -230,6 +246,7 @@ export const hotelQueryRouter = router({ pointsOfInterest: hotelAttributes.pointsOfInterest, roomCategories, activitiesCard: activities?.upcoming_activities_card, + facilities, } }), availability: router({ diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts index 38bb458e3..005941090 100644 --- a/server/routers/user/mutation.ts +++ b/server/routers/user/mutation.ts @@ -1,5 +1,11 @@ +import { metrics } from "@opentelemetry/api" + +import { env } from "@/env/server" import * as api from "@/lib/api" -import { initiateSaveCardSchema } from "@/server/routers/user/output" +import { + initiateSaveCardSchema, + subscriberIdSchema, +} from "@/server/routers/user/output" import { protectedProcedure, router } from "@/server/trpc" import { @@ -8,6 +14,17 @@ import { saveCreditCardInput, } from "./input" +const meter = metrics.getMeter("trpc.user") +const generatePreferencesLinkCounter = meter.createCounter( + "trpc.user.generatePreferencesLink" +) +const generatePreferencesLinkSuccessCounter = meter.createCounter( + "trpc.user.generatePreferencesLink-success" +) +const generatePreferencesLinkFailCounter = meter.createCounter( + "trpc.user.generatePreferencesLink-fail" +) + export const userMutationRouter = router({ creditCard: router({ add: protectedProcedure.input(addCreditCardInput).mutation(async function ({ @@ -128,4 +145,62 @@ export const userMutationRouter = router({ return true }), }), + generatePreferencesLink: protectedProcedure.mutation(async function ({ + ctx, + }) { + generatePreferencesLinkCounter.add(1) + const apiResponse = await api.get(api.endpoints.v1.subscriberId, { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + }) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + generatePreferencesLinkFailCounter.add(1, { + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) + console.error( + "api.user.subscriberId error ", + JSON.stringify({ + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + return null + } + + const data = await apiResponse.json() + + const validatedData = subscriberIdSchema.safeParse(data) + + if (!validatedData.success) { + generatePreferencesLinkSuccessCounter.add(1, { + error_type: "validation_error", + error: JSON.stringify(validatedData.error), + }) + console.error( + "api.user.generatePreferencesLink validation error", + JSON.stringify({ + error: validatedData.error, + }) + ) + console.error(validatedData.error.format()) + + return null + } + const preferencesLink = new URL(env.SALESFORCE_PREFERENCE_BASE_URL) + preferencesLink.searchParams.set("subKey", validatedData.data.subscriberId) + + generatePreferencesLinkSuccessCounter.add(1) + return preferencesLink.toString() + }), }) diff --git a/server/routers/user/output.ts b/server/routers/user/output.ts index 8ee482057..9c5e955ab 100644 --- a/server/routers/user/output.ts +++ b/server/routers/user/output.ts @@ -234,3 +234,7 @@ export const initiateSaveCardSchema = z.object({ type: z.string(), }), }) + +export const subscriberIdSchema = z.object({ + subscriberId: z.string(), +}) diff --git a/types/components/cardImage.ts b/types/components/cardImage.ts index 9976a2db3..229299d9c 100644 --- a/types/components/cardImage.ts +++ b/types/components/cardImage.ts @@ -1,7 +1,8 @@ +import { FacilityCard, FacilityImage } from "./hotelPage/facilities" + import type { CardProps } from "@/components/TempDesignSystem/Card/card" -import type { FacilityCard } from "./hotelPage/facilities" export interface CardImageProps extends React.HTMLAttributes { - card: FacilityCard | undefined - imageCards: Pick[] + card: FacilityCard | CardProps + imageCards?: FacilityImage[] } diff --git a/types/components/enterDetails/bedType.ts b/types/components/enterDetails/bedType.ts new file mode 100644 index 000000000..c4e6e4ff0 --- /dev/null +++ b/types/components/enterDetails/bedType.ts @@ -0,0 +1,5 @@ +import { z } from "zod" + +import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" + +export interface BedTypeSchema extends z.output {} diff --git a/types/components/enterDetails/breakfast.ts b/types/components/enterDetails/breakfast.ts new file mode 100644 index 000000000..868bc96a1 --- /dev/null +++ b/types/components/enterDetails/breakfast.ts @@ -0,0 +1,5 @@ +import { z } from "zod" + +import { breakfastSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" + +export interface BreakfastSchema extends z.output {} diff --git a/types/components/header/myPagesMenu.ts b/types/components/header/myPagesMenu.ts index 22cab2ca7..054dc41df 100644 --- a/types/components/header/myPagesMenu.ts +++ b/types/components/header/myPagesMenu.ts @@ -3,6 +3,7 @@ import { navigationQueryRouter } from "@/server/routers/contentstack/myPages/nav import { FriendsMembership } from "@/utils/user" import type { User } from "@/types/user" +import type { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output" type MyPagesNavigation = Awaited< ReturnType<(typeof navigationQueryRouter)["get"]> @@ -12,6 +13,7 @@ export interface MyPagesMenuProps { navigation: MyPagesNavigation user: Pick membership?: FriendsMembership | null + membershipLevel: LoyaltyLevel | null } export interface MyPagesMenuContentProps extends MyPagesMenuProps { diff --git a/types/components/hotelPage/amenities.ts b/types/components/hotelPage/amenities.ts new file mode 100644 index 000000000..b394576f6 --- /dev/null +++ b/types/components/hotelPage/amenities.ts @@ -0,0 +1,5 @@ +import type { Amenities } from "@/types/hotel" + +export type AmenitiesListProps = { + detailedFacilities: Amenities +} diff --git a/types/components/hotelPage/facilities.ts b/types/components/hotelPage/facilities.ts index 228093fe0..98a6a444d 100644 --- a/types/components/hotelPage/facilities.ts +++ b/types/components/hotelPage/facilities.ts @@ -1,19 +1,54 @@ +import type { Facility } from "@/types/hotel" +import type { ActivityCard } from "@/types/trpc/routers/contentstack/hotelPage" import type { CardProps } from "@/components/TempDesignSystem/Card/card" -interface ColumnSpanOptions { - columnSpan: "one" | "two" | "three" +export type FacilitiesProps = { + facilities: Facility[] + activitiesCard?: ActivityCard } -export type FacilityCard = CardProps & ColumnSpanOptions - -export type Facility = Array - -export type Facilities = Array - -export type FacilityProps = { - facilities: Facilities +export type FacilityImage = { + backgroundImage: CardProps["backgroundImage"] + theme: CardProps["theme"] + id: string } +export type FacilityCard = { + secondaryButton: { + href: string + title: string + openInNewTab?: boolean + isExternal: boolean + } + heading: string + scriptedTopTitle: string + theme: CardProps["theme"] + id: string +} + +export type FacilityCardType = FacilityImage | FacilityCard +export type FacilityGrid = FacilityCardType[] +export type Facilities = FacilityGrid[] + export type CardGridProps = { - facility: Facility + facilitiesCardGrid: FacilityGrid +} + +export enum FacilityEnum { + wellness = "wellness-and-exercise", + conference = "meetings-and-conferences", + restaurant = "restaurant-and-bar", +} + +export enum RestaurantHeadings { + restaurantAndBar = "Restaurant & Bar", + bar = "Bar", + restaurant = "Restaurant", + breakfastRestaurant = "Breakfast restaurant", +} + +export enum FacilityIds { + bar = 1606, + rooftopBar = 1014, + restaurant = 1383, } diff --git a/types/components/hotelPage/tabNavigation.ts b/types/components/hotelPage/tabNavigation.ts index d7286285b..c16f6c1ab 100644 --- a/types/components/hotelPage/tabNavigation.ts +++ b/types/components/hotelPage/tabNavigation.ts @@ -1,4 +1,4 @@ -export enum HotelHashValues { +export enum HotelHashValues { // Should these be translated? overview = "#overview", rooms = "#rooms-section", restaurant = "#restaurant-and-bar", @@ -7,3 +7,7 @@ export enum HotelHashValues { activities = "#activities", faq = "#faq", } + +export type TabNavigationProps = { + restaurantTitle: string +} diff --git a/types/components/image.ts b/types/components/image.ts new file mode 100644 index 000000000..972c521d6 --- /dev/null +++ b/types/components/image.ts @@ -0,0 +1,9 @@ +export type ApiImage = { + id: string + url: string + title: string + meta: { + alt: string + caption: string + } +} diff --git a/types/enums/bedType.ts b/types/enums/bedType.ts new file mode 100644 index 000000000..0b4ba284d --- /dev/null +++ b/types/enums/bedType.ts @@ -0,0 +1,4 @@ +export enum bedTypeEnum { + KING = "KING", + QUEEN = "QUEEN", +} diff --git a/types/enums/breakfast.ts b/types/enums/breakfast.ts new file mode 100644 index 000000000..567db2860 --- /dev/null +++ b/types/enums/breakfast.ts @@ -0,0 +1,4 @@ +export enum breakfastEnum { + BREAKFAST = "BREAKFAST", + NO_BREAKFAST = "NO_BREAKFAST", +} diff --git a/types/hotel.ts b/types/hotel.ts index f4436fb6c..203e2423f 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -1,6 +1,7 @@ import { z } from "zod" import { + facilitySchema, getHotelDataSchema, parkingSchema, pointOfInterestSchema, @@ -13,6 +14,8 @@ export type Hotel = HotelData["data"]["attributes"] export type HotelAddress = HotelData["data"]["attributes"]["address"] export type HotelLocation = HotelData["data"]["attributes"]["location"] +export type Amenities = HotelData["data"]["attributes"]["detailedFacilities"] + type HotelRatings = HotelData["data"]["attributes"]["ratings"] export type HotelTripAdvisor = | NonNullable["tripAdvisor"] @@ -52,3 +55,4 @@ export enum PointOfInterestGroupEnum { } export type ParkingData = z.infer +export type Facility = z.infer & { id: string } diff --git a/utils/facilityCards.ts b/utils/facilityCards.ts new file mode 100644 index 000000000..dd8c0a1a4 --- /dev/null +++ b/utils/facilityCards.ts @@ -0,0 +1,141 @@ +import { + meetingsAndConferences, + restaurantAndBar, + wellnessAndExercise, +} from "@/constants/routes/hotelPageParams" + +import { getLang } from "@/i18n/serverContext" + +import { + type Facilities, + type FacilityCard, + type FacilityCardType, + FacilityEnum, + type FacilityGrid, + FacilityIds, + type FacilityImage, + RestaurantHeadings, +} from "@/types/components/hotelPage/facilities" +import type { Amenities, Facility } from "@/types/hotel" +import type { CardProps } from "@/components/TempDesignSystem/Card/card" + +export function isFacilityCard(card: FacilityCardType): card is FacilityCard { + return "heading" in card +} + +export function isFacilityImage(card: FacilityCardType): card is FacilityImage { + return "backgroundImage" in card +} + +function setCardProps( + theme: CardProps["theme"], + heading: string, + buttonText: string, + href: string, + scriptedTopTitle: string +): FacilityCard { + return { + theme, + id: href, + heading, + scriptedTopTitle, + secondaryButton: { + href: `?s=${href}`, + title: buttonText, + isExternal: false, + }, + } +} + +export function setFacilityCardGrids(facilities: Facility[]): Facilities { + const lang = getLang() + + const cards: Facilities = facilities.map((facility) => { + let card: FacilityCard + + const grid: FacilityGrid = facility.heroImages.slice(0, 2).map((image) => { + // Can be a maximum 2 images per grid + const img: FacilityImage = { + backgroundImage: { + url: image.imageSizes.large, + title: image.metaData.title, + meta: { + alt: image.metaData.altText, + caption: image.metaData.altText_En, + }, + id: image.imageSizes.large, + }, + theme: "image", + id: image.imageSizes.large, + } + return img + }) + + switch (facility.id) { + case FacilityEnum.wellness: + card = setCardProps( + "one", + "Sauna and gym", + "Read more about wellness & exercise", + wellnessAndExercise[lang], + facility.headingText + ) + grid.unshift(card) + break + + case FacilityEnum.conference: + card = setCardProps( + "primaryDim", + "Events that make an impression", + "About meetings & conferences", + meetingsAndConferences[lang], + facility.headingText + ) + grid.push(card) + break + + case FacilityEnum.restaurant: + //const title = getRestaurantHeading(amenities) // TODO will be used later + card = setCardProps( + "primaryDark", + "Enjoy relaxed restaurant experiences", + "Read more & book a table", + restaurantAndBar[lang], + facility.headingText + ) + grid.unshift(card) + break + } + return grid + }) + return cards +} + +export function getRestaurantHeading(amenities: Amenities): RestaurantHeadings { + const hasBar = amenities.some( + (facility) => + facility.id === FacilityIds.bar || facility.id === FacilityIds.rooftopBar + ) + const hasRestaurant = amenities.some( + (facility) => facility.id === FacilityIds.restaurant + ) + + if (hasBar && hasRestaurant) { + return RestaurantHeadings.restaurantAndBar + } else if (hasBar) { + return RestaurantHeadings.bar + } else if (hasRestaurant) { + return RestaurantHeadings.restaurant + } + return RestaurantHeadings.breakfastRestaurant +} + +export function filterFacilityCards(cards: FacilityGrid) { + const card = cards.filter((card) => isFacilityCard(card)) + const images = cards.filter((card) => isFacilityImage(card)) + + return { + card: card[0] as FacilityCard, + images: images as FacilityImage[], + } +} diff --git a/utils/imageCard.ts b/utils/imageCard.ts deleted file mode 100644 index b66c65954..000000000 --- a/utils/imageCard.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { - Facility, - FacilityCard, -} from "@/types/components/hotelPage/facilities" - -export function sortCards(grid: Facility) { - const sortedCards = grid.slice(0).sort((a: FacilityCard, b: FacilityCard) => { - if (!a.backgroundImage && b.backgroundImage) { - return 1 - } - if (a.backgroundImage && !b.backgroundImage) { - return -1 - } - return 0 - }) - - return { card: sortedCards.pop(), images: sortedCards } -}