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 b3ef81538..fa2581a54 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx @@ -1,13 +1,13 @@ 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 Details from "@/components/HotelReservation/SelectRate/Details" import Payment from "@/components/HotelReservation/SelectRate/Payment" -import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" import SectionAccordion from "@/components/HotelReservation/SelectRate/SectionAccordion" import Summary from "@/components/HotelReservation/SelectRate/Summary" import { getIntl } from "@/i18n" @@ -79,6 +79,7 @@ export default async function SectionsPage({ searchParams, }: PageArgs) { setLang(params.lang) + const profile = await getProfileSafely() const hotel = await serverClient().hotel.hotelData.get({ hotelId: "811", @@ -114,6 +115,11 @@ export default async function SectionsPage({ const currentSearchParams = new URLSearchParams(searchParams).toString() + let user = null + if (profile && !("error" in profile)) { + user = profile + } + return (
@@ -131,47 +137,26 @@ export default async function SectionsPage({ : undefined } path={`select-rate?${currentSearchParams}`} - > - {params.section === "select-rate" && ( - - )} - + > - {params.section === "select-bed" && ( - - )} + {params.section === "select-bed" ? : null} - {params.section === "breakfast" && ( - - )} + {params.section === "breakfast" ? : null} - {params.section === "details" &&
} + {params.section === "details" ?
: null} { - const availableHotels = await serverClient().hotel.availability.get(input) + const availableHotels = await serverClient().hotel.availability.hotels(input) if (!availableHotels) throw new Error() diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css new file mode 100644 index 000000000..3266c418d --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css @@ -0,0 +1,25 @@ +.page { + min-height: 100dvh; + padding-top: var(--Spacing-x6); + padding-left: var(--Spacing-x2); + padding-right: var(--Spacing-x2); + background-color: var(--Scandic-Brand-Warm-White); +} + +.content { + max-width: 1134px; + margin-top: var(--Spacing-x5); + margin-left: auto; + margin-right: auto; + display: flex; + justify-content: space-between; + gap: var(--Spacing-x7); +} + +.main { + flex-grow: 1; +} + +.summary { + max-width: 340px; +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx new file mode 100644 index 000000000..235eeda52 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx @@ -0,0 +1,53 @@ +import { serverClient } from "@/lib/trpc/server" +import tempHotelData from "@/server/routers/hotels/tempHotelData.json" + +import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" +import { getIntl } from "@/i18n" +import { setLang } from "@/i18n/serverContext" + +import styles from "./page.module.css" + +import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import { LangParams, PageArgs } from "@/types/params" + +export default async function SelectRatePage({ + params, + searchParams, +}: PageArgs) { + setLang(params.lang) + + // TODO: Use real endpoint. + const hotel = tempHotelData.data.attributes + + const rates = await serverClient().hotel.rates.get({ + // TODO: pass the correct hotel ID and all other parameters that should be included in the search + hotelId: searchParams.hotel, + }) + + // const rates = await serverClient().hotel.availability.getForHotel({ + // hotelId: 811, + // roomStayStartDate: "2024-11-02", + // roomStayEndDate: "2024-11-03", + // adults: 1, + // }) + const intl = await getIntl() + + return ( +
+ {/* TODO: Add Hotel Listing Card */} +
Hotel Listing Card TBI
+ +
+
+ +
+
+
+ ) +} diff --git a/app/[lang]/(live)/@bookingwidget/loading.module.css b/app/[lang]/(live)/@bookingwidget/loading.module.css new file mode 100644 index 000000000..1fafcbb91 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/loading.module.css @@ -0,0 +1,4 @@ +.container { + height: 76px; + width: 100%; +} diff --git a/app/[lang]/(live)/@bookingwidget/loading.tsx b/app/[lang]/(live)/@bookingwidget/loading.tsx new file mode 100644 index 000000000..2c203967d --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/loading.tsx @@ -0,0 +1,11 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +import styles from "./loading.module.css" + +export default function LoadingBookingWidget() { + return ( +
+ +
+ ) +} 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/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx b/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx index a734e30ec..116934531 100644 --- a/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx @@ -10,6 +10,7 @@ import type { ClearSearchButtonProps } from "@/types/components/search" export default function ClearSearchButton({ getItemProps, + handleClearSearchHistory, highlightedIndex, index, }: ClearSearchButtonProps) { @@ -18,13 +19,6 @@ export default function ClearSearchButton({ variant: index === highlightedIndex ? "active" : "default", }) - function handleClick() { - // noop - // the click bubbles to handleOnSelect - // where selectedItem = "clear-search" - // which is the value for item below - } - return ( + +
+ + ) +} diff --git a/components/HotelReservation/EnterDetails/Details/schema.ts b/components/HotelReservation/EnterDetails/Details/schema.ts new file mode 100644 index 000000000..92f1a5629 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Details/schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod" + +import { phoneValidator } from "@/utils/phoneValidator" + +export const detailsSchema = z.object({ + countryCode: z.string(), + email: z.string().email(), + firstname: z.string(), + lastname: z.string(), + phoneNumber: phoneValidator(), +}) + +export const signedInDetailsSchema = z.object({ + countryCode: z.string().optional(), + email: z.string().email().optional(), + firstname: z.string().optional(), + lastname: z.string().optional(), + phoneNumber: phoneValidator().optional(), +}) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 82b0c4b91..99feb01ab 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -48,10 +48,10 @@ export default async function HotelCard({ hotel }: HotelCardProps) { {hotelData.name} - + {`${hotelData.address.streetAddress}, ${hotelData.address.city}`} - + {`${hotelData.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
@@ -79,7 +79,7 @@ export default async function HotelCard({ hotel }: HotelCardProps) { {price?.regularAmount} {price?.currency} / {intl.formatMessage({ id: "night" })} - approx 280 eur + approx 280 eur
@@ -90,7 +90,7 @@ export default async function HotelCard({ hotel }: HotelCardProps) { {price?.memberAmount} {price?.currency} / {intl.formatMessage({ id: "night" })} - approx 280 eur + approx 280 eur
+ ) +} 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/Card/card.module.css b/components/TempDesignSystem/Form/Card/card.module.css new file mode 100644 index 000000000..1044596f6 --- /dev/null +++ b/components/TempDesignSystem/Form/Card/card.module.css @@ -0,0 +1,72 @@ +.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: grid; + grid-template-columns: 1fr auto; + padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); + transition: all 200ms ease; + width: min(100%, 600px); +} + +.label:hover { + background-color: var(--Base-Surface-Secondary-light-Hover); +} + +.label:has(:checked) { + background-color: var(--Primary-Light-Surface-Normal); + border-color: var(--Base-Border-Hover); +} + +.icon { + align-self: center; + grid-column: 2/3; + grid-row: 1/3; + justify-self: flex-end; + transition: fill 200ms ease; +} + +.label:hover .icon, +.label:hover .icon *, +.label:has(:checked) .icon, +.label:has(:checked) .icon * { + fill: var(--Base-Text-Medium-contrast); +} + +.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-column: 1 / 2; + grid-row: 2; +} + +.title { + grid-column: 1 / 2; +} + +.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/Country/country.ts b/components/TempDesignSystem/Form/Country/country.ts index 29211f0fb..693b555db 100644 --- a/components/TempDesignSystem/Form/Country/country.ts +++ b/components/TempDesignSystem/Form/Country/country.ts @@ -1,9 +1,11 @@ import type { RegisterOptions } from "react-hook-form" export type CountryProps = { + className?: string label: string name?: string placeholder?: string + readOnly?: boolean registerOptions?: RegisterOptions } diff --git a/components/TempDesignSystem/Form/Country/index.tsx b/components/TempDesignSystem/Form/Country/index.tsx index 3ff4d8c72..9777a4a48 100644 --- a/components/TempDesignSystem/Form/Country/index.tsx +++ b/components/TempDesignSystem/Form/Country/index.tsx @@ -28,8 +28,10 @@ import type { } from "./country" export default function CountrySelect({ + className = "", label, name = "country", + readOnly = false, registerOptions = {}, }: CountryProps) { const { formatMessage } = useIntl() @@ -54,12 +56,13 @@ export default function CountrySelect({ const selectCountryLabel = formatMessage({ id: "Select a country" }) return ( -
+
+) { + return ( + + + + + + + ) +}) + +export default AriaInputWithLabel diff --git a/components/TempDesignSystem/Form/Input/AriaInputWithLabel/input.module.css b/components/TempDesignSystem/Form/Input/AriaInputWithLabel/input.module.css new file mode 100644 index 000000000..6afcfd82a --- /dev/null +++ b/components/TempDesignSystem/Form/Input/AriaInputWithLabel/input.module.css @@ -0,0 +1,55 @@ +.container { + align-content: center; + background-color: var(--Main-Grey-White); + border-color: var(--Scandic-Beige-40); + border-style: solid; + border-width: 1px; + border-radius: var(--Corner-radius-Medium); + display: grid; + height: 60px; + padding: var(--Spacing-x1) var(--Spacing-x2); + transition: border-color 200ms ease; +} + +.container:has(.input:active, .input:focus) { + border-color: var(--Scandic-Blue-90); +} + +.container:has(.input:disabled) { + background-color: var(--Main-Grey-10); + border: none; + color: var(--Main-Grey-40); +} + +.container:has(.input[data-invalid="true"], .input[aria-invalid="true"]) { + border-color: var(--Scandic-Red-60); +} + +.input { + background: none; + border: none; + color: var(--Main-Grey-100); + height: 18px; + margin: 0; + order: 2; + overflow: visible; + padding: 0; +} + +.input:not(:active, :focus):placeholder-shown { + height: 0px; + transition: height 150ms ease; +} + +.input:focus, +.input:focus:placeholder-shown, +.input:active, +.input:active:placeholder-shown { + height: 18px; + transition: height 150ms ease; + outline: none; +} + +.input:disabled { + color: var(--Main-Grey-40); +} diff --git a/components/TempDesignSystem/Form/Input/AriaInputWithLabel/input.ts b/components/TempDesignSystem/Form/Input/AriaInputWithLabel/input.ts new file mode 100644 index 000000000..8e9c6b743 --- /dev/null +++ b/components/TempDesignSystem/Form/Input/AriaInputWithLabel/input.ts @@ -0,0 +1,4 @@ +export interface AriaInputWithLabelProps + extends React.InputHTMLAttributes { + label: string +} diff --git a/components/TempDesignSystem/Form/Input/index.tsx b/components/TempDesignSystem/Form/Input/index.tsx index a0951b4ca..dda34d115 100644 --- a/components/TempDesignSystem/Form/Input/index.tsx +++ b/components/TempDesignSystem/Form/Input/index.tsx @@ -1,15 +1,9 @@ "use client" -import { - Input as AriaInput, - Label as AriaLabel, - Text, - TextField, -} from "react-aria-components" +import { Text, TextField } from "react-aria-components" import { Controller, useFormContext } from "react-hook-form" import { CheckIcon, InfoCircleIcon } from "@/components/Icons" -import Label from "@/components/TempDesignSystem/Form/Label" -import Body from "@/components/TempDesignSystem/Text/Body" +import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel" import Caption from "@/components/TempDesignSystem/Text/Caption" import styles from "./input.module.css" @@ -20,11 +14,13 @@ import type { InputProps } from "./input" export default function Input({ "aria-label": ariaLabel, + className = "", disabled = false, helpText = "", label, name, placeholder = "", + readOnly = false, registerOptions = {}, type = "text", }: InputProps) { @@ -44,6 +40,7 @@ export default function Input({ render={({ field, fieldState }) => ( - - - - - - + {helpText && !fieldState.error ? ( diff --git a/components/TempDesignSystem/Form/Input/input.module.css b/components/TempDesignSystem/Form/Input/input.module.css index dd4fb6209..56d0be4b5 100644 --- a/components/TempDesignSystem/Form/Input/input.module.css +++ b/components/TempDesignSystem/Form/Input/input.module.css @@ -1,59 +1,3 @@ -.container { - align-content: center; - background-color: var(--Main-Grey-White); - border-color: var(--Scandic-Beige-40); - border-style: solid; - border-width: 1px; - border-radius: var(--Corner-radius-Medium); - display: grid; - height: 60px; - padding: var(--Spacing-x1) var(--Spacing-x2); - transition: border-color 200ms ease; -} - -.container:has(.input:active, .input:focus) { - border-color: var(--Scandic-Blue-90); -} - -.container:has(.input:disabled) { - background-color: var(--Main-Grey-10); - border: none; - color: var(--Main-Grey-40); -} - -.container:has(.input[data-invalid="true"], .input[aria-invalid="true"]) { - border-color: var(--Scandic-Red-60); -} - -.input { - background: none; - border: none; - color: var(--Main-Grey-100); - height: 18px; - margin: 0; - order: 2; - overflow: visible; - padding: 0; -} - -.input:not(:active, :focus):placeholder-shown { - height: 0px; - transition: height 150ms ease; -} - -.input:focus, -.input:focus:placeholder-shown, -.input:active, -.input:active:placeholder-shown { - height: 18px; - transition: height 150ms ease; - outline: none; -} - -.input:disabled { - color: var(--Main-Grey-40); -} - .helpText { align-items: flex-start; display: flex; diff --git a/components/TempDesignSystem/Form/Input/input.ts b/components/TempDesignSystem/Form/Input/input.ts index 067d1754b..e480363b9 100644 --- a/components/TempDesignSystem/Form/Input/input.ts +++ b/components/TempDesignSystem/Form/Input/input.ts @@ -1,4 +1,4 @@ -import type { RegisterOptions, UseFormRegister } from "react-hook-form" +import type { RegisterOptions } from "react-hook-form" export interface InputProps extends React.InputHTMLAttributes { diff --git a/components/TempDesignSystem/Form/Label/label.module.css b/components/TempDesignSystem/Form/Label/label.module.css index 6948a6594..9d92486c3 100644 --- a/components/TempDesignSystem/Form/Label/label.module.css +++ b/components/TempDesignSystem/Form/Label/label.module.css @@ -5,6 +5,7 @@ letter-spacing: 0.03px; line-height: 120%; text-align: left; + transition: font-size 100ms ease; } span.small { @@ -21,7 +22,6 @@ input:active ~ .label, input:not(:placeholder-shown) ~ .label { display: block; font-size: 12px; - transition: font-size 100ms ease; } input:focus ~ .label { diff --git a/components/TempDesignSystem/Form/NewPassword/index.tsx b/components/TempDesignSystem/Form/NewPassword/index.tsx index 99f2dbda6..3c9950e1f 100644 --- a/components/TempDesignSystem/Form/NewPassword/index.tsx +++ b/components/TempDesignSystem/Form/NewPassword/index.tsx @@ -1,17 +1,11 @@ "use client" -import { - Input as AriaInput, - Label as AriaLabel, - Text, - TextField, -} from "react-aria-components" +import { Text, TextField } from "react-aria-components" import { Controller, useFormContext } from "react-hook-form" import { useIntl } from "react-intl" import { CheckIcon, CloseIcon } from "@/components/Icons" import Error from "@/components/TempDesignSystem/Form/ErrorMessage/Error" -import Label from "@/components/TempDesignSystem/Form/Label" -import Body from "@/components/TempDesignSystem/Text/Body" +import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel" import Caption from "@/components/TempDesignSystem/Text/Caption" import { type IconProps, Key, type NewPasswordProps } from "./newPassword" @@ -47,20 +41,14 @@ export default function NewPassword({ value={field.value} type="password" > - - - - - - + {field.value ? (
diff --git a/components/TempDesignSystem/Form/NewPassword/newPassword.module.css b/components/TempDesignSystem/Form/NewPassword/newPassword.module.css index d8a5d781b..7b0c4509f 100644 --- a/components/TempDesignSystem/Form/NewPassword/newPassword.module.css +++ b/components/TempDesignSystem/Form/NewPassword/newPassword.module.css @@ -1,59 +1,3 @@ -.container { - align-content: center; - background-color: var(--Main-Grey-White); - border-color: var(--Scandic-Beige-40); - border-style: solid; - border-width: 1px; - border-radius: var(--Corner-radius-Medium); - display: grid; - height: 60px; - padding: var(--Spacing-x1) var(--Spacing-x2); - transition: border-color 200ms ease; -} - -.container:has(.input:active, .input:focus) { - border-color: var(--Scandic-Blue-90); -} - -.container:has(.input:disabled) { - background-color: var(--Main-Grey-10); - border: none; - color: var(--Main-Grey-40); -} - -.container:has(.input[data-invalid="true"], .input[aria-invalid="true"]) { - border-color: var(--Scandic-Red-60); -} - -.input { - background: none; - border: none; - color: var(--Main-Grey-100); - height: 18px; - margin: 0; - order: 2; - overflow: visible; - padding: 0; -} - -.input:not(:active, :focus):placeholder-shown { - height: 0px; - transition: height 150ms ease; -} - -.input:focus, -.input:focus:placeholder-shown, -.input:active, -.input:active:placeholder-shown { - height: 18px; - transition: height 150ms ease; - outline: none; -} - -.input:disabled { - color: var(--Main-Grey-40); -} - .helpText { align-items: flex-start; display: flex; diff --git a/components/TempDesignSystem/Form/Phone/index.tsx b/components/TempDesignSystem/Form/Phone/index.tsx index df3d18c75..6e9c2024b 100644 --- a/components/TempDesignSystem/Form/Phone/index.tsx +++ b/components/TempDesignSystem/Form/Phone/index.tsx @@ -2,11 +2,7 @@ import "react-international-phone/style.css" import { isValidPhoneNumber, parsePhoneNumber } from "libphonenumber-js" -import { - Input as AriaInput, - Label as AriaLabel, - TextField, -} from "react-aria-components" +import { TextField } from "react-aria-components" import { useController, useFormContext, useWatch } from "react-hook-form" import { CountrySelector, @@ -18,6 +14,7 @@ import { useIntl } from "react-intl" import { ChevronDownIcon } from "@/components/Icons" import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage" +import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel" import Label from "@/components/TempDesignSystem/Form/Label" import Body from "@/components/TempDesignSystem/Text/Body" @@ -29,10 +26,12 @@ import type { PhoneProps } from "./phone" export default function Phone({ ariaLabel = "Phone number input", + className = "", disabled = false, label, name = "phoneNumber", placeholder = "", + readOnly = false, registerOptions = { required: true, }, @@ -72,8 +71,9 @@ export default function Phone({ } return ( -
+
- - - - - - +
diff --git a/components/TempDesignSystem/Form/Phone/phone.module.css b/components/TempDesignSystem/Form/Phone/phone.module.css index af3d09f70..bda78a1af 100644 --- a/components/TempDesignSystem/Form/Phone/phone.module.css +++ b/components/TempDesignSystem/Form/Phone/phone.module.css @@ -19,6 +19,9 @@ --react-international-phone-dropdown-top: calc( var(--react-international-phone-height) + var(--Spacing-x1) ); + --react-international-phone-dial-code-preview-font-size: var( + --typography-Body-Regular-fontSize + ); } .phone:has(.input:active, .input:focus) { @@ -46,7 +49,6 @@ align-self: center; } -.inputContainer, .select { align-content: center; background-color: var(--Main-Grey-White); @@ -93,42 +95,8 @@ .select .dialCode { border: none; - color: var(--Main-Grey-100); + color: var(--UI-Text-High-contrast); line-height: 1; justify-self: flex-start; padding: 0; } - -.inputContainer:has(.input:not(:focus):placeholder-shown) { - gap: 0; - grid-template-rows: 1fr; -} - -.inputContainer:has(.input:active, .input:focus) { - border-color: var(--Scandic-Blue-90); -} - -.inputContainer:has(.input[data-invalid="true"], .input[aria-invalid="true"]) { - border-color: var(--Scandic-Red-60); -} - -.input { - background: none; - border: none; - color: var(--Main-Grey-100); - height: 18px; - margin: 0; - order: 2; - overflow: visible; - padding: 0; -} - -.input:not(:active, :focus):placeholder-shown { - height: 0px; -} - -.input:focus, -.input:focus:placeholder-shown { - height: 18px; - outline: none; -} diff --git a/components/TempDesignSystem/Form/Phone/phone.ts b/components/TempDesignSystem/Form/Phone/phone.ts index 4828682bc..f7dbd0e15 100644 --- a/components/TempDesignSystem/Form/Phone/phone.ts +++ b/components/TempDesignSystem/Form/Phone/phone.ts @@ -2,9 +2,11 @@ import type { RegisterOptions } from "react-hook-form" export type PhoneProps = { ariaLabel?: string + className?: string disabled?: boolean label: string name?: string placeholder?: string + readOnly?: boolean registerOptions?: RegisterOptions } diff --git a/components/TempDesignSystem/Text/Body/body.module.css b/components/TempDesignSystem/Text/Body/body.module.css index 30939d217..fd605447d 100644 --- a/components/TempDesignSystem/Text/Body/body.module.css +++ b/components/TempDesignSystem/Text/Body/body.module.css @@ -92,6 +92,10 @@ color: var(--Base-Text-Medium-contrast); } +.uiTextHighContrast { + color: var(--UI-Text-High-contrast); +} + .uiTextPlaceholder { color: var(--UI-Text-Placeholder); } diff --git a/components/TempDesignSystem/Text/Body/variants.ts b/components/TempDesignSystem/Text/Body/variants.ts index 055710eb3..0509c6bcd 100644 --- a/components/TempDesignSystem/Text/Body/variants.ts +++ b/components/TempDesignSystem/Text/Body/variants.ts @@ -15,6 +15,7 @@ const config = { white: styles.white, peach50: styles.peach50, peach80: styles.peach80, + uiTextHighContrast: styles.uiTextHighContrast, uiTextPlaceholder: styles.uiTextPlaceholder, }, textAlign: { diff --git a/components/TempDesignSystem/Text/Caption/caption.module.css b/components/TempDesignSystem/Text/Caption/caption.module.css index 627f7132a..a7b51bb52 100644 --- a/components/TempDesignSystem/Text/Caption/caption.module.css +++ b/components/TempDesignSystem/Text/Caption/caption.module.css @@ -35,6 +35,10 @@ p.caption { text-decoration: var(--typography-Caption-Regular-textDecoration); } +.baseTextAccent { + color: var(--Base-Text-Accent); +} + .black { color: var(--Main-Grey-100); } @@ -67,10 +71,14 @@ p.caption { color: var(--UI-Text-Medium-contrast); } +.uiTextHighContrast { + color: var(--UI-Text-High-contrast); +} + .center { text-align: center; } .left { text-align: left; -} +} \ No newline at end of file diff --git a/components/TempDesignSystem/Text/Caption/index.tsx b/components/TempDesignSystem/Text/Caption/index.tsx index 0e43263c6..e80caf79b 100644 --- a/components/TempDesignSystem/Text/Caption/index.tsx +++ b/components/TempDesignSystem/Text/Caption/index.tsx @@ -11,6 +11,7 @@ export default function Caption({ fontOnly = false, textAlign, textTransform, + uppercase, ...props }: CaptionProps) { const Comp = asChild ? Slot : "p" @@ -18,12 +19,14 @@ export default function Caption({ ? fontOnlycaptionVariants({ className, textTransform, + uppercase, }) : captionVariants({ className, color, textTransform, textAlign, + uppercase, }) return } diff --git a/components/TempDesignSystem/Text/Caption/variants.ts b/components/TempDesignSystem/Text/Caption/variants.ts index 4b0dd96af..401893857 100644 --- a/components/TempDesignSystem/Text/Caption/variants.ts +++ b/components/TempDesignSystem/Text/Caption/variants.ts @@ -5,12 +5,14 @@ import styles from "./caption.module.css" const config = { variants: { color: { + baseTextAccent: styles.baseTextAccent, black: styles.black, burgundy: styles.burgundy, pale: styles.pale, textMediumContrast: styles.textMediumContrast, red: styles.red, white: styles.white, + uiTextHighContrast: styles.uiTextHighContrast, uiTextActive: styles.uiTextActive, uiTextMediumContrast: styles.uiTextMediumContrast, }, @@ -23,6 +25,9 @@ const config = { center: styles.center, left: styles.left, }, + uppercase: { + true: styles.uppercase, + }, }, defaultVariants: { color: "black", @@ -39,6 +44,9 @@ const fontOnlyConfig = { regular: styles.regular, uppercase: styles.uppercase, }, + uppercase: { + true: styles.uppercase, + }, }, defaultVariants: { textTransform: "regular", diff --git a/components/TempDesignSystem/Text/Footnote/footnote.module.css b/components/TempDesignSystem/Text/Footnote/footnote.module.css index 39cd86591..b3ae66d80 100644 --- a/components/TempDesignSystem/Text/Footnote/footnote.module.css +++ b/components/TempDesignSystem/Text/Footnote/footnote.module.css @@ -59,7 +59,7 @@ color: var(--Scandic-Peach-50); } -.textMediumContrast { +.uiTextMediumContrast { color: var(--UI-Text-Medium-contrast); } diff --git a/components/TempDesignSystem/Text/Footnote/variants.ts b/components/TempDesignSystem/Text/Footnote/variants.ts index 3c2e58441..16f5b3185 100644 --- a/components/TempDesignSystem/Text/Footnote/variants.ts +++ b/components/TempDesignSystem/Text/Footnote/variants.ts @@ -9,7 +9,7 @@ const config = { burgundy: styles.burgundy, pale: styles.pale, peach50: styles.peach50, - textMediumContrast: styles.textMediumContrast, + uiTextMediumContrast: styles.uiTextMediumContrast, uiTextPlaceholder: styles.uiTextPlaceholder, }, textAlign: { 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 01abf69d0..e29d7cf6d 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -1,39 +1,43 @@ { + "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?", "Arrival date": "Ankomstdato", - "as of today": "fra idag", "As our": "Som vores {level}", "As our Close Friend": "Som vores nære ven", "At latest": "Senest", "At the hotel": "På hotellet", "Attractions": "Attraktioner", "Back to scandichotels.com": "Tilbage til scandichotels.com", + "Bar": "Bar", "Bed type": "Seng type", "Book": "Book", "Book reward night": "Book bonusnat", "Booking number": "Bookingnummer", - "booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}", "Breakfast": "Morgenmad", + "Breakfast buffet": "Morgenbuffet", "Breakfast excluded": "Morgenmad ikke inkluderet", "Breakfast included": "Morgenmad inkluderet", "Bus terminal": "Busstation", "Business": "Forretning", - "by": "inden", + "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Afbestille", - "characters": "tegn", "Check in": "Check ind", "Check out": "Check ud", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tjek de kreditkort, der er gemt på din profil. Betal med et gemt kort, når du er logget ind for en mere jævn weboplevelse.", @@ -64,32 +68,40 @@ "Date of Birth": "Fødselsdato", "Day": "Dag", "Description": "Beskrivelse", + "Destination": "Destination", "Destinations & hotels": "Destinationer & hoteller", "Discard changes": "Kassér ændringer", "Discard unsaved changes?": "Slette ændringer, der ikke er gemt?", "Distance to city centre": "{number}km til centrum", "Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte morgenbuffet?", "Download the Scandic app": "Download Scandic-appen", + "Earn bonus nights & points": "Optjen bonusnætter og point", "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", + "FAQ": "Ofte stillede spørgsmål", "Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.", "Fair": "Messe", - "FAQ": "Ofte stillede spørgsmål", "Find booking": "Find booking", "Find hotels": "Find hotel", + "Firstname": "Fornavn", "Flexibility": "Fleksibilitet", "Former Scandic Hotel": "Tidligere Scandic Hotel", "Free cancellation": "Gratis afbestilling", "Free rebooking": "Gratis ombooking", "From": "Fra", "Get inspired": "Bliv inspireret", + "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", @@ -97,16 +109,16 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel faciliteter", "Hotel surroundings": "Hotel omgivelser", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", - "hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer", "Hotels": "Hoteller", "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", - "km to city center": "km til byens centrum", + "Join at no cost": "Tilmeld dig uden omkostninger", + "King bed": "Kingsize-seng", "Language": "Sprog", + "Lastname": "Efternavn", "Latest searches": "Seneste søgninger", "Level": "Niveau", "Level 1": "Niveau 1", @@ -132,9 +144,9 @@ "Member price": "Medlemspris", "Member price from": "Medlemspris fra", "Members": "Medlemmer", - "Membership cards": "Medlemskort", "Membership ID": "Medlems-id", "Membership ID copied to clipboard": "Medlems-ID kopieret til udklipsholder", + "Membership cards": "Medlemskort", "Menu": "Menu", "Modify": "Ændre", "Month": "Måned", @@ -149,10 +161,8 @@ "Nearby companies": "Nærliggende virksomheder", "New password": "Nyt kodeord", "Next": "Næste", - "next level:": "Næste niveau:", - "night": "nat", - "nights": "nætter", "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", @@ -162,13 +172,11 @@ "Non-refundable": "Ikke-refunderbart", "Not found": "Ikke fundet", "Nr night, nr adult": "{nights, number} nat, {adults, number} voksen", - "number": "nummer", "On your journey": "På din rejse", "Open": "Åben", "Open language menu": "Åbn sprogmenuen", "Open menu": "Åbn menuen", "Open my pages menu": "Åbn mine sider menuen", - "or": "eller", "Overview": "Oversigt", "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", @@ -180,7 +188,6 @@ "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", - "points": "Point", "Points": "Point", "Points being calculated": "Point udregnes", "Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021", @@ -188,17 +195,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", @@ -210,6 +224,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", @@ -223,29 +238,25 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.", "Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.", "Something went wrong!": "Noget gik galt!", - "special character": "speciel karakter", - "spendable points expiring by": "{points} Brugbare point udløber den {date}", "Sports": "Sport", "Standard price": "Standardpris", "Street": "Gade", "Successfully updated profile!": "Profilen er opdateret med succes!", "Summary": "Opsummering", + "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortæl os, hvilke oplysninger og opdateringer du gerne vil modtage, og hvordan, ved at klikke på linket nedenfor.", "Thank you": "Tak", "Theatre": "Teater", "There are no transactions to display": "Der er ingen transaktioner at vise", "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", - "to": "til", "Total Points": "Samlet antal point", "Tourist": "Turist", "Transaction date": "Overførselsdato", "Transactions": "Transaktioner", "Transportations": "Transport", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", - "TUI Points": "TUI Points", "Type of bed": "Sengtype", "Type of room": "Værelsestype", - "uppercase letter": "stort bogstav", "Use bonus cheque": "Brug Bonus Cheque", "User information": "Brugeroplysninger", "View as list": "Vis som liste", @@ -268,12 +279,13 @@ "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.", + "Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!", "Your card was successfully removed!": "Dit kort blev fjernet!", "Your card was successfully saved!": "Dit kort blev gemt!", - "Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!", "Your current level": "Dit nuværende niveau", "Your details": "Dine oplysninger", "Your level": "Dit niveau", @@ -281,5 +293,28 @@ "Zip code": "Postnummer", "Zoo": "Zoo", "Zoom in": "Zoom ind", - "Zoom out": "Zoom ud" + "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", + "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer", + "km to city center": "km til byens centrum", + "next level:": "Næste niveau:", + "night": "nat", + "nights": "nætter", + "number": "nummer", + "or": "eller", + "points": "Point", + "special character": "speciel karakter", + "spendable points expiring by": "{points} Brugbare point udløber den {date}", + "to": "til", + "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 ab6a0d31d..ee4bccf96 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", "Attractions": "Attraktionen", "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,32 +68,40 @@ "Date of Birth": "Geburtsdatum", "Day": "Tag", "Description": "Beschreibung", + "Destination": "Bestimmungsort", "Destinations & hotels": "Reiseziele & Hotels", "Discard changes": "Änderungen verwerfen", "Discard unsaved changes?": "Nicht gespeicherte Änderungen verwerfen?", "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", @@ -97,16 +109,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", @@ -132,9 +144,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", @@ -149,10 +161,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", @@ -162,16 +172,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", @@ -179,7 +188,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", @@ -187,17 +195,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", @@ -209,6 +224,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", @@ -222,29 +238,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", "User information": "Nutzerinformation", "View as list": "Als Liste anzeigen", @@ -267,12 +279,13 @@ "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", @@ -280,5 +293,28 @@ "Zip code": "PLZ", "Zoo": "Zoo", "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 eb7f33489..1e51379f2 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -1,41 +1,43 @@ { + "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.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?", "Arrival date": "Arrival date", - "as of today": "as of today", "As our": "As our {level}", "As our Close Friend": "As our Close Friend", "At latest": "At latest", "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", - "booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}", - "booking.nights": "{totalNights, plural, one {# night} other {# nights}}", - "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", "Breakfast": "Breakfast", + "Breakfast buffet": "Breakfast buffet", "Breakfast excluded": "Breakfast excluded", "Breakfast included": "Breakfast included", + "Breakfast restaurant": "Breakfast restaurant", "Bus terminal": "Bus terminal", "Business": "Business", - "by": "by", "Cancel": "Cancel", - "characters": "characters", "Check in": "Check in", "Check out": "Check out", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.", @@ -73,26 +75,33 @@ "Distance to city centre": "{number}km to city centre", "Do you want to start the day with Scandics famous breakfast buffé?": "Do you want to start the day with Scandics famous breakfast buffé?", "Download the Scandic app": "Download the Scandic app", + "Earn bonus nights & points": "Earn bonus nights & points", "Edit": "Edit", "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", + "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.", "Fair": "Fair", - "FAQ": "FAQ", "Find booking": "Find booking", "Find hotels": "Find hotels", + "Firstname": "Firstname", "Flexibility": "Flexibility", "Former Scandic Hotel": "Former Scandic Hotel", "Free cancellation": "Free cancellation", "Free rebooking": "Free rebooking", "From": "From", "Get inspired": "Get inspired", + "Get member benefits & offers": "Get member benefits & offers", "Go back to edit": "Go back to edit", "Go back to overview": "Go back to overview", + "Guest information": "Guest information", "Guests & Rooms": "Guests & Rooms", "Hi": "Hi", "Highest level": "Highest level", @@ -100,16 +109,16 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel facilities", "Hotel surroundings": "Hotel surroundings", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "persons", - "hotelPages.rooms.roomCard.seeRoomDetails": "See room details", "Hotels": "Hotels", "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", - "km to city center": "km to city center", + "Join at no cost": "Join at no cost", + "King bed": "King bed", "Language": "Language", + "Lastname": "Lastname", "Latest searches": "Latest searches", "Level": "Level", "Level 1": "Level 1", @@ -135,9 +144,9 @@ "Member price": "Member price", "Member price from": "Member price from", "Members": "Members", - "Membership cards": "Membership cards", "Membership ID": "Membership ID", "Membership ID copied to clipboard": "Membership ID copied to clipboard", + "Membership cards": "Membership cards", "Menu": "Menu", "Modify": "Modify", "Month": "Month", @@ -152,10 +161,8 @@ "Nearby companies": "Nearby companies", "New password": "New password", "Next": "Next", - "next level:": "next level:", - "night": "night", - "nights": "nights", "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", @@ -165,13 +172,11 @@ "Non-refundable": "Non-refundable", "Not found": "Not found", "Nr night, nr adult": "{nights, number} night, {adults, number} adult", - "number": "number", "On your journey": "On your journey", "Open": "Open", "Open language menu": "Open language menu", "Open menu": "Open menu", "Open my pages menu": "Open my pages menu", - "or": "or", "Overview": "Overview", "Parking": "Parking", "Parking / Garage": "Parking / Garage", @@ -183,7 +188,6 @@ "Phone is required": "Phone is required", "Phone number": "Phone number", "Please enter a valid phone number": "Please enter a valid phone number", - "points": "Points", "Points": "Points", "Points being calculated": "Points being calculated", "Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021", @@ -191,17 +195,24 @@ "Points needed to level up": "Points needed to level up", "Points needed to stay on level": "Points needed to stay on level", "Previous victories": "Previous victories", + "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", @@ -227,29 +238,25 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.", "Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.", "Something went wrong!": "Something went wrong!", - "special character": "special character", - "spendable points expiring by": "{points} spendable points expiring by {date}", "Sports": "Sports", "Standard price": "Standard price", "Street": "Street", "Successfully updated profile!": "Successfully updated profile!", "Summary": "Summary", + "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.", "Thank you": "Thank you", "Theatre": "Theatre", "There are no transactions to display": "There are no transactions to display", "Things nearby HOTEL_NAME": "Things nearby {hotelName}", - "to": "to", "Total Points": "Total Points", "Tourist": "Tourist", "Transaction date": "Transaction date", "Transactions": "Transactions", "Transportations": "Transportations", "Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)", - "TUI Points": "TUI Points", "Type of bed": "Type of bed", "Type of room": "Type of room", - "uppercase letter": "uppercase letter", "Use bonus cheque": "Use bonus cheque", "User information": "User information", "View as list": "View as list", @@ -258,7 +265,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", @@ -272,12 +279,13 @@ "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.", + "Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!", "Your card was successfully removed!": "Your card was successfully removed!", "Your card was successfully saved!": "Your card was successfully saved!", - "Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!", "Your current level": "Your current level", "Your details": "Your details", "Your level": "Your level", @@ -285,5 +293,28 @@ "Zip code": "Zip code", "Zoo": "Zoo", "Zoom in": "Zoom in", - "Zoom out": "Zoom out" + "Zoom out": "Zoom out", + "as of today": "as of today", + "booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}", + "booking.nights": "{totalNights, plural, one {# night} other {# nights}}", + "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", + "by": "by", + "characters": "characters", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "persons", + "hotelPages.rooms.roomCard.seeRoomDetails": "See room details", + "km to city center": "km to city center", + "next level:": "next level:", + "night": "night", + "nights": "nights", + "number": "number", + "or": "or", + "points": "Points", + "special character": "special character", + "spendable points expiring by": "{points} spendable points expiring by {date}", + "to": "to", + "uppercase letter": "uppercase letter", + "{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 6faf8b396..5e3ab2fc4 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -1,39 +1,43 @@ { + "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?", "Arrival date": "Saapumispäivä", - "as of today": "tänään", "As our": "{level}-etu", "As our Close Friend": "Läheisenä ystävänämme", "At latest": "Viimeistään", "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", - "booking.nights": "{totalNights, plural, one {# yö} other {# yötä}}", "Breakfast": "Aamiainen", + "Breakfast buffet": "Aamiaisbuffet", "Breakfast excluded": "Aamiainen ei sisälly", "Breakfast included": "Aamiainen sisältyy", "Bus terminal": "Bussiasema", "Business": "Business", - "by": "mennessä", + "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Peruuttaa", - "characters": "hahmoja", "Check in": "Sisäänkirjautuminen", "Check out": "Uloskirjautuminen", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tarkista profiiliisi tallennetut luottokortit. Maksa tallennetulla kortilla kirjautuneena, jotta verkkokokemus on sujuvampi.", @@ -64,32 +68,40 @@ "Date of Birth": "Syntymäaika", "Day": "Päivä", "Description": "Kuvaus", + "Destination": "Kohde", "Destinations & hotels": "Kohteet ja hotellit", "Discard changes": "Hylkää muutokset", "Discard unsaved changes?": "Hylkäätkö tallentamattomat muutokset?", "Distance to city centre": "{number}km Etäisyys kaupunkiin", "Do you want to start the day with Scandics famous breakfast buffé?": "Haluatko aloittaa päiväsi Scandicsin kuuluisalla aamiaisbuffella?", "Download the Scandic app": "Lataa Scandic-sovellus", + "Earn bonus nights & points": "Ansaitse bonusöitä ja -pisteitä", "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", + "FAQ": "UKK", "Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.", "Fair": "Messukeskus", - "FAQ": "UKK", "Find booking": "Etsi varaus", "Find hotels": "Etsi hotelleja", + "Firstname": "Etunimi", "Flexibility": "Joustavuus", "Former Scandic Hotel": "Entinen Scandic-hotelli", "Free cancellation": "Ilmainen peruutus", "Free rebooking": "Ilmainen uudelleenvaraus", "From": "From", "Get inspired": "Inspiroidu", + "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", @@ -97,16 +109,16 @@ "Hotel": "Hotelli", "Hotel facilities": "Hotellin palvelut", "Hotel surroundings": "Hotellin ympäristö", - "hotelPages.rooms.roomCard.person": "henkilö", - "hotelPages.rooms.roomCard.persons": "Henkilöä", - "hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot", "Hotels": "Hotellit", "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", - "km to city center": "km keskustaan", + "Join at no cost": "Liity maksutta", + "King bed": "King-vuode", "Language": "Kieli", + "Lastname": "Sukunimi", "Latest searches": "Viimeisimmät haut", "Level": "Level", "Level 1": "Taso 1", @@ -132,9 +144,9 @@ "Member price": "Jäsenhinta", "Member price from": "Jäsenhinta alkaen", "Members": "Jäsenet", - "Membership cards": "Jäsenkortit", "Membership ID": "Jäsentunnus", "Membership ID copied to clipboard": "Jäsenyystunnus kopioitu leikepöydälle", + "Membership cards": "Jäsenkortit", "Menu": "Valikko", "Modify": "Muokkaa", "Month": "Kuukausi", @@ -149,10 +161,8 @@ "Nearby companies": "Läheiset yritykset", "New password": "Uusi salasana", "Next": "Seuraava", - "next level:": "pistettä tasolle:", - "night": "yö", - "nights": "yötä", "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", @@ -162,13 +172,11 @@ "Non-refundable": "Ei palautettavissa", "Not found": "Ei löydetty", "Nr night, nr adult": "{nights, number} yö, {adults, number} aikuinen", - "number": "määrä", "On your journey": "Matkallasi", "Open": "Avata", "Open language menu": "Avaa kielivalikko", "Open menu": "Avaa valikko", "Open my pages menu": "Avaa omat sivut -valikko", - "or": "tai", "Overview": "Yleiskatsaus", "Parking": "Pysäköinti", "Parking / Garage": "Pysäköinti / Autotalli", @@ -180,7 +188,6 @@ "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", - "points": "pistettä", "Points": "Pisteet", "Points being calculated": "Pisteitä lasketaan", "Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021", @@ -188,17 +195,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", @@ -210,6 +225,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", @@ -223,29 +239,25 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.", "Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.", "Something went wrong!": "Jotain meni pieleen!", - "special character": "erikoishahmo", - "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "Sports": "Urheilu", "Standard price": "Normaali hinta", "Street": "Katu", "Successfully updated profile!": "Profiilin päivitys onnistui!", "Summary": "Yhteenveto", + "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.", "Thank you": "Kiitos", "Theatre": "Teatteri", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", - "to": "to", "Total Points": "Kokonaispisteet", "Tourist": "Turisti", "Transaction date": "Tapahtuman päivämäärä", "Transactions": "Tapahtumat", "Transportations": "Kuljetukset", "Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)", - "TUI Points": "TUI Points", "Type of bed": "Vuodetyyppi", "Type of room": "Huonetyyppi", - "uppercase letter": "iso kirjain", "Use bonus cheque": "Käytä bonussekkiä", "User information": "Käyttäjän tiedot", "View as list": "Näytä listana", @@ -268,12 +280,13 @@ "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.", + "Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!", "Your card was successfully removed!": "Korttisi poistettiin onnistuneesti!", "Your card was successfully saved!": "Korttisi tallennettu onnistuneesti!", - "Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!", "Your current level": "Nykyinen tasosi", "Your details": "Tietosi", "Your level": "Tasosi", @@ -281,5 +294,28 @@ "Zip code": "Postinumero", "Zoo": "Eläintarha", "Zoom in": "Lähennä", - "Zoom out": "Loitonna" + "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ö", + "hotelPages.rooms.roomCard.persons": "Henkilöä", + "hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot", + "km to city center": "km keskustaan", + "next level:": "pistettä tasolle:", + "night": "yö", + "nights": "yötä", + "number": "määrä", + "or": "tai", + "points": "pistettä", + "special character": "erikoishahmo", + "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", + "to": "to", + "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 916e8d15d..1dd97b8f2 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -1,39 +1,42 @@ { + "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?", "Arrival date": "Ankomstdato", - "as of today": "per idag", "As our": "Som vår {level}", "As our Close Friend": "Som vår nære venn", "At latest": "Senest", "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", - "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "Breakfast": "Frokost", + "Breakfast buffet": "Breakfast buffet", "Breakfast excluded": "Frokost ekskludert", "Breakfast included": "Frokost inkludert", "Bus terminal": "Bussterminal", "Business": "Forretnings", - "by": "innen", "Cancel": "Avbryt", - "characters": "tegn", "Check in": "Sjekk inn", "Check out": "Sjekk ut", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sjekk ut kredittkortene som er lagret på profilen din. Betal med et lagret kort når du er pålogget for en jevnere nettopplevelse.", @@ -64,16 +67,21 @@ "Date of Birth": "Fødselsdato", "Day": "Dag", "Description": "Beskrivelse", + "Destination": "Destinasjon", "Destinations & hotels": "Destinasjoner og hoteller", "Discard changes": "Forkaste endringer", "Discard unsaved changes?": "Forkaste endringer som ikke er lagret?", "Distance to city centre": "{number}km til sentrum", "Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte frokostbuffé?", "Download the Scandic app": "Last ned Scandic-appen", + "Earn bonus nights & points": "Tjen bonusnetter og poeng", "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", @@ -82,14 +90,17 @@ "FAQ": "FAQ", "Find booking": "Finn booking", "Find hotels": "Finn hotell", + "Firstname": "Fornavn", "Flexibility": "Fleksibilitet", "Former Scandic Hotel": "Tidligere Scandic-hotell", "Free cancellation": "Gratis avbestilling", "Free rebooking": "Gratis ombooking", "From": "Fra", "Get inspired": "Bli inspirert", + "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å", @@ -97,16 +108,16 @@ "Hotel": "Hotel", "Hotel facilities": "Hotelfaciliteter", "Hotel surroundings": "Hotellomgivelser", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", - "hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet", "Hotels": "Hoteller", "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", - "km to city center": "km til sentrum", + "Join at no cost": "Bli med uten kostnad", + "King bed": "King-size-seng", "Language": "Språk", + "Lastname": "Etternavn", "Latest searches": "Siste søk", "Level": "Nivå", "Level 1": "Nivå 1", @@ -132,9 +143,9 @@ "Member price": "Medlemspris", "Member price from": "Medlemspris fra", "Members": "Medlemmer", - "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", "Membership ID copied to clipboard": "Medlems-ID kopiert til utklippstavlen", + "Membership cards": "Medlemskort", "Menu": "Menu", "Modify": "Endre", "Month": "Måned", @@ -149,10 +160,8 @@ "Nearby companies": "Nærliggende selskaper", "New password": "Nytt passord", "Next": "Neste", - "next level:": "Neste nivå:", - "night": "natt", - "nights": "netter", "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", @@ -162,13 +171,11 @@ "Non-refundable": "Ikke-refunderbart", "Not found": "Ikke funnet", "Nr night, nr adult": "{nights, number} natt, {adults, number} voksen", - "number": "antall", "On your journey": "På reisen din", "Open": "Åpen", "Open language menu": "Åpne språkmenyen", "Open menu": "Åpne menyen", "Open my pages menu": "Åpne mine sider menyen", - "or": "eller", "Overview": "Oversikt", "Parking": "Parkering", "Parking / Garage": "Parkering / Garasje", @@ -180,7 +187,6 @@ "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", - "points": "poeng", "Points": "Poeng", "Points being calculated": "Poeng beregnes", "Points earned prior to May 1, 2021": "Opptjente poeng før 1. mai 2021", @@ -188,17 +194,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", @@ -210,6 +223,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", @@ -223,29 +237,25 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.", "Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.", "Something went wrong!": "Noe gikk galt!", - "special character": "spesiell karakter", - "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", "Sports": "Sport", "Standard price": "Standardpris", "Street": "Gate", "Successfully updated profile!": "Vellykket oppdatert profil!", "Summary": "Sammendrag", + "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortell oss hvilken informasjon og hvilke oppdateringer du ønsker å motta, og hvordan, ved å klikke på lenken nedenfor.", "Thank you": "Takk", "Theatre": "Teater", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", - "to": "til", "Total Points": "Totale poeng", "Tourist": "Turist", "Transaction date": "Transaksjonsdato", "Transactions": "Transaksjoner", "Transportations": "Transport", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", - "TUI Points": "TUI Points", "Type of bed": "Sengtype", "Type of room": "Romtype", - "uppercase letter": "stor bokstav", "Use bonus cheque": "Bruk bonussjekk", "User information": "Brukerinformasjon", "View as list": "Vis som liste", @@ -268,12 +278,13 @@ "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.", + "Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!", "Your card was successfully removed!": "Kortet ditt ble fjernet!", "Your card was successfully saved!": "Kortet ditt ble lagret!", - "Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!", "Your current level": "Ditt nåværende nivå", "Your details": "Dine detaljer", "Your level": "Ditt nivå", @@ -281,5 +292,28 @@ "Zip code": "Post kode", "Zoo": "Dyrehage", "Zoom in": "Zoom inn", - "Zoom out": "Zoom ut" + "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", + "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet", + "km to city center": "km til sentrum", + "next level:": "Neste nivå:", + "night": "natt", + "nights": "netter", + "number": "antall", + "or": "eller", + "points": "poeng", + "special character": "spesiell karakter", + "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", + "to": "til", + "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 02291102e..6f5fcd37a 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -1,39 +1,43 @@ { + "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?", "Arrival date": "Ankomstdatum", - "as of today": "från och med idag", "As our": "Som vår {level}", "As our Close Friend": "Som vår nära vän", "At latest": "Senast", "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", - "booking.nights": "{totalNights, plural, one {# natt} other {# nätter}}", "Breakfast": "Frukost", + "Breakfast buffet": "Frukostbuffé", "Breakfast excluded": "Frukost ingår ej", "Breakfast included": "Frukost ingår", "Bus terminal": "Bussterminal", "Business": "Business", - "by": "innan", + "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Avbryt", - "characters": "tecken", "Check in": "Checka in", "Check out": "Checka ut", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Kolla in kreditkorten som sparats i din profil. Betala med ett sparat kort när du är inloggad för en smidigare webbupplevelse.", @@ -64,32 +68,40 @@ "Date of Birth": "Födelsedatum", "Day": "Dag", "Description": "Beskrivning", + "Destination": "Destination", "Destinations & hotels": "Destinationer & hotell", "Discard changes": "Ignorera ändringar", "Discard unsaved changes?": "Vill du ignorera ändringar som inte har sparats?", "Distance to city centre": "{number}km till centrum", "Do you want to start the day with Scandics famous breakfast buffé?": "Vill du starta dagen med Scandics berömda frukostbuffé?", "Download the Scandic app": "Ladda ner Scandic-appen", + "Earn bonus nights & points": "Tjäna bonusnätter och poäng", "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", + "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.", "Fair": "Mässa", - "FAQ": "FAQ", "Find booking": "Hitta bokning", "Find hotels": "Hitta hotell", + "Firstname": "Förnamn", "Flexibility": "Flexibilitet", "Former Scandic Hotel": "Tidigare Scandichotell", "Free cancellation": "Fri avbokning", "Free rebooking": "Fri ombokning", "From": "Från", "Get inspired": "Bli inspirerad", + "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å", @@ -97,16 +109,17 @@ "Hotel": "Hotell", "Hotel facilities": "Hotellfaciliteter", "Hotel surroundings": "Hotellomgivning", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", - "hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet", "Hotels": "Hotell", "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", - "km to city center": "km till stadens centrum", + "Join at no cost": "Gå med utan kostnad", + "King bed": "King size-säng", "Language": "Språk", + "Lastname": "Efternamn", "Latest searches": "Senaste sökningarna", "Level": "Nivå", "Level 1": "Nivå 1", @@ -132,9 +145,9 @@ "Member price": "Medlemspris", "Member price from": "Medlemspris från", "Members": "Medlemmar", - "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", "Membership ID copied to clipboard": "Medlems-ID kopierat till urklipp", + "Membership cards": "Medlemskort", "Menu": "Meny", "Modify": "Ändra", "Month": "Månad", @@ -149,10 +162,8 @@ "Nearby companies": "Närliggande företag", "New password": "Nytt lösenord", "Next": "Nästa", - "next level:": "Nästa nivå:", - "night": "natt", - "nights": "nätter", "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", @@ -162,13 +173,11 @@ "Non-refundable": "Ej återbetalningsbar", "Not found": "Hittades inte", "Nr night, nr adult": "{nights, number} natt, {adults, number} vuxen", - "number": "nummer", "On your journey": "På din resa", "Open": "Öppna", "Open language menu": "Öppna språkmenyn", "Open menu": "Öppna menyn", "Open my pages menu": "Öppna mina sidor menyn", - "or": "eller", "Overview": "Översikt", "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", @@ -180,7 +189,6 @@ "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", - "points": "poäng", "Points": "Poäng", "Points being calculated": "Poäng beräknas", "Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021", @@ -188,17 +196,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", @@ -210,6 +225,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", @@ -223,29 +239,25 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.", "Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.", "Something went wrong!": "Något gick fel!", - "special character": "speciell karaktär", - "spendable points expiring by": "{points} poäng förfaller {date}", "Sports": "Sport", "Standard price": "Standardpris", "Street": "Gata", "Successfully updated profile!": "Profilen har uppdaterats framgångsrikt!", "Summary": "Sammanfattning", + "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Berätta för oss vilken information och vilka uppdateringar du vill få och hur genom att klicka på länken nedan.", "Thank you": "Tack", "Theatre": "Teater", "There are no transactions to display": "Det finns inga transaktioner att visa", "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", - "to": "till", "Total Points": "Poäng totalt", "Tourist": "Turist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktioner", "Transportations": "Transport", "Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)", - "TUI Points": "TUI Points", "Type of bed": "Sängtyp", "Type of room": "Rumstyp", - "uppercase letter": "stor bokstav", "Use bonus cheque": "Use bonus cheque", "User information": "Användarinformation", "View as list": "Visa som lista", @@ -268,12 +280,13 @@ "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.", + "Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!", "Your card was successfully removed!": "Ditt kort har tagits bort!", "Your card was successfully saved!": "Ditt kort har sparats!", - "Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!", "Your current level": "Din nuvarande nivå", "Your details": "Dina uppgifter", "Your level": "Din nivå", @@ -281,5 +294,28 @@ "Zip code": "Postnummer", "Zoo": "Djurpark", "Zoom in": "Zooma in", - "Zoom out": "Zooma ut" + "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", + "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet", + "km to city center": "km till stadens centrum", + "next level:": "Nästa nivå:", + "night": "natt", + "nights": "nätter", + "number": "nummer", + "or": "eller", + "points": "poäng", + "special character": "speciell karaktär", + "spendable points expiring by": "{points} poäng förfaller {date}", + "to": "till", + "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..9ead56554 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -6,7 +6,7 @@ export namespace endpoints { profile = "profile/v0/Profile", } export const enum v1 { - availability = "availability/v1/availabilities/city", + hotelsAvailability = "availability/v1/availabilities/city", profile = "profile/v1/Profile", booking = "booking/v1/Bookings", creditCards = `${profile}/creditCards`, @@ -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/discriminatedUnion.ts b/lib/discriminatedUnion.ts index 2f7c5c12a..4da9486bb 100644 --- a/lib/discriminatedUnion.ts +++ b/lib/discriminatedUnion.ts @@ -21,7 +21,7 @@ import type { * is an Interface e.g). */ -export function discriminatedUnion(options: T[]) { +export function discriminatedUnion(options: Option[]) { return z .discriminatedUnion("__typename", [ z.object({ __typename: z.literal(undefined) }), @@ -37,6 +37,12 @@ export function discriminatedUnion(options: T[]) { } throw new Error(error.message) }) + .transform((data) => { + if (data.__typename === "undefined" || data.__typename === undefined) { + return null + } + return data as R + }) } export function discriminatedUnionArray(options: T[]) { 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/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 6130a8556..c1e58bcd7 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -10,6 +10,12 @@ export const getProfile = cache(async function getMemoizedProfile() { return serverClient().user.get() }) +export const getProfileSafely = cache( + async function getMemoizedProfileSafely() { + return serverClient().user.getSafely() + } +) + export const getFooter = cache(async function getMemoizedFooter() { return serverClient().contentstack.base.footer() }) 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/base/output.ts b/server/routers/contentstack/base/output.ts index ddca32139..02651dfe2 100644 --- a/server/routers/contentstack/base/output.ts +++ b/server/routers/contentstack/base/output.ts @@ -551,10 +551,13 @@ const linkSchema = z }) .transform((data) => { if (data.linkConnection.edges.length) { - const link = pageLinks.transform(data.linkConnection.edges[0].node) - if (link) { - return { - link, + const linkNode = data.linkConnection.edges[0].node + if (linkNode) { + const link = pageLinks.transform(linkNode) + if (link) { + return { + link, + } } } } 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/contentstack/schemas/linkConnection.ts b/server/routers/contentstack/schemas/linkConnection.ts index f70e8dd81..902e6fca1 100644 --- a/server/routers/contentstack/schemas/linkConnection.ts +++ b/server/routers/contentstack/schemas/linkConnection.ts @@ -25,10 +25,13 @@ export const linkConnectionSchema = z }) .transform((data) => { if (data.linkConnection.edges.length) { - const link = pageLinks.transform(data.linkConnection.edges[0].node) - if (link) { - return { - link, + const linkNode = data.linkConnection.edges[0].node + if (linkNode) { + const link = pageLinks.transform(linkNode) + if (link) { + return { + link, + } } } } @@ -54,17 +57,20 @@ export const linkConnectionRefs = z linkConnection: z.object({ edges: z.array( z.object({ - node: linkRefsUnionSchema, + node: discriminatedUnion(linkRefsUnionSchema.options), }) ), }), }) .transform((data) => { if (data.linkConnection.edges.length) { - const link = pageLinks.transformRef(data.linkConnection.edges[0].node) - if (link) { - return { - link, + const linkNode = data.linkConnection.edges[0].node + if (linkNode) { + const link = pageLinks.transformRef(linkNode) + if (link) { + return { + link, + } } } } diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 416d41eee..b0ff5e679 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -6,7 +6,7 @@ export const getHotelInputSchema = z.object({ .optional(), }) -export const getAvailabilityInputSchema = z.object({ +export const getHotelsAvailabilityInputSchema = z.object({ cityId: z.string(), roomStayStartDate: z.string(), roomStayEndDate: z.string(), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index d1fb2dfe8..d233f1a82 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, }), @@ -512,26 +525,18 @@ const occupancySchema = z.object({ const bestPricePerStaySchema = z.object({ currency: z.string(), - amount: z.number(), - regularAmount: z.number(), - memberAmount: z.number(), - discountRate: z.number(), - discountAmount: z.number(), - points: z.number(), - numberOfVouchers: z.number(), - numberOfBonusCheques: z.number(), + // TODO: remove optional when API is ready + regularAmount: z.string().optional(), + // TODO: remove optional when API is ready + memberAmount: z.string().optional(), }) const bestPricePerNightSchema = z.object({ currency: z.string(), - amount: z.number(), - regularAmount: z.number(), - memberAmount: z.number(), - discountRate: z.number(), - discountAmount: z.number(), - points: z.number(), - numberOfVouchers: z.number(), - numberOfBonusCheques: z.number(), + // TODO: remove optional when API is ready + regularAmount: z.string().optional(), + // TODO: remove optional when API is ready + memberAmount: z.string().optional(), }) const linksSchema = z.object({ @@ -543,7 +548,7 @@ const linksSchema = z.object({ ), }) -const availabilitySchema = z.object({ +const hotelsAvailabilitySchema = z.object({ data: z.array( z.object({ attributes: z.object({ @@ -562,10 +567,10 @@ const availabilitySchema = z.object({ ), }) -export const getAvailabilitySchema = availabilitySchema -export type Availability = z.infer -export type AvailabilityPrices = - Availability["data"][number]["attributes"]["bestPricePerNight"] +export const getHotelsAvailabilitySchema = hotelsAvailabilitySchema +export type HotelsAvailability = z.infer +export type HotelsAvailabilityPrices = + HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"] const flexibilityPrice = z.object({ standard: z.number(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 9c1042dbd..0e319bf6e 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -20,14 +20,14 @@ import { toApiLang } from "@/server/utils" import { hotelPageSchema } from "../contentstack/hotelPage/output" import { - getAvailabilityInputSchema, getHotelInputSchema, + getHotelsAvailabilityInputSchema, getlHotelDataInputSchema, getRatesInputSchema, } from "./input" import { - getAvailabilitySchema, getHotelDataSchema, + getHotelsAvailabilitySchema, getRatesSchema, roomSchema, } from "./output" @@ -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") @@ -49,12 +51,14 @@ const getHotelCounter = meter.createCounter("trpc.hotel.get") const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success") const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail") -const availabilityCounter = meter.createCounter("trpc.hotel.availability") -const availabilitySuccessCounter = meter.createCounter( - "trpc.hotel.availability-success" +const hotelsAvailabilityCounter = meter.createCounter( + "trpc.hotel.availability.hotels" ) -const availabilityFailCounter = meter.createCounter( - "trpc.hotel.availability-fail" +const hotelsAvailabilitySuccessCounter = meter.createCounter( + "trpc.hotel.availability.hotels-success" +) +const hotelsAvailabilityFailCounter = meter.createCounter( + "trpc.hotel.availability.hotels-fail" ) async function getContentstackData( @@ -173,7 +177,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 +215,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,11 +248,12 @@ export const hotelQueryRouter = router({ pointsOfInterest: hotelAttributes.pointsOfInterest, roomCategories, activitiesCard: activities?.upcoming_activities_card, + facilities, } }), availability: router({ - get: hotelServiceProcedure - .input(getAvailabilityInputSchema) + hotels: hotelServiceProcedure + .input(getHotelsAvailabilityInputSchema) .query(async ({ input, ctx }) => { const { cityId, @@ -257,7 +276,7 @@ export const hotelQueryRouter = router({ attachedProfileId, } - availabilityCounter.add(1, { + hotelsAvailabilityCounter.add(1, { cityId, roomStayStartDate, roomStayEndDate, @@ -267,11 +286,11 @@ export const hotelQueryRouter = router({ reservationProfileType, }) console.info( - "api.hotels.availability start", + "api.hotels.hotelsAvailability start", JSON.stringify({ query: { cityId, params } }) ) const apiResponse = await api.get( - `${api.endpoints.v1.availability}/${cityId}`, + `${api.endpoints.v1.hotelsAvailability}/${cityId}`, { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -281,7 +300,7 @@ export const hotelQueryRouter = router({ ) if (!apiResponse.ok) { const text = await apiResponse.text() - availabilityFailCounter.add(1, { + hotelsAvailabilityFailCounter.add(1, { cityId, roomStayStartDate, roomStayEndDate, @@ -297,7 +316,7 @@ export const hotelQueryRouter = router({ }), }) console.error( - "api.hotels.availability error", + "api.hotels.hotelsAvailability error", JSON.stringify({ query: { cityId, params }, error: { @@ -311,9 +330,9 @@ export const hotelQueryRouter = router({ } const apiJson = await apiResponse.json() const validateAvailabilityData = - getAvailabilitySchema.safeParse(apiJson) + getHotelsAvailabilitySchema.safeParse(apiJson) if (!validateAvailabilityData.success) { - availabilityFailCounter.add(1, { + hotelsAvailabilityFailCounter.add(1, { cityId, roomStayStartDate, roomStayEndDate, @@ -325,7 +344,7 @@ export const hotelQueryRouter = router({ error: JSON.stringify(validateAvailabilityData.error), }) console.error( - "api.hotels.availability validation error", + "api.hotels.hotelsAvailability validation error", JSON.stringify({ query: { cityId, params }, error: validateAvailabilityData.error, @@ -333,7 +352,7 @@ export const hotelQueryRouter = router({ ) throw badRequestError() } - availabilitySuccessCounter.add(1, { + hotelsAvailabilitySuccessCounter.add(1, { cityId, roomStayStartDate, roomStayEndDate, @@ -343,7 +362,7 @@ export const hotelQueryRouter = router({ reservationProfileType, }) console.info( - "api.hotels.availability success", + "api.hotels.hotelsAvailability success", JSON.stringify({ query: { cityId, params: params }, }) 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/server/routers/user/query.ts b/server/routers/user/query.ts index b3e9a1e4d..28dee9cd2 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -1,4 +1,5 @@ import { metrics } from "@opentelemetry/api" +import { SafeParseSuccess } from "zod" import * as api from "@/lib/api" import { @@ -27,8 +28,8 @@ import type { LoginType, TrackingSDKUserData, } from "@/types/components/tracking" -import { BlocksEnums } from "@/types/enums/blocks" import { Transactions } from "@/types/enums/transactions" +import { User } from "@/types/user" import type { MembershipLevel } from "@/constants/membershipLevels" // OpenTelemetry metrics: User @@ -161,6 +162,51 @@ export async function getVerifiedUser({ session }: { session: Session }) { return verifiedData } +function parsedUser(data: User, isMFA: boolean) { + const country = countries.find((c) => c.code === data.address.countryCode) + + const user = { + address: { + city: data.address.city, + country: country?.name ?? "", + countryCode: data.address.countryCode, + streetAddress: data.address.streetAddress, + zipCode: data.address.zipCode, + }, + dateOfBirth: data.dateOfBirth, + email: data.email, + firstName: data.firstName, + language: data.language, + lastName: data.lastName, + membership: getMembership(data.memberships), + memberships: data.memberships, + name: `${data.firstName} ${data.lastName}`, + phoneNumber: data.phoneNumber, + profileId: data.profileId, + } + + if (!isMFA) { + if (user.address.city) { + user.address.city = maskValue.text(user.address.city) + } + if (user.address.streetAddress) { + user.address.streetAddress = maskValue.text(user.address.streetAddress) + } + + user.address.zipCode = data.address?.zipCode + ? maskValue.text(data.address.zipCode) + : "" + + user.dateOfBirth = maskValue.all(user.dateOfBirth) + + user.email = maskValue.email(user.email) + + user.phoneNumber = user.phoneNumber ? maskValue.phone(user.phoneNumber) : "" + } + + return user +} + export const userQueryRouter = router({ get: protectedProcedure .use(async function (opts) { @@ -184,57 +230,25 @@ export const userQueryRouter = router({ return data } - const verifiedData = data - - const country = countries.find( - (c) => c.code === verifiedData.data.address.countryCode - ) - - const user = { - address: { - city: verifiedData.data.address.city, - country: country?.name ?? "", - countryCode: verifiedData.data.address.countryCode, - streetAddress: verifiedData.data.address.streetAddress, - zipCode: verifiedData.data.address.zipCode, - }, - dateOfBirth: verifiedData.data.dateOfBirth, - email: verifiedData.data.email, - firstName: verifiedData.data.firstName, - language: verifiedData.data.language, - lastName: verifiedData.data.lastName, - membership: getMembership(verifiedData.data.memberships), - memberships: verifiedData.data.memberships, - name: `${verifiedData.data.firstName} ${verifiedData.data.lastName}`, - phoneNumber: verifiedData.data.phoneNumber, - profileId: verifiedData.data.profileId, - } - - if (!ctx.isMFA) { - if (user.address.city) { - user.address.city = maskValue.text(user.address.city) - } - if (user.address.streetAddress) { - user.address.streetAddress = maskValue.text( - user.address.streetAddress - ) - } - - user.address.zipCode = verifiedData.data.address?.zipCode - ? maskValue.text(verifiedData.data.address.zipCode) - : "" - - user.dateOfBirth = maskValue.all(user.dateOfBirth) - - user.email = maskValue.email(user.email) - - user.phoneNumber = user.phoneNumber - ? maskValue.phone(user.phoneNumber) - : "" - } - - return user + return parsedUser(data.data, ctx.isMFA) }), + getSafely: safeProtectedProcedure.query(async function getUser({ ctx }) { + if (!ctx.session) { + return null + } + + const data = await getVerifiedUser({ session: ctx.session }) + + if (!data) { + return null + } + + if ("error" in data) { + return data + } + + return parsedUser(data.data, true) + }), name: safeProtectedProcedure.query(async function ({ ctx }) { if (!ctx.session) { return null 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/enterDetails/details.ts b/types/components/enterDetails/details.ts new file mode 100644 index 000000000..84996da56 --- /dev/null +++ b/types/components/enterDetails/details.ts @@ -0,0 +1,11 @@ +import { z } from "zod" + +import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" + +import { User } from "@/types/user" + +export interface DetailsSchema extends z.output {} + +export interface DetailsProps { + user: User | null +} 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/hotelReservation/selectHotel/hotelCardListingProps.ts b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts index 0ab3df1ad..431d53c0e 100644 --- a/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts +++ b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts @@ -1,4 +1,4 @@ -import { AvailabilityPrices } from "@/server/routers/hotels/output" +import { HotelsAvailabilityPrices } from "@/server/routers/hotels/output" import { Hotel } from "@/types/hotel" @@ -8,5 +8,5 @@ export type HotelCardListingProps = { export type HotelData = { hotelData: Hotel - price: AvailabilityPrices + price: HotelsAvailabilityPrices } diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts new file mode 100644 index 000000000..d5203f461 --- /dev/null +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -0,0 +1,7 @@ +import { Rate } from "@/server/routers/hotels/output" + +export interface RoomSelectionProps { + rates: Rate[] + nrOfAdults: number + nrOfNights: number +} diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index 69460962b..4da378cdd 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -1,5 +1,3 @@ -import { Rate } from "@/server/routers/hotels/output" - import { Hotel } from "@/types/hotel" export interface SectionProps { @@ -27,12 +25,6 @@ export interface BreakfastSelectionProps extends SectionProps { }[] } -export interface RoomSelectionProps extends SectionProps { - alternatives: Rate[] - nrOfAdults: number - nrOfNights: number -} - export interface DetailsProps extends SectionProps {} export interface PaymentProps { diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts new file mode 100644 index 000000000..bc325bbf2 --- /dev/null +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -0,0 +1,5 @@ +export interface SelectRateSearchParams { + fromDate: string + toDate: string + hotel: 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/components/search.ts b/types/components/search.ts index b35f9d3d2..6f1017126 100644 --- a/types/components/search.ts +++ b/types/components/search.ts @@ -15,40 +15,43 @@ export interface SearchListProps { getItemProps: PropGetters["getItemProps"] getMenuProps: PropGetters["getMenuProps"] isOpen: boolean + handleClearSearchHistory: () => void highlightedIndex: HighlightedIndex locations: Locations search: string searchHistory: Locations | null } -export interface ListProps { - getItemProps: PropGetters["getItemProps"] - highlightedIndex: HighlightedIndex +export interface ListProps + extends Pick< + SearchListProps, + "getItemProps" | "highlightedIndex" | "locations" + > { initialIndex?: number label?: string - locations: Locations } -export interface ListItemProps { - getItemProps: PropGetters["getItemProps"] - highlightedIndex: HighlightedIndex +export interface ListItemProps + extends Pick { index: number location: Location } export interface DialogProps extends React.PropsWithChildren, - VariantProps { + VariantProps, + Pick { className?: string - getMenuProps: PropGetters["getMenuProps"] } -export interface ErrorDialogProps extends React.PropsWithChildren { - getMenuProps: PropGetters["getMenuProps"] -} +export interface ErrorDialogProps + extends React.PropsWithChildren, + Pick { } -export interface ClearSearchButtonProps { - getItemProps: PropGetters["getItemProps"] - highlightedIndex: HighlightedIndex +export interface ClearSearchButtonProps + extends Pick< + SearchListProps, + "getItemProps" | "handleClearSearchHistory" | "highlightedIndex" + > { index: number } 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/types/requests/system.ts b/types/requests/system.ts index d3d053dac..2fb397201 100644 --- a/types/requests/system.ts +++ b/types/requests/system.ts @@ -1,9 +1,7 @@ -import { Lang } from "@/constants/languages" +import { z } from "zod" + +import { systemSchema } from "@/server/routers/contentstack/schemas/system" export interface System { - system: { - content_type_uid: string - locale: Lang - uid: string - } + system: z.output } 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 } -}