feat(SW-344): Hotel list in mobile
This commit is contained in:
@@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
|
|||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
import { getHotelPins } from "@/components/HotelReservation/HotelCardDialogListing/utils"
|
||||||
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
|
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
|
||||||
import {
|
import {
|
||||||
generateChildrenString,
|
generateChildrenString,
|
||||||
@@ -11,11 +12,7 @@ import {
|
|||||||
import { MapModal } from "@/components/MapModal"
|
import { MapModal } from "@/components/MapModal"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import {
|
import { fetchAvailableHotels, getCentralCoordinates } from "../../utils"
|
||||||
fetchAvailableHotels,
|
|
||||||
getCentralCoordinates,
|
|
||||||
getHotelPins,
|
|
||||||
} from "../../utils"
|
|
||||||
|
|
||||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|||||||
@@ -88,25 +88,6 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHotelPins(hotels: HotelData[]): HotelPin[] {
|
|
||||||
return hotels.map((hotel) => ({
|
|
||||||
coordinates: {
|
|
||||||
lat: hotel.hotelData.location.latitude,
|
|
||||||
lng: hotel.hotelData.location.longitude,
|
|
||||||
},
|
|
||||||
name: hotel.hotelData.name,
|
|
||||||
publicPrice: hotel.price?.regularAmount ?? null,
|
|
||||||
memberPrice: hotel.price?.memberAmount ?? null,
|
|
||||||
currency: hotel.price?.currency || null,
|
|
||||||
images: [
|
|
||||||
hotel.hotelData.hotelContent.images,
|
|
||||||
...(hotel.hotelData.gallery?.heroImages ?? []),
|
|
||||||
],
|
|
||||||
amenities: hotel.hotelData.detailedFacilities.slice(0, 3),
|
|
||||||
ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCentralCoordinates(hotels: HotelPin[]) {
|
export function getCentralCoordinates(hotels: HotelPin[]) {
|
||||||
const centralCoordinates = hotels.reduce(
|
const centralCoordinates = hotels.reduce(
|
||||||
(acc, pin) => {
|
(acc, pin) => {
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import styles from "./hotelCardDialog.module.css"
|
|||||||
import type { HotelCardDialogProps } from "@/types/components/hotelReservation/selectHotel/map"
|
import type { HotelCardDialogProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
|
|
||||||
export default function HotelCardDialog({
|
export default function HotelCardDialog({
|
||||||
pin,
|
data,
|
||||||
isOpen,
|
isOpen,
|
||||||
handleClose,
|
handleClose,
|
||||||
}: HotelCardDialogProps) {
|
}: HotelCardDialogProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
if (!pin) {
|
if (!data) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export default function HotelCardDialog({
|
|||||||
amenities,
|
amenities,
|
||||||
images,
|
images,
|
||||||
ratings,
|
ratings,
|
||||||
} = pin
|
} = data
|
||||||
|
|
||||||
const firstImage = images[0]?.imageSizes?.small
|
const firstImage = images[0]?.imageSizes?.small
|
||||||
const altText = images[0]?.metaData?.altText
|
const altText = images[0]?.metaData?.altText
|
||||||
|
|||||||
47
components/HotelReservation/HotelCardDialogListing/index.tsx
Normal file
47
components/HotelReservation/HotelCardDialogListing/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react"
|
||||||
|
|
||||||
|
import HotelCardDialog from "../HotelCardDialog"
|
||||||
|
import { getHotelPins } from "./utils"
|
||||||
|
|
||||||
|
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
|
|
||||||
|
export default function HotelCardDialogListing({
|
||||||
|
hotels,
|
||||||
|
activeCard,
|
||||||
|
}: {
|
||||||
|
hotels: HotelData[]
|
||||||
|
activeCard: string | null | undefined
|
||||||
|
}) {
|
||||||
|
const hotelsPinData = getHotelPins(hotels)
|
||||||
|
const activeCardRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeCardRef.current) {
|
||||||
|
activeCardRef.current.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "nearest",
|
||||||
|
inline: "center",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [activeCard])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{hotelsPinData?.length &&
|
||||||
|
hotelsPinData.map((data) => {
|
||||||
|
const isActive = data.name === activeCard
|
||||||
|
return (
|
||||||
|
<div key={data.name} ref={isActive ? activeCardRef : null}>
|
||||||
|
<HotelCardDialog
|
||||||
|
data={data}
|
||||||
|
isOpen={!!activeCard}
|
||||||
|
handleClose={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
components/HotelReservation/HotelCardDialogListing/utils.ts
Normal file
21
components/HotelReservation/HotelCardDialogListing/utils.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
|
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
|
|
||||||
|
export function getHotelPins(hotels: HotelData[]): HotelPin[] {
|
||||||
|
return hotels.map((hotel) => ({
|
||||||
|
coordinates: {
|
||||||
|
lat: hotel.hotelData.location.latitude,
|
||||||
|
lng: hotel.hotelData.location.longitude,
|
||||||
|
},
|
||||||
|
name: hotel.hotelData.name,
|
||||||
|
publicPrice: hotel.price?.regularAmount ?? null,
|
||||||
|
memberPrice: hotel.price?.memberAmount ?? null,
|
||||||
|
currency: hotel.price?.currency || null,
|
||||||
|
images: [
|
||||||
|
hotel.hotelData.hotelContent.images,
|
||||||
|
...(hotel.hotelData.gallery?.heroImages ?? []),
|
||||||
|
],
|
||||||
|
amenities: hotel.hotelData.detailedFacilities.slice(0, 3),
|
||||||
|
ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -2,6 +2,37 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hotelListingMobile {
|
||||||
|
display: none;
|
||||||
|
align-items: flex-end;
|
||||||
|
overflow-x: auto;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
height: 350px;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotelListingMobile[data-open="true"] {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotelListingMobile dialog {
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotelListingMobile > div:first-child {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotelListingMobile > div:last-child {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.hotelListing {
|
.hotelListing {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -9,4 +40,9 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-top: var(--Spacing-x2);
|
padding-top: var(--Spacing-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hotelListingMobile,
|
||||||
|
.hotelListingMobile[data-open="true"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import HotelCardDialogListing from "@/components/HotelReservation/HotelCardDialogListing"
|
||||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||||
|
|
||||||
import styles from "./hotelListing.module.css"
|
import styles from "./hotelListing.module.css"
|
||||||
@@ -13,13 +14,18 @@ export default function HotelListing({
|
|||||||
onHotelCardHover,
|
onHotelCardHover,
|
||||||
}: HotelListingProps) {
|
}: HotelListingProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.hotelListing}>
|
<>
|
||||||
<HotelCardListing
|
<div className={styles.hotelListing}>
|
||||||
hotelData={hotels}
|
<HotelCardListing
|
||||||
type={HotelCardListingTypeEnum.MapListing}
|
hotelData={hotels}
|
||||||
activeCard={activeHotelPin}
|
type={HotelCardListingTypeEnum.MapListing}
|
||||||
onHotelCardHover={onHotelCardHover}
|
activeCard={activeHotelPin}
|
||||||
/>
|
onHotelCardHover={onHotelCardHover}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.hotelListingMobile} data-open={!!activeHotelPin}>
|
||||||
|
<HotelCardDialogListing hotels={hotels} activeCard={activeHotelPin} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
min-width: 109px !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */
|
min-width: 109px !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialogContainer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.pin {
|
.pin {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -67,3 +71,9 @@
|
|||||||
.card.active {
|
.card.active {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.dialogContainer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,17 +50,18 @@ export default function HotelListingMapContent({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HotelCardDialog
|
<div className={styles.dialogContainer}>
|
||||||
isOpen={isActiveOrHovered}
|
<HotelCardDialog
|
||||||
handleClose={(event: { stopPropagation: () => void }) => {
|
isOpen={isActiveOrHovered}
|
||||||
event.stopPropagation()
|
handleClose={(event: { stopPropagation: () => void }) => {
|
||||||
if (activeHotelPin === pin.name) {
|
event.stopPropagation()
|
||||||
toggleActiveHotelPin(null)
|
if (activeHotelPin === pin.name) {
|
||||||
}
|
toggleActiveHotelPin(null)
|
||||||
}}
|
}
|
||||||
pin={pin}
|
}}
|
||||||
/>
|
data={pin}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`${styles.pin} ${isActiveOrHovered ? styles.active : ""}`}
|
className={`${styles.pin} ${isActiveOrHovered ? styles.active : ""}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -50,6 +50,6 @@ export interface HotelListingMapContentProps {
|
|||||||
|
|
||||||
export interface HotelCardDialogProps {
|
export interface HotelCardDialogProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
pin: HotelPin
|
data: HotelPin
|
||||||
handleClose: (event: { stopPropagation: () => void }) => void
|
handleClose: (event: { stopPropagation: () => void }) => void
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user