Feat/SW-2271 hotel list filtering

* feat(SW-2271): Changes to hotel data types in preperation for filtering
* feat(SW-2271): Added filter and sort functionality

Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-07-04 09:27:20 +00:00
parent 82e21af0d4
commit fa7214cb58
58 changed files with 1572 additions and 450 deletions

View File

@@ -12,57 +12,64 @@ import {
} from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useHotelListingDataStore } from "@/stores/hotel-listing-data"
import CampaignHotelListingSkeleton from "@/components/Blocks/CampaignHotelListing/CampaignHotelListingSkeleton"
import HotelFilterAndSort from "@/components/HotelFilterAndSort"
import HotelListingItem from "./HotelListingItem"
import styles from "./campaignHotelListing.module.css"
import type { HotelDataWithUrl } from "@scandic-hotels/trpc/types/hotel"
interface CampaignHotelListingClientProps {
heading: string
preamble?: string | null
hotels: HotelDataWithUrl[]
visibleCountMobile?: 3 | 6
visibleCountDesktop?: 3 | 6
visibleCountMobile: 3 | 6
visibleCountDesktop: 3 | 6
isMainBlock: boolean
}
export default function CampaignHotelListingClient({
heading,
preamble,
hotels,
visibleCountMobile = 3,
visibleCountDesktop = 6,
visibleCountMobile,
visibleCountDesktop,
isMainBlock,
}: CampaignHotelListingClientProps) {
const intl = useIntl()
const isMobile = useMediaQuery("(max-width: 767px)")
const scrollRef = useRef<HTMLElement>(null)
const { activeHotels, isLoading } = useHotelListingDataStore((state) => ({
activeHotels: state.activeHotels,
isLoading: state.isLoading,
}))
const initialCount = isMobile ? visibleCountMobile : visibleCountDesktop // Initial number of hotels to show
const initialCount = isMobile ? visibleCountMobile : visibleCountDesktop // Initial number of activeHotels to show
const thresholdCount = initialCount + 3 // This is the threshold at which we start showing the "Show More" button
const showAllThreshold = initialCount * 3 // This is the threshold at which we show the "Show All" button
const incrementCount = initialCount // Number of hotels to increment when the button is clicked
const incrementCount = initialCount // Number of activeHotels to increment when the button is clicked
const [visibleCount, setVisibleCount] = useState(() =>
// Set initial visible count based on the number of hotels and the threshold
hotels.length <= thresholdCount ? hotels.length : initialCount
// Set initial visible count based on the number of activeHotels and the threshold
activeHotels.length <= thresholdCount ? activeHotels.length : initialCount
)
// Only show the show more/less button if the length of hotels exceeds the threshold count
const showButton = hotels.length > thresholdCount
// Only show the show more/less button if the length of activeHotels exceeds the threshold count
const showButton = activeHotels.length > thresholdCount
// Determine if we are at the stage where the user can click to show all hotels
// Determine if we are at the stage where the user can click to show all activeHotels
const canShowAll =
hotels.length > visibleCount &&
activeHotels.length > visibleCount &&
(visibleCount + incrementCount > showAllThreshold ||
visibleCount + incrementCount >= hotels.length)
visibleCount + incrementCount >= activeHotels.length)
function handleButtonClick() {
if (visibleCount < hotels.length) {
if (visibleCount < activeHotels.length) {
if (canShowAll) {
setVisibleCount(hotels.length)
setVisibleCount(activeHotels.length)
} else {
setVisibleCount((prev) =>
Math.min(prev + incrementCount, hotels.length)
Math.min(prev + incrementCount, activeHotels.length)
)
}
} else {
@@ -78,7 +85,7 @@ export default function CampaignHotelListingClient({
})
let iconDirection: MaterialIconProps["icon"] = "keyboard_arrow_down"
if (visibleCount === hotels.length) {
if (visibleCount === activeHotels.length) {
buttonText = intl.formatMessage({
defaultMessage: "Show less",
})
@@ -89,20 +96,30 @@ export default function CampaignHotelListingClient({
})
}
if (isLoading) {
return <CampaignHotelListingSkeleton />
}
return (
<section className={styles.hotelListingSection} ref={scrollRef}>
<section
className={cx(styles.hotelListingSection, {
[styles.isMainBlock]: isMainBlock,
})}
ref={scrollRef}
>
<header className={styles.header}>
<Typography variant="Title/Subtitle/lg">
<h3>{heading}</h3>
<Typography variant={isMainBlock ? "Title/md" : "Title/Subtitle/lg"}>
<h3 className={styles.heading}>{heading}</h3>
</Typography>
{isMainBlock ? <HotelFilterAndSort /> : null}
{preamble ? (
<Typography variant="Body/Paragraph/mdRegular">
<p>{preamble}</p>
<p className={styles.preamble}>{preamble}</p>
</Typography>
) : null}
</header>
<ul className={styles.list}>
{hotels.map(({ hotel, url }, index) => (
{activeHotels.map(({ hotel, url }, index) => (
<li
key={hotel.id}
className={cx(styles.listItem, {