diff --git a/.env.test b/.env.test index 0355b15cc..a353c6d88 100644 --- a/.env.test +++ b/.env.test @@ -7,6 +7,7 @@ CMS_API_KEY="test" CMS_PREVIEW_TOKEN="test" CMS_PREVIEW_URL="test" CMS_URL="test" +CMS_BRANCH="development" CURITY_CLIENT_ID_SERVICE="test" CURITY_CLIENT_SECRET_SERVICE="test" CURITY_CLIENT_ID_USER="test" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx deleted file mode 100644 index 8f6f8657c..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import LoadingSpinner from "@/components/LoadingSpinner" - -export default function LoadingModal() { - return -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx deleted file mode 100644 index 600afbb38..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { notFound } from "next/navigation" - -import { env } from "@/env/server" -import { getCityCoordinates, getLocations } from "@/lib/trpc/memoizedRequests" - -import { getHotelPins } from "@/components/HotelReservation/HotelCardDialogListing/utils" -import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap" -import { - generateChildrenString, - getHotelReservationQueryParams, -} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" -import { MapModal } from "@/components/MapModal" -import { setLang } from "@/i18n/serverContext" - -import { fetchAvailableHotels, getFiltersFromHotels } from "../../utils" - -import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" -import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" -import type { LangParams, PageArgs } from "@/types/params" - -export default async function SelectHotelMapPage({ - params, - 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 googleMapId = env.GOOGLE_DYNAMIC_MAP_ID - const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY - - 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 - ? generateChildrenString(selectHotelParamsObject.room[0].child) - : undefined // TODO: Handle multiple rooms - - const hotels = await fetchAvailableHotels({ - cityId: city.id, - roomStayStartDate: searchParams.fromDate, - roomStayEndDate: searchParams.toDate, - adults, - children, - }) - - const validHotels = hotels.filter( - (hotel): hotel is HotelData => hotel !== null - ) - - const hotelPins = getHotelPins(validHotels) - const filterList = getFiltersFromHotels(validHotels) - const cityCoordinates = await getCityCoordinates({ - city: city.name, - hotel: { address: hotels?.[0]?.hotelData?.address.streetAddress }, - }) - - return ( - - - - ) -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx deleted file mode 100644 index 86b9e9a38..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Default() { - return null -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css index b86e58a72..f955dbffc 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css @@ -3,3 +3,9 @@ background-color: var(--Base-Background-Primary-Normal); position: relative; } + +@media screen and (min-width: 768px) { + .layout { + z-index: 0; + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx index 907b02c2a..ee96f3c10 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx @@ -4,14 +4,6 @@ import { LangParams, LayoutArgs } from "@/types/params" export default function HotelReservationLayout({ children, - modal, -}: React.PropsWithChildren< - LayoutArgs & { modal: React.ReactNode } ->) { - return ( -
- {children} - {modal} -
- ) +}: React.PropsWithChildren>) { + return
{children}
} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/loading.tsx new file mode 100644 index 000000000..fefdc7682 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/loading.tsx @@ -0,0 +1,5 @@ +import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton" + +export default function Loading() { + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx index bfd164880..47af6c424 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx @@ -1 +1,56 @@ -export { default } from "../@modal/(.)map/page" +import { notFound } from "next/navigation" +import { Suspense } from "react" + +import { getLocations } from "@/lib/trpc/memoizedRequests" + +import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer" +import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton" +import { + generateChildrenString, + getHotelReservationQueryParams, +} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { MapContainer } from "@/components/MapContainer" +import { setLang } from "@/i18n/serverContext" + +import styles from "./page.module.css" + +import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" +import type { LangParams, PageArgs } from "@/types/params" + +export default async function SelectHotelMapPage({ + params, + 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 selectHotelParams = new URLSearchParams(searchParams) + const selectHotelParamsObject = + getHotelReservationQueryParams(selectHotelParams) + const adultsInRoom = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms + const childrenInRoom = selectHotelParamsObject.room[0].child + ? generateChildrenString(selectHotelParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms + + return ( +
+ + + +
+ ) +} diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index 353273141..0631bc612 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -3,7 +3,6 @@ import "@scandic-hotels/design-system/style.css" import Script from "next/script" -import { env } from "@/env/server" import TrpcProvider from "@/lib/trpc/Provider" import TokenRefresher from "@/components/Auth/TokenRefresher" diff --git a/components/Forms/BookingWidget/FormContent/Search/SearchList/List/index.tsx b/components/Forms/BookingWidget/FormContent/Search/SearchList/List/index.tsx index 946fb2e28..4ed6998bb 100644 --- a/components/Forms/BookingWidget/FormContent/Search/SearchList/List/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/SearchList/List/index.tsx @@ -23,7 +23,7 @@ export default function List({ getItemProps={getItemProps} highlightedIndex={highlightedIndex} index={initialIndex + index} - key={location.id} + key={location.id + index} location={location} /> ))} diff --git a/components/Forms/BookingWidget/FormContent/Search/index.tsx b/components/Forms/BookingWidget/FormContent/Search/index.tsx index 1fe45ed04..93d99a89b 100644 --- a/components/Forms/BookingWidget/FormContent/Search/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/index.tsx @@ -26,6 +26,7 @@ import type { SearchProps } from "@/types/components/search" import type { Location } from "@/types/trpc/routers/hotel/locations" const name = "search" + export default function Search({ locations }: SearchProps) { const { register, setValue, trigger, unregister } = useFormContext() @@ -166,7 +167,6 @@ export default function Search({ locations }: SearchProps) { onInputValueChange={(inputValue) => dispatchInputValue(inputValue)} > {({ - closeMenu, getInputProps, getItemProps, getLabelProps, @@ -207,9 +207,6 @@ export default function Search({ locations }: SearchProps) { id: "Destinations & hotels", }), ...register(name, { - onBlur: function () { - closeMenu() - }, onChange: handleOnChange, }), type: "search", diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 5c2f788ee..471a0e264 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -4,7 +4,8 @@ import { memo, useCallback } from "react" import { useIntl } from "react-intl" import { Lang } from "@/constants/languages" -import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation" +import { selectRate } from "@/constants/routes/hotelReservation" +import { useHotelsMapStore } from "@/stores/hotels-map" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import ImageGallery from "@/components/ImageGallery" @@ -32,26 +33,27 @@ function HotelCard({ hotel, type = HotelCardListingTypeEnum.PageListing, state = "default", - onHotelCardHover, }: HotelCardProps) { const params = useParams() const lang = params.lang as Lang const intl = useIntl() + const { setActiveHotelPin, setActiveHotelCard } = useHotelsMapStore() const { hotelData } = hotel const { price } = hotel const handleMouseEnter = useCallback(() => { - if (onHotelCardHover && hotelData) { - onHotelCardHover(hotelData.name) + if (hotelData) { + setActiveHotelPin(hotelData.name) } - }, [onHotelCardHover, hotelData]) + }, [setActiveHotelPin, hotelData]) const handleMouseLeave = useCallback(() => { - if (onHotelCardHover) { - onHotelCardHover(null) + if (hotelData) { + setActiveHotelPin(null) + setActiveHotelCard(null) } - }, [onHotelCardHover]) + }, [setActiveHotelPin, hotelData, setActiveHotelCard]) if (!hotel || !hotelData) return null @@ -96,15 +98,21 @@ function HotelCard({ {hotelData.address.streetAddress}, {hotelData.address.city} - - + + {hotelData.address.streetAddress}, {hotelData.address.city} - - + +
diff --git a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css index 7d607d3ad..30cd03385 100644 --- a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css +++ b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css @@ -106,8 +106,7 @@ } @media (min-width: 768px) { - .facilities, - .memberPrice { + .facilities { display: none; } .dialog { diff --git a/components/HotelReservation/HotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/index.tsx index e6a5e2f02..7269ee7cc 100644 --- a/components/HotelReservation/HotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/index.tsx @@ -103,12 +103,14 @@ export default function HotelCardDialog({ {intl.formatMessage({ id: "From" })} - - {publicPrice} {currency} - - /{intl.formatMessage({ id: "night" })} - - + {publicPrice && ( + + {publicPrice} {currency} + + /{intl.formatMessage({ id: "night" })} + + + )} {memberPrice && ( (null) const observerRef = useRef(null) const dialogRef = useRef(null) const isMobile = useMediaQuery("(max-width: 768px)") + const { activeHotelCard, setActiveHotelCard, setActiveHotelPin } = + useHotelsMapStore() - useClickOutside(dialogRef, !!activeCard && isMobile, () => { - onActiveCardChange(null) - }) + function handleClose() { + setActiveHotelCard(null) + setActiveHotelPin(null) + } + + useClickOutside(dialogRef, !!activeHotelCard && isMobile, handleClose) const handleIntersection = useCallback( (entries: IntersectionObserverEntry[]) => { @@ -33,12 +38,12 @@ export default function HotelCardDialogListing({ if (entry.isIntersecting) { const cardName = entry.target.getAttribute("data-name") if (cardName) { - onActiveCardChange(cardName) + setActiveHotelCard(cardName) } } }) }, - [onActiveCardChange] + [setActiveHotelCard] ) useEffect(() => { @@ -73,13 +78,13 @@ export default function HotelCardDialogListing({ elements.forEach((el) => observerRef.current?.observe(el)) }, 1000) } - }, [activeCard]) + }, [activeHotelCard]) return (
{!!hotelsPinData?.length && hotelsPinData.map((data) => { - const isActive = data.name === activeCard + const isActive = data.name === activeHotelCard return (
onActiveCardChange(null)} + isOpen={!!activeHotelCard} + handleClose={handleClose} />
) diff --git a/components/HotelReservation/HotelCardDialogListing/utils.ts b/components/HotelReservation/HotelCardDialogListing/utils.ts index 1a0e05ad8..fb658adc0 100644 --- a/components/HotelReservation/HotelCardDialogListing/utils.ts +++ b/components/HotelReservation/HotelCardDialogListing/utils.ts @@ -12,7 +12,10 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] { name: hotel.hotelData.name, publicPrice: hotel.price?.public?.localPrice.pricePerNight ?? null, memberPrice: hotel.price?.member?.localPrice.pricePerNight ?? null, - currency: hotel.price?.public?.localPrice.currency || null, + currency: + hotel.price?.public?.localPrice.currency || + hotel.price?.member?.localPrice.currency || + null, images: [ hotel.hotelData.hotelContent.images, ...(hotel.hotelData.gallery?.heroImages ?? []), @@ -25,5 +28,8 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] { .slice(0, 3), ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null, operaId: hotel.hotelData.operaId, + facilityIds: hotel.hotelData.detailedFacilities.map( + (facility) => facility.id + ), })) } diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index d9d0b4f30..c7687362b 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react" import { useIntl } from "react-intl" import { useHotelFilterStore } from "@/stores/hotel-filters" +import { useHotelsMapStore } from "@/stores/hotels-map" import Alert from "@/components/TempDesignSystem/Alert" import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" @@ -24,14 +25,13 @@ import { AlertTypeEnum } from "@/types/enums/alert" export default function HotelCardListing({ hotelData, type = HotelCardListingTypeEnum.PageListing, - activeCard, - onHotelCardHover, }: HotelCardListingProps) { const searchParams = useSearchParams() const activeFilters = useHotelFilterStore((state) => state.activeFilters) const setResultCount = useHotelFilterStore((state) => state.setResultCount) const [showBackToTop, setShowBackToTop] = useState(false) const intl = useIntl() + const { activeHotelCard } = useHotelsMapStore() const sortBy = useMemo( () => searchParams.get("sort") ?? DEFAULT_SORT, @@ -111,13 +111,16 @@ export default function HotelCardListing({ hotels.map((hotel) => (
)) @@ -128,7 +131,9 @@ export default function HotelCardListing({ text={intl.formatMessage({ id: "filters.nohotel.text" })} /> ) : null} - {showBackToTop && } + {showBackToTop && ( + + )} ) } diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx index 8a68a8105..f1d3d9751 100644 --- a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx @@ -1,5 +1,10 @@ "use client" +import { + usePathname, + useSearchParams, +} from "next/dist/client/components/navigation" +import { useCallback, useState } from "react" import { Dialog as AriaDialog, DialogTrigger, @@ -13,25 +18,69 @@ import { useHotelFilterStore } from "@/stores/hotel-filters" import { CloseLargeIcon, FilterIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" +import Select from "@/components/TempDesignSystem/Select" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useInitializeFiltersFromUrl from "@/hooks/useInitializeFiltersFromUrl" import HotelFilter from "../HotelFilter" -import HotelSorter from "../HotelSorter" +import { DEFAULT_SORT } from "../HotelSorter" import styles from "./filterAndSortModal.module.css" import type { FilterAndSortModalProps } from "@/types/components/hotelReservation/selectHotel/filterAndSortModal" +import { + type SortItem, + SortOrder, +} from "@/types/components/hotelReservation/selectHotel/hotelSorter" export default function FilterAndSortModal({ filters, }: FilterAndSortModalProps) { const intl = useIntl() useInitializeFiltersFromUrl() + const searchParams = useSearchParams() + const pathname = usePathname() const resultCount = useHotelFilterStore((state) => state.resultCount) const setFilters = useHotelFilterStore((state) => state.setFilters) const activeFilters = useHotelFilterStore((state) => state.activeFilters) + const [sort, setSort] = useState(searchParams.get("sort") ?? DEFAULT_SORT) + + const sortItems: SortItem[] = [ + { + label: intl.formatMessage({ id: "Distance to city centre" }), + value: SortOrder.Distance, + }, + { label: intl.formatMessage({ id: "Name" }), value: SortOrder.Name }, + { label: intl.formatMessage({ id: "Price" }), value: SortOrder.Price }, + { + label: intl.formatMessage({ id: "TripAdvisor rating" }), + value: SortOrder.TripAdvisorRating, + }, + ] + + const handleSortSelect = useCallback((value: string | number) => { + setSort(value.toString()) + }, []) + + const handleApplyFiltersAndSorting = useCallback( + (close: () => void) => { + if (sort === searchParams.get("sort")) { + close() + } + + const newSearchParams = new URLSearchParams(searchParams) + newSearchParams.set("sort", sort) + + window.history.replaceState( + null, + "", + `${pathname}?${newSearchParams.toString()}` + ) + close() + }, + [pathname, searchParams, sort] + ) return ( <> @@ -65,7 +114,16 @@ export default function FilterAndSortModal({
- +