diff --git a/actions/editProfile.ts b/actions/editProfile.ts index 010772c4a..8f0791c48 100644 --- a/actions/editProfile.ts +++ b/actions/editProfile.ts @@ -4,7 +4,7 @@ import { z } from "zod" import { ApiLang } from "@/constants/languages" import * as api from "@/lib/api" -import { serverClient } from "@/lib/trpc/server" +import { getProfile } from "@/lib/trpc/memoizedRequests" import { protectedServerActionProcedure } from "@/server/trpc" import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema" @@ -68,7 +68,7 @@ export const editProfile = protectedServerActionProcedure } } - const profile = await serverClient().user.get() + const profile = await getProfile() if (!profile || "error" in profile) { console.error( "editProfile profile fetch error", diff --git a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx index a5b818f77..6775fd188 100644 --- a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx @@ -1,4 +1,7 @@ +import { Suspense } from "react" + import Breadcrumbs from "@/components/Breadcrumbs" +import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" import { setLang } from "@/i18n/serverContext" import { LangParams, PageArgs } from "@/types/params" @@ -6,5 +9,9 @@ import { LangParams, PageArgs } from "@/types/params" export default function AllBreadcrumbs({ params }: PageArgs) { setLang(params.lang) - return + return ( + }> + + + ) } diff --git a/app/[lang]/(live)/(protected)/my-pages/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/layout.tsx index f5b84dbd7..43268adca 100644 --- a/app/[lang]/(live)/(protected)/my-pages/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/layout.tsx @@ -1,5 +1,9 @@ +import { Suspense } from "react" + +import LoadingSpinner from "@/components/LoadingSpinner" import Sidebar from "@/components/MyPages/Sidebar" +// import Surprises from "@/components/MyPages/Surprises" import styles from "./layout.module.css" export default async function MyPagesLayout({ @@ -13,10 +17,16 @@ export default async function MyPagesLayout({
{breadcrumbs}
- + }> + + {children}
+ + {/* TODO: Waiting on new API stuff + + */} ) } diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx index 226a33860..693bdc171 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx @@ -1,4 +1,4 @@ -import { serverClient } from "@/lib/trpc/server" +import { getMembershipCards } from "@/lib/trpc/memoizedRequests" import { PlusCircleIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" @@ -16,7 +16,7 @@ export default async function MembershipCardSlot({ }: PageArgs) { setLang(params.lang) const { formatMessage } = await getIntl() - const membershipCards = await serverClient().user.membershipCards() + const membershipCards = await getMembershipCards() return (
diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx index 4119f48e3..ec8f14553 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx @@ -1,4 +1,7 @@ +import { Suspense } from "react" + import Breadcrumbs from "@/components/Breadcrumbs" +import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" import { setLang } from "@/i18n/serverContext" import { LangParams, PageArgs } from "@/types/params" @@ -6,5 +9,9 @@ import { LangParams, PageArgs } from "@/types/params" export default function PageBreadcrumbs({ params }: PageArgs) { setLang(params.lang) - return + return ( + }> + + + ) } diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index cbb83a9dc..08fa09422 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -16,7 +16,7 @@ import { export { generateMetadata } from "@/utils/generateMetadata" -export default async function ContentTypePage({ +export default function ContentTypePage({ params, }: PageArgs) { setLang(params.lang) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 2f2465756..1a2bb5118 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -1,7 +1,9 @@ import { differenceInCalendarDays, format, isWeekend } from "date-fns" +import { notFound } from "next/navigation" import { Lang } from "@/constants/languages" import { selectHotelMap } from "@/constants/routes/hotelReservation" +import { getLocations } from "@/lib/trpc/memoizedRequests" import { fetchAvailableHotels, @@ -9,6 +11,7 @@ import { } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" +import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { ChevronRightIcon } from "@/components/Icons" import StaticMap from "@/components/Maps/StaticMap" import Link from "@/components/TempDesignSystem/Link" @@ -18,6 +21,7 @@ import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" +import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" import { TrackingChannelEnum, TrackingSDKHotelInfo, @@ -27,21 +31,38 @@ import { LangParams, PageArgs } from "@/types/params" export default async function SelectHotelPage({ params, -}: PageArgs) { + searchParams, +}: PageArgs) { setLang(params.lang) + const locations = await getLocations() + + if (!locations || "error" in locations) { + return null + } + const city = locations.data.find( + (location) => + location.name.toLowerCase() === searchParams.city.toLowerCase() + ) + if (!city) return notFound() - const tempSearchTerm = "Stockholm" - const tempArrivalDate = new Date("2024-10-25") - const tempDepartureDate = new Date("2024-10-26") const intl = await getIntl() + const selectHotelParams = new URLSearchParams(searchParams) + const selectHotelParamsObject = + getHotelReservationQueryParams(selectHotelParams) + const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms + const children = selectHotelParamsObject.room[0].child?.length // TODO: Handle multiple rooms const hotels = await fetchAvailableHotels({ - cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54", - roomStayStartDate: "2024-11-02", - roomStayEndDate: "2024-11-03", - adults: 1, + cityId: city.id, + roomStayStartDate: searchParams.fromDate, + roomStayEndDate: searchParams.toDate, + adults, + children, }) + const arrivalDate = new Date(searchParams.fromDate) + const departureDate = new Date(searchParams.toDate) + const filterList = getFiltersFromHotels(hotels) const pageTrackingData: TrackingSDKPageData = { @@ -55,16 +76,16 @@ export default async function SelectHotelPage({ const hotelsTrackingData: TrackingSDKHotelInfo = { availableResults: hotels.length, - searchTerm: tempSearchTerm, - arrivalDate: format(tempArrivalDate, "yyyy-MM-dd"), - departureDate: format(tempDepartureDate, "yyyy-MM-dd"), - noOfAdults: 1, // TODO: Use values from searchParams - noOfChildren: 0, // TODO: Use values from searchParams - noOfRooms: 1, // TODO: Use values from searchParams - duration: differenceInCalendarDays(tempDepartureDate, tempArrivalDate), - leadTime: differenceInCalendarDays(tempArrivalDate, new Date()), + searchTerm: searchParams.city, + arrivalDate: format(arrivalDate, "yyyy-MM-dd"), + departureDate: format(departureDate, "yyyy-MM-dd"), + noOfAdults: adults, + noOfChildren: children, + noOfRooms: 1, // // TODO: Handle multiple rooms + duration: differenceInCalendarDays(departureDate, arrivalDate), + leadTime: differenceInCalendarDays(arrivalDate, new Date()), searchType: "destination", - bookingTypeofDay: isWeekend(tempArrivalDate) ? "weekend" : "weekday", + bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", country: hotels[0].hotelData.address.country, region: hotels[0].hotelData.address.city, } @@ -74,12 +95,12 @@ export default async function SelectHotelPage({
) { if (env.HIDE_FOR_NEXT_RELEASE) { return null } @@ -18,5 +24,5 @@ export default async function BookingWidgetPage() { return null } - return + return } diff --git a/app/[lang]/webview/loading.tsx b/app/[lang]/webview/loading.tsx new file mode 100644 index 000000000..c739b6635 --- /dev/null +++ b/app/[lang]/webview/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return +} diff --git a/app/globals.css b/app/globals.css index 5fff0324b..539f9827a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -115,7 +115,8 @@ --header-z-index: 10; --menu-overlay-z-index: 10; --dialog-z-index: 9; - --sidepeek-z-index: 11; + --sidepeek-z-index: 100; + --lightbox-z-index: 150; } * { diff --git a/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx b/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx index a17e8d0ee..1a99f8985 100644 --- a/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx +++ b/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx @@ -14,11 +14,12 @@ export default async function Points({ user }: UserProps) { const membership = getMembership(user.memberships) - const nextLevel = membership?.nextLevel - ? await serverClient().contentstack.loyaltyLevels.byLevel({ - level: MembershipLevelEnum[membership.nextLevel], - }) - : null + const nextLevel = + membership?.nextLevel && MembershipLevelEnum[membership.nextLevel] + ? await serverClient().contentstack.loyaltyLevels.byLevel({ + level: MembershipLevelEnum[membership.nextLevel], + }) + : null return ( diff --git a/components/Blocks/DynamicContent/Overview/overview.module.css b/components/Blocks/DynamicContent/Overview/overview.module.css index 1dd9b754f..b1b2b62a0 100644 --- a/components/Blocks/DynamicContent/Overview/overview.module.css +++ b/components/Blocks/DynamicContent/Overview/overview.module.css @@ -1,5 +1,5 @@ .divider { - padding-top: var(--Spacing-x2); + margin-top: var(--Spacing-x2); } @media screen and (max-width: 767px) { diff --git a/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/rewardCard.module.css b/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/rewardCard.module.css index 02984b0f1..d7b3bd729 100644 --- a/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/rewardCard.module.css +++ b/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/rewardCard.module.css @@ -1,6 +1,5 @@ .rewardCard { padding-bottom: var(--Spacing-x-one-and-half); - z-index: 2; grid-column: 1/3; } diff --git a/components/Blocks/DynamicContent/OverviewTable/index.tsx b/components/Blocks/DynamicContent/OverviewTable/index.tsx index 866a0c7d6..f39fb9a25 100644 --- a/components/Blocks/DynamicContent/OverviewTable/index.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/index.tsx @@ -1,3 +1,4 @@ +import { getMembershipLevelSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import SectionWrapper from "../SectionWrapper" @@ -9,8 +10,11 @@ export default async function OverviewTable({ dynamic_content, firstItem, }: OverviewTableProps) { - const levels = await serverClient().contentstack.rewards.all() - const membershipLevel = await serverClient().user.safeMembershipLevel() + const [levels, membershipLevel] = await Promise.all([ + serverClient().contentstack.rewards.all(), + getMembershipLevelSafely(), + ]) + return ( }> + + + ) +} + +function DynamicContentBlocks(props: DynamicContentProps) { + const { dynamic_content, firstItem } = props switch (dynamic_content.component) { case DynamicContentEnum.Blocks.components.current_benefits: return diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 68a6a1fc2..fda8683c9 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -10,6 +10,7 @@ import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import { CloseLargeIcon } from "@/components/Icons" import { debounce } from "@/utils/debounce" +import getHotelReservationQueryParams from "../HotelReservation/SelectRate/RoomSelection/utils" import MobileToggleButton from "./MobileToggleButton" import styles from "./bookingWidget.module.css" @@ -23,6 +24,7 @@ import type { Location } from "@/types/trpc/routers/hotel/locations" export default function BookingWidgetClient({ locations, type, + searchParams, }: BookingWidgetClientProps) { const [isOpen, setIsOpen] = useState(false) @@ -33,17 +35,59 @@ export default function BookingWidgetClient({ const initialSelectedLocation: Location | undefined = sessionStorageSearchData ? JSON.parse(sessionStorageSearchData) : undefined + + const bookingWidgetSearchParams = searchParams + ? new URLSearchParams(searchParams) + : undefined + const bookingWidgetSearchData = bookingWidgetSearchParams + ? getHotelReservationQueryParams(bookingWidgetSearchParams) + : undefined + + const getLocationObj = (destination: string): Location | undefined => { + if (destination) { + const location: Location | undefined = locations.find((location) => { + return ( + location.name.toLowerCase() === destination.toLowerCase() || + //@ts-ignore (due to operaId not property error) + (location.operaId && location.operaId == destination) + ) + }) + return location + } + return undefined + } + + const isDateParamValid = + bookingWidgetSearchData?.fromDate && + bookingWidgetSearchData?.toDate && + dt(bookingWidgetSearchData?.toDate.toString()).isAfter( + dt(bookingWidgetSearchData?.fromDate.toString()) + ) + + const selectedLocation = bookingWidgetSearchData + ? getLocationObj( + (bookingWidgetSearchData.city ?? + bookingWidgetSearchData.hotel) as string + ) + : undefined + const methods = useForm({ defaultValues: { - search: initialSelectedLocation?.name ?? "", - location: sessionStorageSearchData - ? encodeURIComponent(sessionStorageSearchData) - : undefined, + search: selectedLocation?.name ?? initialSelectedLocation?.name ?? "", + location: selectedLocation + ? JSON.stringify(selectedLocation) + : sessionStorageSearchData + ? encodeURIComponent(sessionStorageSearchData) + : undefined, date: { // UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 // This is specifically to handle timezones falling in different dates. - fromDate: dt().utc().format("YYYY-MM-DD"), - toDate: dt().utc().add(1, "day").format("YYYY-MM-DD"), + fromDate: isDateParamValid + ? bookingWidgetSearchData?.fromDate.toString() + : dt().utc().format("YYYY-MM-DD"), + toDate: isDateParamValid + ? bookingWidgetSearchData?.toDate?.toString() + : dt().utc().add(1, "day").format("YYYY-MM-DD"), }, bookingCode: "", redemption: false, diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx index 113d8b365..df84cd65d 100644 --- a/components/BookingWidget/MobileToggleButton/index.tsx +++ b/components/BookingWidget/MobileToggleButton/index.tsx @@ -33,10 +33,10 @@ export default function MobileToggleButton({ ? JSON.parse(decodeURIComponent(location)) : null - const nights = dt(d.to).diff(dt(d.from), "days") + const nights = dt(d.toDate).diff(dt(d.fromDate), "days") - const selectedFromDate = dt(d.from).locale(lang).format("D MMM") - const selectedToDate = dt(d.to).locale(lang).format("D MMM") + const selectedFromDate = dt(d.fromDate).locale(lang).format("D MMM") + const selectedToDate = dt(d.toDate).locale(lang).format("D MMM") useEffect(() => { setHasMounted(true) diff --git a/components/BookingWidget/bookingWidget.module.css b/components/BookingWidget/bookingWidget.module.css index 22ff009ee..d8235bae8 100644 --- a/components/BookingWidget/bookingWidget.module.css +++ b/components/BookingWidget/bookingWidget.module.css @@ -50,3 +50,9 @@ display: none; } } + +@media screen and (min-width: 1367px) { + .container { + z-index: 9; + } +} diff --git a/components/BookingWidget/index.tsx b/components/BookingWidget/index.tsx index f7f220a0b..c7674b9d7 100644 --- a/components/BookingWidget/index.tsx +++ b/components/BookingWidget/index.tsx @@ -8,12 +8,21 @@ export function preload() { void getLocations() } -export default async function BookingWidget({ type }: BookingWidgetProps) { +export default async function BookingWidget({ + type, + searchParams, +}: BookingWidgetProps) { const locations = await getLocations() if (!locations || "error" in locations) { return null } - return + return ( + + ) } diff --git a/components/Breadcrumbs/BreadcrumbsSkeleton.tsx b/components/Breadcrumbs/BreadcrumbsSkeleton.tsx new file mode 100644 index 000000000..6841c2dba --- /dev/null +++ b/components/Breadcrumbs/BreadcrumbsSkeleton.tsx @@ -0,0 +1,25 @@ +import { ChevronRightIcon, HouseIcon } from "@/components/Icons" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" + +import styles from "./breadcrumbs.module.css" + +export default function BreadcrumbsSkeleton() { + return ( + + ) +} diff --git a/components/ContentType/ContentPage/contentPage.module.css b/components/ContentType/ContentPage/contentPage.module.css index c0c94aa42..825a2445d 100644 --- a/components/ContentType/ContentPage/contentPage.module.css +++ b/components/ContentType/ContentPage/contentPage.module.css @@ -1,7 +1,5 @@ .contentPage { padding-bottom: var(--Spacing-x9); - container-name: content-page; - container-type: inline-size; } .header { @@ -35,12 +33,16 @@ .contentContainer { display: grid; - padding: var(--Spacing-x4) var(--Spacing-x2) 0; + grid-template-areas: + "main" + "sidebar"; gap: var(--Spacing-x4); align-items: start; + padding: var(--Spacing-x4) var(--Spacing-x2) 0; } .mainContent { + grid-area: main; display: grid; gap: var(--Spacing-x4); width: 100%; @@ -57,6 +59,7 @@ padding: var(--Spacing-x4) 0; } .contentContainer { + grid-template-areas: "main sidebar"; grid-template-columns: var(--max-width-text-block) 1fr; gap: var(--Spacing-x9); max-width: var(--max-width-content); diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx index c76b3f089..5ed761081 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx @@ -1,5 +1,6 @@ "use client" +import { useMap } from "@vis.gl/react-google-maps" import { useState } from "react" import { useIntl } from "react-intl" @@ -11,15 +12,19 @@ import Title from "@/components/TempDesignSystem/Text/Title" import styles from "./sidebar.module.css" import type { SidebarProps } from "@/types/components/hotelPage/map/sidebar" +import type { Coordinates } from "@/types/components/maps/coordinates" export default function Sidebar({ activePoi, hotelName, pointsOfInterest, onActivePoiChange, + coordinates, }: SidebarProps) { const intl = useIntl() + const map = useMap() const [isFullScreenSidebar, setIsFullScreenSidebar] = useState(false) + const [isClicking, setIsClicking] = useState(false) const poiGroups = new Set(pointsOfInterest.map(({ group }) => group)) const poisInGroups = Array.from(poiGroups).map((group) => ({ group, @@ -30,6 +35,48 @@ export default function Sidebar({ setIsFullScreenSidebar((prev) => !prev) } + function moveToPoi(poiCoordinates: Coordinates) { + if (map) { + const bounds = new google.maps.LatLngBounds() + const boundPadding = 0.02 + + const minLat = Math.min(coordinates.lat, poiCoordinates.lat) + const maxLat = Math.max(coordinates.lat, poiCoordinates.lat) + const minLng = Math.min(coordinates.lng, poiCoordinates.lng) + const maxLng = Math.max(coordinates.lng, poiCoordinates.lng) + + bounds.extend( + new google.maps.LatLng(minLat - boundPadding, minLng - boundPadding) + ) + bounds.extend( + new google.maps.LatLng(maxLat + boundPadding, maxLng + boundPadding) + ) + map.fitBounds(bounds) + } + } + + function handleMouseEnter(poiName: string) { + if (!isClicking) { + onActivePoiChange(poiName) + } + } + + function handleMouseLeave() { + if (!isClicking) { + onActivePoiChange(null) + } + } + + function handlePoiClick(poiName: string, poiCoordinates: Coordinates) { + setIsClicking(true) + toggleFullScreenSidebar() + onActivePoiChange(poiName) + moveToPoi(poiCoordinates) + setTimeout(() => { + setIsClicking(false) + }, 200) + } + return (