Merged in feat/SW-1451-country-page-sorting (pull request #1426)
Feat/SW-1451 country page filtering and sorting * feat(SW-1451): implemented sorting and filtering on country pages * feat(SW-1451): Renamed hotel-data to destination-data because of its multi-purpose use * feat(SW-1451): Now filtering after change of url instead of inside the store after submit Approved-by: Fredrik Thorsson
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
getDestinationCityPagesByCountry,
|
||||
getHotelsByCountry,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
import DestinationDataProvider from "@/providers/DestinationDataProvider"
|
||||
|
||||
import type { SortItem } from "@/types/components/destinationFilterAndSort"
|
||||
import type { Country } from "@/types/enums/country"
|
||||
import { SortOption } from "@/types/enums/destinationFilterAndSort"
|
||||
|
||||
interface CityDataContainerProps extends React.PropsWithChildren {
|
||||
country: Country
|
||||
}
|
||||
|
||||
export function preload(country: Country) {
|
||||
void getHotelsByCountry(country)
|
||||
void getDestinationCityPagesByCountry(country)
|
||||
}
|
||||
|
||||
export default async function CityDataContainer({
|
||||
country,
|
||||
children,
|
||||
}: CityDataContainerProps) {
|
||||
const intl = await getIntl()
|
||||
const [hotels, cities] = await Promise.all([
|
||||
getHotelsByCountry(country),
|
||||
getDestinationCityPagesByCountry(country),
|
||||
])
|
||||
|
||||
const sortItems: SortItem[] = [
|
||||
{
|
||||
label: intl.formatMessage({ id: "Recommended" }),
|
||||
value: SortOption.Recommended,
|
||||
isDefault: true,
|
||||
},
|
||||
{ label: intl.formatMessage({ id: "Name" }), value: SortOption.Name },
|
||||
]
|
||||
|
||||
return (
|
||||
<DestinationDataProvider
|
||||
allHotels={hotels}
|
||||
allCities={cities}
|
||||
sortItems={sortItems}
|
||||
>
|
||||
{children}
|
||||
</DestinationDataProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
@@ -5,7 +7,7 @@ import ExperienceListSkeleton from "../../ExperienceList/ExperienceListSkeleton"
|
||||
|
||||
import styles from "./cityListingItem.module.css"
|
||||
|
||||
export default async function CityListingItemSkeleton() {
|
||||
export default function CityListingItemSkeleton() {
|
||||
return (
|
||||
<article className={styles.container}>
|
||||
<div className={styles.imageWrapper}>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { mapImageVaultImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import ExperienceList from "../../ExperienceList"
|
||||
@@ -18,8 +20,8 @@ interface CityListingItemProps {
|
||||
city: DestinationCityListItem
|
||||
}
|
||||
|
||||
export default async function CityListingItem({ city }: CityListingItemProps) {
|
||||
const intl = await getIntl()
|
||||
export default function CityListingItem({ city }: CityListingItemProps) {
|
||||
const intl = useIntl()
|
||||
const galleryImages = mapImageVaultImagesToGalleryImages(city.images)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
"use client"
|
||||
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import CityListingItemSkeleton from "./CityListingItem/CityListingItemSkeleton"
|
||||
|
||||
import styles from "./cityListing.module.css"
|
||||
|
||||
export default async function CityListingSkeleton() {
|
||||
export default function CityListingSkeleton() {
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<div>
|
||||
<SkeletonShimmer height="30px" width="300px" />
|
||||
<div className={styles.listHeader}>
|
||||
<SkeletonShimmer height="30px" width="200px" />
|
||||
<SkeletonShimmer height="30px" width="120px" />
|
||||
</div>
|
||||
<ul className={styles.cityList}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
.container {
|
||||
--scroll-margin-top: calc(
|
||||
var(--booking-widget-mobile-height) + var(--Spacing-x2)
|
||||
);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
scroll-margin-top: var(--scroll-margin-top);
|
||||
}
|
||||
|
||||
.listHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cityList {
|
||||
@@ -8,3 +17,15 @@
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.cityList:not(.allVisible) li:nth-child(n + 6) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
--scroll-margin-top: calc(
|
||||
var(--booking-widget-desktop-height) + var(--Spacing-x2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,83 @@
|
||||
import { getDestinationCityPagesByCountry } from "@/lib/trpc/memoizedRequests"
|
||||
"use client"
|
||||
|
||||
import { useRef, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||
|
||||
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
|
||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||
|
||||
import CityListingItem from "./CityListingItem"
|
||||
import CityListingSkeleton from "./CityListingSkeleton"
|
||||
|
||||
import styles from "./cityListing.module.css"
|
||||
|
||||
import type { Country } from "@/types/enums/country"
|
||||
export default function CityListing() {
|
||||
const intl = useIntl()
|
||||
const scrollRef = useRef<HTMLElement>(null)
|
||||
const { showBackToTop, scrollToTop } = useScrollToTop({
|
||||
threshold: 300,
|
||||
elementRef: scrollRef,
|
||||
})
|
||||
const { activeCities, filters, sortItems, isLoading } =
|
||||
useDestinationDataStore((state) => ({
|
||||
activeCities: state.activeCities,
|
||||
filters: state.allFilters,
|
||||
sortItems: state.sortItems,
|
||||
isLoading: state.isLoading,
|
||||
}))
|
||||
const [allCitiesVisible, setAllCitiesVisible] = useState(
|
||||
activeCities.length <= 5
|
||||
)
|
||||
|
||||
interface CityListingProps {
|
||||
country: Country
|
||||
}
|
||||
|
||||
export default async function CityListing({ country }: CityListingProps) {
|
||||
const intl = await getIntl()
|
||||
const cities = await getDestinationCityPagesByCountry(country)
|
||||
|
||||
if (!cities.length) {
|
||||
return null
|
||||
function handleShowMore() {
|
||||
if (scrollRef.current && allCitiesVisible) {
|
||||
scrollRef.current.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
setAllCitiesVisible((state) => !state)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<div>
|
||||
return isLoading ? (
|
||||
<CityListingSkeleton />
|
||||
) : (
|
||||
<section className={styles.container} ref={scrollRef}>
|
||||
<div className={styles.listHeader}>
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: `{count, plural, one {{count} Location} other {{count} Locations}}`,
|
||||
},
|
||||
{ count: cities.length }
|
||||
{ count: activeCities.length }
|
||||
)}
|
||||
</Subtitle>
|
||||
<DestinationFilterAndSort
|
||||
filters={filters}
|
||||
sortItems={sortItems}
|
||||
listType="city"
|
||||
/>
|
||||
</div>
|
||||
<ul className={styles.cityList}>
|
||||
{cities.map((city) => (
|
||||
<ul
|
||||
className={`${styles.cityList} ${allCitiesVisible ? styles.allVisible : ""}`}
|
||||
>
|
||||
{activeCities.map((city) => (
|
||||
<li key={city.system.uid}>
|
||||
<CityListingItem city={city} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{activeCities.length > 5 ? (
|
||||
<ShowMoreButton
|
||||
loadMoreData={handleShowMore}
|
||||
showLess={allCitiesVisible}
|
||||
/>
|
||||
) : null}
|
||||
{showBackToTop && (
|
||||
<BackToTopButton position="center" onClick={scrollToTop} />
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client"
|
||||
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import HotelListItemSkeleton from "../HotelListItem/HotelListItemSkeleton"
|
||||
|
||||
import styles from "./hotelList.module.css"
|
||||
|
||||
export default function HotelListSkeleton() {
|
||||
return (
|
||||
<div className={styles.hotelListWrapper}>
|
||||
<div className={styles.header}>
|
||||
<SkeletonShimmer height="30px" width="200px" />
|
||||
<SkeletonShimmer height="30px" width="120px" />
|
||||
</div>
|
||||
<ul className={styles.hotelList}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<HotelListItemSkeleton key={index} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,13 +4,14 @@ import { useMap, useMapsLibrary } from "@vis.gl/react-google-maps"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useHotelDataStore } from "@/stores/hotel-data"
|
||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||
|
||||
import HotelFilterAndSort from "@/components/HotelFilterAndSort"
|
||||
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import { debounce } from "@/utils/debounce"
|
||||
|
||||
import HotelListItem from "../HotelListItem"
|
||||
import HotelListSkeleton from "./HotelListSkeleton"
|
||||
import { getVisibleHotels } from "./utils"
|
||||
|
||||
import styles from "./hotelList.module.css"
|
||||
@@ -22,11 +23,13 @@ export default function HotelList() {
|
||||
const map = useMap()
|
||||
const coreLib = useMapsLibrary("core")
|
||||
const [visibleHotels, setVisibleHotels] = useState<HotelDataWithUrl[]>([])
|
||||
const { filters, sortItems, activeHotels } = useHotelDataStore((state) => ({
|
||||
filters: state.allFilters,
|
||||
sortItems: state.sortItems,
|
||||
activeHotels: state.activeHotels,
|
||||
}))
|
||||
const { filters, sortItems, activeHotels, isLoading } =
|
||||
useDestinationDataStore((state) => ({
|
||||
filters: state.allFilters,
|
||||
sortItems: state.sortItems,
|
||||
activeHotels: state.activeHotels,
|
||||
isLoading: state.isLoading,
|
||||
}))
|
||||
|
||||
const debouncedUpdateVisibleHotels = useMemo(
|
||||
() =>
|
||||
@@ -51,7 +54,9 @@ export default function HotelList() {
|
||||
}
|
||||
}, [map, coreLib, debouncedUpdateVisibleHotels])
|
||||
|
||||
return (
|
||||
return isLoading ? (
|
||||
<HotelListSkeleton />
|
||||
) : (
|
||||
<div className={styles.hotelListWrapper}>
|
||||
<div className={styles.header}>
|
||||
<Body>
|
||||
@@ -60,7 +65,11 @@ export default function HotelList() {
|
||||
{ count: visibleHotels.length }
|
||||
)}
|
||||
</Body>
|
||||
<HotelFilterAndSort filters={filters} sortItems={sortItems} />
|
||||
<DestinationFilterAndSort
|
||||
filters={filters}
|
||||
sortItems={sortItems}
|
||||
listType="hotel"
|
||||
/>
|
||||
</div>
|
||||
<ul className={styles.hotelList}>
|
||||
{visibleHotels.map(({ hotel, url }) => (
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import styles from "./hotelListItem.module.css"
|
||||
|
||||
export default function HotelListItemSkeleton() {
|
||||
return (
|
||||
<article className={styles.hotelListItem}>
|
||||
<div className={styles.imageWrapper}>
|
||||
<SkeletonShimmer width="100%" height="100%" />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.intro}>
|
||||
<div className={styles.logo}>
|
||||
<SkeletonShimmer height="24px" width="100px" />
|
||||
</div>
|
||||
<SkeletonShimmer height="35px" width="300px" />
|
||||
<div className={styles.captions}>
|
||||
<SkeletonShimmer height="20px" width="130px" />
|
||||
<SkeletonShimmer height="20px" width="180px" />
|
||||
</div>
|
||||
</div>
|
||||
<ul className={styles.amenityList}>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<li className={styles.amenityItem} key={index}>
|
||||
<SkeletonShimmer height="20px" width="40px" />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.ctaWrapper}>
|
||||
<SkeletonShimmer height="45px" width="100%" />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -11,8 +11,25 @@
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
align-content: start;
|
||||
justify-items: start;
|
||||
align-content: flex-start;
|
||||
justify-items: flex-start;
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.tripAdvisor {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x-half);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
padding: var(--Spacing-x-quarter) var(--Spacing-x1);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
}
|
||||
|
||||
.intro {
|
||||
@@ -48,7 +65,11 @@
|
||||
.hotelListItem {
|
||||
width: 360px;
|
||||
min-height: 150px;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-template-columns: 160px 1fr;
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -4,6 +4,7 @@ import Link from "next/link"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import { TripAdvisorIcon } from "@/components/Icons"
|
||||
import HotelLogo from "@/components/Icons/Logos"
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
@@ -29,13 +30,25 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) {
|
||||
|
||||
return (
|
||||
<article className={styles.hotelListItem}>
|
||||
<ImageGallery
|
||||
images={galleryImages}
|
||||
title={intl.formatMessage(
|
||||
{ id: "{title} - Image gallery" },
|
||||
{ title: hotel.name }
|
||||
<div className={styles.imageWrapper}>
|
||||
<ImageGallery
|
||||
images={galleryImages}
|
||||
fill
|
||||
sizes="(min-width: 768px) 350px, 100vw"
|
||||
title={intl.formatMessage(
|
||||
{ id: "{title} - Image gallery" },
|
||||
{ title: hotel.name }
|
||||
)}
|
||||
/>
|
||||
{hotel.ratings?.tripAdvisor.rating && (
|
||||
<div className={styles.tripAdvisor}>
|
||||
<TripAdvisorIcon color="burgundy" />
|
||||
<Caption color="burgundy">
|
||||
{hotel.ratings.tripAdvisor.rating}
|
||||
</Caption>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.intro}>
|
||||
<div className={styles.logo}>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useHotelDataStore } from "@/stores/hotel-data"
|
||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import Map from "../../Map"
|
||||
import { getCityHeadingText } from "../../utils"
|
||||
import { getHeadingText } from "../../utils"
|
||||
import HotelList from "./HotelList"
|
||||
|
||||
import styles from "./cityMap.module.css"
|
||||
@@ -21,7 +21,7 @@ interface CityMapProps {
|
||||
|
||||
export default function CityMap({ mapId, apiKey, city }: CityMapProps) {
|
||||
const intl = useIntl()
|
||||
const { activeHotels, allFilters, activeFilters } = useHotelDataStore(
|
||||
const { activeHotels, allFilters, activeFilters } = useDestinationDataStore(
|
||||
(state) => ({
|
||||
activeHotels: state.activeHotels,
|
||||
allFilters: state.allFilters,
|
||||
@@ -37,7 +37,7 @@ export default function CityMap({ mapId, apiKey, city }: CityMapProps) {
|
||||
textTransform="regular"
|
||||
className={styles.title}
|
||||
>
|
||||
{getCityHeadingText(intl, city.name, allFilters, activeFilters[0])}
|
||||
{getHeadingText(intl, city.name, allFilters, activeFilters[0])}
|
||||
</Title>
|
||||
<HotelList />
|
||||
</Map>
|
||||
|
||||
@@ -23,13 +23,7 @@ import styles from "./destinationCityPage.module.css"
|
||||
|
||||
import { PageContentTypeEnum } from "@/types/requests/contentType"
|
||||
|
||||
interface DestinationCityPageProps {
|
||||
filterFromUrl: string | undefined
|
||||
}
|
||||
|
||||
export default async function DestinationCityPage({
|
||||
filterFromUrl,
|
||||
}: DestinationCityPageProps) {
|
||||
export default async function DestinationCityPage() {
|
||||
const pageData = await getDestinationCityPage()
|
||||
|
||||
if (!pageData) {
|
||||
@@ -53,10 +47,7 @@ export default async function DestinationCityPage({
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={<DestinationCityPageSkeleton />}>
|
||||
<HotelDataContainer
|
||||
cityIdentifier={cityIdentifier}
|
||||
filterFromUrl={filterFromUrl}
|
||||
>
|
||||
<HotelDataContainer cityIdentifier={cityIdentifier}>
|
||||
<div className={styles.pageContainer}>
|
||||
<header className={styles.header}>
|
||||
<Breadcrumbs variant={PageContentTypeEnum.destinationCityPage} />
|
||||
@@ -67,7 +58,7 @@ export default async function DestinationCityPage({
|
||||
{blocks && <Blocks blocks={blocks} />}
|
||||
</main>
|
||||
<aside className={styles.sidebar}>
|
||||
<SidebarContentWrapper cityName={city.name}>
|
||||
<SidebarContentWrapper location={city.name}>
|
||||
<Body color="uiTextMediumContrast">{preamble}</Body>
|
||||
<ExperienceList experiences={experiences} />
|
||||
{has_sidepeek && (
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client"
|
||||
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import CityListItemSkeleton from "../CityListItem/CityListItemSkeleton"
|
||||
|
||||
import styles from "./cityList.module.css"
|
||||
|
||||
export default function CityListSkeleton() {
|
||||
return (
|
||||
<div className={styles.cityListWrapper}>
|
||||
<div className={styles.header}>
|
||||
<SkeletonShimmer height="30px" width="200px" />
|
||||
<SkeletonShimmer height="30px" width="120px" />
|
||||
</div>
|
||||
<ul className={styles.cityList}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<CityListItemSkeleton key={index} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +1,46 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||
|
||||
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import CityListItem from "../CityListItem"
|
||||
import CityListSkeleton from "./CityListSkeleton"
|
||||
|
||||
import styles from "./cityList.module.css"
|
||||
|
||||
import type { DestinationCityListItem } from "@/types/trpc/routers/contentstack/destinationCityPage"
|
||||
export default function CityList() {
|
||||
const intl = useIntl()
|
||||
const { filters, sortItems, activeCities, isLoading } =
|
||||
useDestinationDataStore((state) => ({
|
||||
filters: state.allFilters,
|
||||
sortItems: state.sortItems,
|
||||
activeCities: state.activeCities,
|
||||
isLoading: state.isLoading,
|
||||
}))
|
||||
|
||||
interface CityListProps {
|
||||
cities: DestinationCityListItem[]
|
||||
}
|
||||
|
||||
export default async function CityList({ cities }: CityListProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
return isLoading ? (
|
||||
<CityListSkeleton />
|
||||
) : (
|
||||
<div className={styles.cityListWrapper}>
|
||||
<div className={styles.header}>
|
||||
<Body>
|
||||
{intl.formatMessage(
|
||||
{ id: "{count} destinations" },
|
||||
{ count: cities.length }
|
||||
{ count: activeCities.length }
|
||||
)}
|
||||
</Body>
|
||||
<DestinationFilterAndSort
|
||||
filters={filters}
|
||||
sortItems={sortItems}
|
||||
listType="city"
|
||||
/>
|
||||
</div>
|
||||
<ul className={styles.cityList}>
|
||||
{cities.map((city) => (
|
||||
{activeCities.map((city) => (
|
||||
<li key={city.system.uid}>
|
||||
<CityListItem city={city} />
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import ExperienceListSkeleton from "../../../ExperienceList/ExperienceListSkeleton"
|
||||
|
||||
import styles from "./cityListItem.module.css"
|
||||
|
||||
export default function CityListItemSkeleton() {
|
||||
return (
|
||||
<article className={styles.cityListItem}>
|
||||
<div className={styles.imageWrapper}>
|
||||
<SkeletonShimmer width="100%" height="100%" />
|
||||
</div>
|
||||
<section className={styles.content}>
|
||||
<SkeletonShimmer height="52px" />
|
||||
<div className={styles.experienceList}>
|
||||
<ExperienceListSkeleton />
|
||||
</div>
|
||||
<div className={styles.ctaWrapper}>
|
||||
<SkeletonShimmer height="45px" width="100%" />
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { mapImageVaultImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import ExperienceList from "../../../ExperienceList"
|
||||
@@ -16,8 +18,8 @@ interface CityListItemProps {
|
||||
city: DestinationCityListItem
|
||||
}
|
||||
|
||||
export default async function CityListItem({ city }: CityListItemProps) {
|
||||
const intl = await getIntl()
|
||||
export default function CityListItem({ city }: CityListItemProps) {
|
||||
const intl = useIntl()
|
||||
const galleryImages = mapImageVaultImagesToGalleryImages(city.images)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
import { env } from "@/env/server"
|
||||
import {
|
||||
getDestinationCityPagesByCountry,
|
||||
getHotelsByCountry,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import Map from "../../Map"
|
||||
import { getHeadingText } from "../../utils"
|
||||
import CityList from "./CityList"
|
||||
|
||||
import styles from "./countryMap.module.css"
|
||||
|
||||
import type { Country } from "@/types/enums/country"
|
||||
|
||||
interface CountryMapProps {
|
||||
country: Country
|
||||
mapId: string
|
||||
apiKey: string
|
||||
country: string
|
||||
}
|
||||
|
||||
export function preload(country: Country) {
|
||||
void getHotelsByCountry(country)
|
||||
void getDestinationCityPagesByCountry(country)
|
||||
}
|
||||
export default async function CountryMap({ country }: CountryMapProps) {
|
||||
const intl = await getIntl()
|
||||
const [hotels, cities] = await Promise.all([
|
||||
getHotelsByCountry(country),
|
||||
getDestinationCityPagesByCountry(country),
|
||||
])
|
||||
export default function CountryMap({
|
||||
mapId,
|
||||
apiKey,
|
||||
country,
|
||||
}: CountryMapProps) {
|
||||
const intl = useIntl()
|
||||
const { activeHotels, allFilters, activeFilters } = useDestinationDataStore(
|
||||
(state) => ({
|
||||
activeHotels: state.activeHotels,
|
||||
allFilters: state.allFilters,
|
||||
activeFilters: state.activeFilters,
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<Map
|
||||
hotels={hotels}
|
||||
mapId={env.GOOGLE_DYNAMIC_MAP_ID}
|
||||
apiKey={env.GOOGLE_STATIC_MAP_KEY}
|
||||
pageType="country"
|
||||
>
|
||||
<Map hotels={activeHotels} mapId={mapId} apiKey={apiKey} pageType="country">
|
||||
<Title
|
||||
level="h2"
|
||||
as="h3"
|
||||
textTransform="regular"
|
||||
className={styles.title}
|
||||
>
|
||||
{intl.formatMessage({ id: `Destinations in {country}` }, { country })}
|
||||
{getHeadingText(intl, country, allFilters, activeFilters[0])}
|
||||
</Title>
|
||||
<CityList cities={cities} />
|
||||
<CityList />
|
||||
</Map>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client"
|
||||
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||
|
||||
import CityListingSkeleton from "../CityListing/CityListingSkeleton"
|
||||
import ExperienceListSkeleton from "../ExperienceList/ExperienceListSkeleton"
|
||||
import SidebarContentWrapperSkeleton from "../SidebarContentWrapper/SidebarContentWrapperSkeleton"
|
||||
import TopImagesSkeleton from "../TopImages/TopImagesSkeleton"
|
||||
|
||||
import styles from "./destinationCountryPage.module.css"
|
||||
|
||||
export default function DestinationCountryPageSkeleton() {
|
||||
return (
|
||||
<div className={styles.pageContainer}>
|
||||
<header className={styles.header}>
|
||||
<BreadcrumbsSkeleton />
|
||||
<TopImagesSkeleton />
|
||||
</header>
|
||||
<main className={styles.mainContent}>
|
||||
<CityListingSkeleton />
|
||||
</main>
|
||||
<aside className={styles.sidebar}>
|
||||
<SidebarContentWrapperSkeleton>
|
||||
<div>
|
||||
<SkeletonShimmer height="20px" width="90%" />
|
||||
<SkeletonShimmer height="20px" width="70%" />
|
||||
<SkeletonShimmer height="20px" width="100%" />
|
||||
<SkeletonShimmer height="20px" width="40%" />
|
||||
</div>
|
||||
<ExperienceListSkeleton />
|
||||
<div>
|
||||
<SkeletonShimmer height="200px" width="100%" />
|
||||
</div>
|
||||
</SidebarContentWrapperSkeleton>
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { IntlShape } from "react-intl"
|
||||
|
||||
import type { CategorizedFilters } from "@/types/components/destinationFilterAndSort"
|
||||
|
||||
export function getHeadingText(
|
||||
intl: IntlShape,
|
||||
location: string,
|
||||
allFilters: CategorizedFilters,
|
||||
filter?: string
|
||||
) {
|
||||
if (filter) {
|
||||
const facilityFilter = allFilters.facilityFilters.find(
|
||||
(f) => f.slug === filter
|
||||
)
|
||||
const surroudingsFilter = allFilters.surroundingsFilters.find(
|
||||
(f) => f.slug === filter
|
||||
)
|
||||
|
||||
if (facilityFilter) {
|
||||
return intl.formatMessage(
|
||||
{ id: "Hotels with {filter} in {location}" },
|
||||
{ location, filter: facilityFilter.name }
|
||||
)
|
||||
} else if (surroudingsFilter) {
|
||||
return intl.formatMessage(
|
||||
{ id: "Hotels near {filter} in {location}" },
|
||||
{ location, filter: surroudingsFilter.name }
|
||||
)
|
||||
}
|
||||
}
|
||||
return intl.formatMessage({ id: "Hotels in {location}" }, { location })
|
||||
}
|
||||
@@ -30,13 +30,6 @@
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
}
|
||||
|
||||
.experienceList {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.pageContainer {
|
||||
max-width: var(--max-width-page);
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { getDestinationCountryPage } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Blocks from "@/components/Blocks"
|
||||
import Breadcrumbs from "@/components/Breadcrumbs"
|
||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
|
||||
import CityDataContainer, { preload } from "../CityDataContainer"
|
||||
import CityListing from "../CityListing"
|
||||
import CityListingSkeleton from "../CityListing/CityListingSkeleton"
|
||||
import ExperienceList from "../ExperienceList"
|
||||
import SidebarContentWrapper from "../SidebarContentWrapper"
|
||||
import DestinationPageSidePeek from "../Sidepeek"
|
||||
import StaticMap from "../StaticMap"
|
||||
import TopImages from "../TopImages"
|
||||
import CountryMap, { preload } from "./CountryMap"
|
||||
import SidebarContentWrapper from "./SidebarContentWrapper"
|
||||
import CountryMap from "./CountryMap"
|
||||
import DestinationCountryPageSkeleton from "./DestinationCountryPageSkeleton"
|
||||
|
||||
import styles from "./destinationCountryPage.module.css"
|
||||
|
||||
@@ -33,7 +34,6 @@ export default async function DestinationCountryPage() {
|
||||
const {
|
||||
blocks,
|
||||
images,
|
||||
heading,
|
||||
preamble,
|
||||
experiences,
|
||||
has_sidepeek,
|
||||
@@ -46,37 +46,44 @@ export default async function DestinationCountryPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.pageContainer}>
|
||||
<header className={styles.header}>
|
||||
<Suspense fallback={<BreadcrumbsSkeleton />}>
|
||||
<Breadcrumbs variant={PageContentTypeEnum.destinationCityPage} />
|
||||
</Suspense>
|
||||
<TopImages images={images} destinationName={translatedCountry} />
|
||||
</header>
|
||||
<main className={styles.mainContent}>
|
||||
<Suspense fallback={<CityListingSkeleton />}>
|
||||
<CityListing country={destination_settings.country} />
|
||||
</Suspense>
|
||||
{blocks && <Blocks blocks={blocks} />}
|
||||
</main>
|
||||
<aside className={styles.sidebar}>
|
||||
<SidebarContentWrapper>
|
||||
<Title level="h2">{heading}</Title>
|
||||
<Body color="uiTextMediumContrast">{preamble}</Body>
|
||||
<ExperienceList experiences={experiences} />
|
||||
{has_sidepeek && (
|
||||
<DestinationPageSidePeek
|
||||
buttonText={sidepeek_button_text}
|
||||
sidePeekContent={sidepeek_content}
|
||||
/>
|
||||
)}
|
||||
<Suspense fallback={<DestinationCountryPageSkeleton />}>
|
||||
<CityDataContainer country={destination_settings.country}>
|
||||
<div className={styles.pageContainer}>
|
||||
<header className={styles.header}>
|
||||
<Suspense fallback={<BreadcrumbsSkeleton />}>
|
||||
<Breadcrumbs
|
||||
variant={PageContentTypeEnum.destinationCityPage}
|
||||
/>
|
||||
</Suspense>
|
||||
<TopImages images={images} destinationName={translatedCountry} />
|
||||
</header>
|
||||
<main className={styles.mainContent}>
|
||||
<CityListing />
|
||||
{blocks && <Blocks blocks={blocks} />}
|
||||
</main>
|
||||
<aside className={styles.sidebar}>
|
||||
<SidebarContentWrapper location={translatedCountry}>
|
||||
<Body color="uiTextMediumContrast">{preamble}</Body>
|
||||
<ExperienceList experiences={experiences} />
|
||||
{has_sidepeek && (
|
||||
<DestinationPageSidePeek
|
||||
buttonText={sidepeek_button_text}
|
||||
sidePeekContent={sidepeek_content}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StaticMap country={destination_settings.country} />
|
||||
</SidebarContentWrapper>
|
||||
</aside>
|
||||
</div>
|
||||
<CountryMap country={destination_settings.country} />
|
||||
<TrackingSDK pageData={tracking} />
|
||||
<StaticMap country={destination_settings.country} />
|
||||
</SidebarContentWrapper>
|
||||
</aside>
|
||||
</div>
|
||||
<CountryMap
|
||||
country={translatedCountry}
|
||||
apiKey={env.GOOGLE_STATIC_MAP_KEY}
|
||||
mapId={env.GOOGLE_DYNAMIC_MAP_ID}
|
||||
/>
|
||||
</CityDataContainer>
|
||||
</Suspense>
|
||||
<TrackingSDK pageData={tracking} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Chip from "@/components/TempDesignSystem/Chip"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { mapExperiencesToListData } from "./utils"
|
||||
|
||||
@@ -9,10 +12,8 @@ interface ExperienceListProps {
|
||||
experiences: string[]
|
||||
}
|
||||
|
||||
export default async function ExperienceList({
|
||||
experiences,
|
||||
}: ExperienceListProps) {
|
||||
const intl = await getIntl()
|
||||
export default function ExperienceList({ experiences }: ExperienceListProps) {
|
||||
const intl = useIntl()
|
||||
const experienceList = mapExperiencesToListData(experiences, intl)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { getHotelsByCityIdentifier } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
import HotelDataProvider from "@/providers/HotelDataProvider"
|
||||
import DestinationDataProvider from "@/providers/DestinationDataProvider"
|
||||
|
||||
import type { SortItem } from "@/types/components/hotelFilterAndSort"
|
||||
import { SortOption } from "@/types/enums/hotelFilterAndSort"
|
||||
import type { SortItem } from "@/types/components/destinationFilterAndSort"
|
||||
import { SortOption } from "@/types/enums/destinationFilterAndSort"
|
||||
|
||||
interface HotelDataContainerProps extends React.PropsWithChildren {
|
||||
cityIdentifier: string
|
||||
filterFromUrl?: string
|
||||
}
|
||||
|
||||
export function preload(cityIdentifier: string) {
|
||||
@@ -17,7 +16,6 @@ export function preload(cityIdentifier: string) {
|
||||
|
||||
export default async function HotelDataContainer({
|
||||
cityIdentifier,
|
||||
filterFromUrl,
|
||||
children,
|
||||
}: HotelDataContainerProps) {
|
||||
const intl = await getIntl()
|
||||
@@ -27,6 +25,7 @@ export default async function HotelDataContainer({
|
||||
{
|
||||
label: intl.formatMessage({ id: "Distance to city center" }),
|
||||
value: SortOption.Distance,
|
||||
isDefault: true,
|
||||
},
|
||||
{ label: intl.formatMessage({ id: "Name" }), value: SortOption.Name },
|
||||
{
|
||||
@@ -36,12 +35,8 @@ export default async function HotelDataContainer({
|
||||
]
|
||||
|
||||
return (
|
||||
<HotelDataProvider
|
||||
allHotels={hotels}
|
||||
filterFromUrl={filterFromUrl}
|
||||
sortItems={sortItems}
|
||||
>
|
||||
<DestinationDataProvider allHotels={hotels} sortItems={sortItems}>
|
||||
{children}
|
||||
</HotelDataProvider>
|
||||
</DestinationDataProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
.imageWrapper {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.tripAdvisor {
|
||||
@@ -64,6 +65,10 @@
|
||||
grid-template-columns: minmax(250px, 350px) auto;
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ctaWrapper {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ export default function HotelListingItem({
|
||||
<div className={styles.imageWrapper}>
|
||||
<ImageGallery
|
||||
images={galleryImages}
|
||||
fill
|
||||
sizes="(min-width: 768px) 350px, 100vw"
|
||||
title={intl.formatMessage(
|
||||
{ id: "{title} - Image gallery" },
|
||||
{ title: hotel.name }
|
||||
|
||||
@@ -11,6 +11,7 @@ export default function HotelListingSkeleton() {
|
||||
<section className={styles.container}>
|
||||
<div className={styles.listHeader}>
|
||||
<SkeletonShimmer height="30px" width="300px" />
|
||||
<SkeletonShimmer height="30px" width="100px" />
|
||||
</div>
|
||||
<ul className={styles.hotelList}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
|
||||
@@ -1,52 +1,39 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useRef, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useHotelDataStore } from "@/stores/hotel-data"
|
||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||
|
||||
import HotelFilterAndSort from "@/components/HotelFilterAndSort"
|
||||
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
|
||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useHash from "@/hooks/useHash"
|
||||
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||
|
||||
import HotelListingItem from "./HotelListingItem"
|
||||
import HotelListingSkeleton from "./HotelListingSkeleton"
|
||||
|
||||
import styles from "./hotelListing.module.css"
|
||||
|
||||
export default function HotelListing() {
|
||||
const intl = useIntl()
|
||||
const hash = useHash()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const scrollRef = useRef<HTMLElement>(null)
|
||||
const { showBackToTop, scrollToTop } = useScrollToTop({
|
||||
threshold: 300,
|
||||
elementRef: scrollRef,
|
||||
})
|
||||
const {
|
||||
activeHotels,
|
||||
filters,
|
||||
sortItems,
|
||||
initialHashFilterLoaded,
|
||||
loadInitialHashFilter,
|
||||
} = useHotelDataStore((state) => ({
|
||||
activeHotels: state.activeHotels,
|
||||
filters: state.allFilters,
|
||||
sortItems: state.sortItems,
|
||||
initialHashFilterLoaded: state.initialHashFilterLoaded,
|
||||
loadInitialHashFilter: state.actions.loadInitialHashFilter,
|
||||
}))
|
||||
const { activeHotels, filters, sortItems, isLoading } =
|
||||
useDestinationDataStore((state) => ({
|
||||
activeHotels: state.activeHotels,
|
||||
filters: state.allFilters,
|
||||
sortItems: state.sortItems,
|
||||
isLoading: state.isLoading,
|
||||
}))
|
||||
const [allHotelsVisible, setAllHotelsVisible] = useState(
|
||||
activeHotels.length <= 5
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (hash !== undefined && !initialHashFilterLoaded) {
|
||||
loadInitialHashFilter(hash)
|
||||
}
|
||||
}, [hash, loadInitialHashFilter, initialHashFilterLoaded])
|
||||
|
||||
function handleShowMore() {
|
||||
if (scrollRef.current && allHotelsVisible) {
|
||||
scrollRef.current.scrollIntoView({ behavior: "smooth" })
|
||||
@@ -54,7 +41,9 @@ export default function HotelListing() {
|
||||
setAllHotelsVisible((state) => !state)
|
||||
}
|
||||
|
||||
return (
|
||||
return isLoading ? (
|
||||
<HotelListingSkeleton />
|
||||
) : (
|
||||
<section className={styles.container} ref={scrollRef}>
|
||||
<div className={styles.listHeader}>
|
||||
<Subtitle type="two">
|
||||
@@ -65,7 +54,11 @@ export default function HotelListing() {
|
||||
{ count: activeHotels.length }
|
||||
)}
|
||||
</Subtitle>
|
||||
<HotelFilterAndSort filters={filters} sortItems={sortItems} />
|
||||
<DestinationFilterAndSort
|
||||
filters={filters}
|
||||
sortItems={sortItems}
|
||||
listType="hotel"
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
className={`${styles.hotelList} ${allHotelsVisible ? styles.allVisible : ""}`}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
max-width: 500px;
|
||||
background-color: var(--Base-Surface-Primary-Normal);
|
||||
overflow-y: auto;
|
||||
padding: var(--Spacing-x4);
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
import { useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useHotelDataStore } from "@/stores/hotel-data"
|
||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||
import { StickyElementNameEnum } from "@/stores/sticky-position"
|
||||
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import useStickyPosition from "@/hooks/useStickyPosition"
|
||||
|
||||
import { getCityHeadingText } from "../utils"
|
||||
import { getHeadingText } from "../utils"
|
||||
|
||||
import styles from "./sidebarContentWrapper.module.css"
|
||||
|
||||
interface SidebarContentWrapperProps extends React.PropsWithChildren {
|
||||
cityName?: string
|
||||
location: string
|
||||
}
|
||||
|
||||
export default function SidebarContentWrapper({
|
||||
cityName,
|
||||
location,
|
||||
children,
|
||||
}: SidebarContentWrapperProps) {
|
||||
const intl = useIntl()
|
||||
@@ -27,17 +27,16 @@ export default function SidebarContentWrapper({
|
||||
ref: sidebarRef,
|
||||
name: StickyElementNameEnum.DESTINATION_SIDEBAR,
|
||||
})
|
||||
const { activeFilters, allFilters } = useHotelDataStore((state) => ({
|
||||
const { activeFilters, allFilters } = useDestinationDataStore((state) => ({
|
||||
activeFilters: state.activeFilters,
|
||||
allFilters: state.allFilters,
|
||||
}))
|
||||
const headingText = cityName
|
||||
? getCityHeadingText(intl, cityName, allFilters, activeFilters[0])
|
||||
: null
|
||||
|
||||
return (
|
||||
<div ref={sidebarRef} className={styles.sidebarContent}>
|
||||
{headingText && <Title level="h2">{headingText}</Title>}
|
||||
<Title level="h2">
|
||||
{getHeadingText(intl, location, allFilters, activeFilters[0])}
|
||||
</Title>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { IntlShape } from "react-intl"
|
||||
|
||||
import type { CategorizedFilters } from "@/types/components/hotelFilterAndSort"
|
||||
import type { CategorizedFilters } from "@/types/components/destinationFilterAndSort"
|
||||
|
||||
export function getCityHeadingText(
|
||||
export function getHeadingText(
|
||||
intl: IntlShape,
|
||||
cityName: string,
|
||||
location: string,
|
||||
allFilters: CategorizedFilters,
|
||||
filter?: string
|
||||
) {
|
||||
@@ -18,15 +18,15 @@ export function getCityHeadingText(
|
||||
|
||||
if (facilityFilter) {
|
||||
return intl.formatMessage(
|
||||
{ id: "Hotels with {filter} in {cityName}" },
|
||||
{ cityName, filter: facilityFilter.name }
|
||||
{ id: "Hotels with {filter} in {location}" },
|
||||
{ location, filter: facilityFilter.name }
|
||||
)
|
||||
} else if (surroudingsFilter) {
|
||||
return intl.formatMessage(
|
||||
{ id: "Hotels near {filter} in {cityName}" },
|
||||
{ cityName, filter: surroudingsFilter.name }
|
||||
{ id: "Hotels near {filter} in {location}" },
|
||||
{ location, filter: surroudingsFilter.name }
|
||||
)
|
||||
}
|
||||
}
|
||||
return intl.formatMessage({ id: "Hotels in {city}" }, { city: cityName })
|
||||
return intl.formatMessage({ id: "Hotels in {location}" }, { location })
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useHotelDataStore } from "@/stores/hotel-data"
|
||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
@@ -11,7 +11,7 @@ import Checkbox from "./Checkbox"
|
||||
|
||||
import styles from "./filter.module.css"
|
||||
|
||||
import type { CategorizedFilters } from "@/types/components/hotelFilterAndSort"
|
||||
import type { CategorizedFilters } from "@/types/components/destinationFilterAndSort"
|
||||
|
||||
interface FilterProps {
|
||||
filters: CategorizedFilters
|
||||
@@ -20,7 +20,7 @@ interface FilterProps {
|
||||
export default function Filter({ filters }: FilterProps) {
|
||||
const intl = useIntl()
|
||||
const { facilityFilters, surroundingsFilters } = filters
|
||||
const { pendingFilters, togglePendingFilter } = useHotelDataStore(
|
||||
const { pendingFilters, togglePendingFilter } = useDestinationDataStore(
|
||||
(state) => ({
|
||||
pendingFilters: state.pendingFilters,
|
||||
togglePendingFilter: state.actions.togglePendingFilter,
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useHotelDataStore } from "@/stores/hotel-data"
|
||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||
|
||||
import Select from "@/components/TempDesignSystem/Select"
|
||||
|
||||
import type { SortItem } from "@/types/components/hotelFilterAndSort"
|
||||
import type { SortOption } from "@/types/enums/hotelFilterAndSort"
|
||||
import type { SortItem } from "@/types/components/destinationFilterAndSort"
|
||||
import type { SortOption } from "@/types/enums/destinationFilterAndSort"
|
||||
|
||||
interface SortProps {
|
||||
sortItems: SortItem[]
|
||||
@@ -15,7 +15,7 @@ interface SortProps {
|
||||
|
||||
export default function Sort({ sortItems }: SortProps) {
|
||||
const intl = useIntl()
|
||||
const { pendingSort, setPendingSort } = useHotelDataStore((state) => ({
|
||||
const { pendingSort, setPendingSort } = useDestinationDataStore((state) => ({
|
||||
pendingSort: state.pendingSort,
|
||||
setPendingSort: state.actions.setPendingSort,
|
||||
}))
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -8,7 +9,7 @@ import {
|
||||
} from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useHotelDataStore } from "@/stores/hotel-data"
|
||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||
|
||||
import { CloseLargeIcon, FilterIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
@@ -19,39 +20,75 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Filter from "./Filter"
|
||||
import Sort from "./Sort"
|
||||
|
||||
import styles from "./hotelFilterAndSort.module.css"
|
||||
import styles from "./destinationFilterAndSort.module.css"
|
||||
|
||||
import type {
|
||||
CategorizedFilters,
|
||||
SortItem,
|
||||
} from "@/types/components/hotelFilterAndSort"
|
||||
} from "@/types/components/destinationFilterAndSort"
|
||||
|
||||
interface HotelFilterAndSortProps {
|
||||
filters: CategorizedFilters
|
||||
sortItems: SortItem[]
|
||||
listType: "city" | "hotel"
|
||||
}
|
||||
|
||||
export default function HotelFilterAndSort({
|
||||
export default function DestinationFilterAndSort({
|
||||
filters,
|
||||
sortItems,
|
||||
listType,
|
||||
}: HotelFilterAndSortProps) {
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const {
|
||||
pendingFilters,
|
||||
pendingSort,
|
||||
defaultSort,
|
||||
basePath,
|
||||
pendingCount,
|
||||
activeFilters,
|
||||
clearPendingFilters,
|
||||
resetPendingValues,
|
||||
submitFiltersAndSort,
|
||||
} = useHotelDataStore((state) => ({
|
||||
pendingCount: state.pendingCount,
|
||||
setIsLoading,
|
||||
} = useDestinationDataStore((state) => ({
|
||||
pendingFilters: state.pendingFilters,
|
||||
pendingSort: state.pendingSort,
|
||||
basePath: state.basePathnameWithoutFilters,
|
||||
defaultSort: state.defaultSort,
|
||||
pendingCount:
|
||||
listType === "city" ? state.pendingCityCount : state.pendingHotelCount,
|
||||
activeFilters: state.activeFilters,
|
||||
clearPendingFilters: state.actions.clearPendingFilters,
|
||||
resetPendingValues: state.actions.resetPendingValues,
|
||||
submitFiltersAndSort: state.actions.submitFiltersAndSort,
|
||||
setIsLoading: state.actions.setIsLoading,
|
||||
}))
|
||||
|
||||
function submitAndClose(close: () => void) {
|
||||
submitFiltersAndSort()
|
||||
setIsLoading(true)
|
||||
const sort = pendingSort
|
||||
const filters = pendingFilters
|
||||
|
||||
const parsedUrl = new URL(window.location.href)
|
||||
const searchParams = parsedUrl.searchParams
|
||||
if (sort === defaultSort && searchParams.has("sort")) {
|
||||
searchParams.delete("sort")
|
||||
} else if (sort !== defaultSort) {
|
||||
searchParams.set("sort", sort)
|
||||
}
|
||||
|
||||
const [firstFilter, ...remainingFilters] = filters
|
||||
|
||||
parsedUrl.pathname = basePath
|
||||
if (firstFilter) {
|
||||
parsedUrl.pathname += `/${firstFilter}`
|
||||
}
|
||||
if (remainingFilters.length > 0) {
|
||||
parsedUrl.hash = `#${remainingFilters.join("&")}`
|
||||
} else {
|
||||
parsedUrl.hash = ""
|
||||
}
|
||||
|
||||
router.push(parsedUrl.toString(), { scroll: false })
|
||||
close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user