Merged in feat/sw-620-sort-hotels (pull request #868)
Feat/sw-620 sort hotels * SW-620 Add radio button to select box * feat(SW-620): Implement sorting on select hotel * Fix casing * Shallow copy hoteldata * Use translations * Remove unnecessary style * Import order * Type Approved-by: Pontus Dreij
This commit is contained in:
@@ -9,6 +9,15 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
padding: var(--Spacing-x4) var(--Spacing-x5) var(--Spacing-x3)
|
||||
var(--Spacing-x5);
|
||||
justify-content: space-between;
|
||||
max-width: var(--max-width);
|
||||
}
|
||||
|
||||
.sideBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -9,6 +9,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 HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
|
||||
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
||||
import {
|
||||
generateChildrenString,
|
||||
@@ -61,33 +62,39 @@ export default async function SelectHotelPage({
|
||||
const filterList = getFiltersFromHotels(hotels)
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.sideBar}>
|
||||
<Link
|
||||
className={styles.link}
|
||||
color="burgundy"
|
||||
href={selectHotelMap[params.lang]}
|
||||
keepSearchParams
|
||||
>
|
||||
<div className={styles.mapContainer}>
|
||||
<StaticMap
|
||||
city={searchParams.city}
|
||||
width={340}
|
||||
height={180}
|
||||
zoomLevel={11}
|
||||
mapType="roadmap"
|
||||
altText={`Map of ${searchParams.city} city center`}
|
||||
/>
|
||||
<div className={styles.mapLinkText}>
|
||||
{intl.formatMessage({ id: "Show map" })}
|
||||
<ChevronRightIcon color="burgundy" width={20} height={20} />
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
<div>{city.name}</div>
|
||||
<HotelSorter />
|
||||
</header>
|
||||
<main className={styles.main}>
|
||||
<div className={styles.sideBar}>
|
||||
<Link
|
||||
className={styles.link}
|
||||
color="burgundy"
|
||||
href={selectHotelMap[params.lang]}
|
||||
keepSearchParams
|
||||
>
|
||||
<div className={styles.mapContainer}>
|
||||
<StaticMap
|
||||
city={searchParams.city}
|
||||
width={340}
|
||||
height={180}
|
||||
zoomLevel={11}
|
||||
mapType="roadmap"
|
||||
altText={`Map of ${searchParams.city} city center`}
|
||||
/>
|
||||
<div className={styles.mapLinkText}>
|
||||
{intl.formatMessage({ id: "Show map" })}
|
||||
<ChevronRightIcon color="burgundy" width={20} height={20} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<MobileMapButtonContainer city={searchParams.city} />
|
||||
<HotelFilter filters={filterList} />
|
||||
</div>
|
||||
<HotelCardListing hotelData={hotels} />
|
||||
</main>
|
||||
</Link>
|
||||
<MobileMapButtonContainer city={searchParams.city} />
|
||||
<HotelFilter filters={filterList} />
|
||||
</div>
|
||||
<HotelCardListing hotelData={hotels} />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMemo } from "react"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import HotelCard from "../HotelCard"
|
||||
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
|
||||
|
||||
import styles from "./hotelCardListing.module.css"
|
||||
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
type HotelCardListingProps,
|
||||
HotelCardListingTypeEnum,
|
||||
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
||||
|
||||
export default function HotelCardListing({
|
||||
hotelData,
|
||||
@@ -21,25 +23,58 @@ export default function HotelCardListing({
|
||||
}: HotelCardListingProps) {
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const sortBy = useMemo(
|
||||
() => searchParams.get("sort") ?? DEFAULT_SORT,
|
||||
[searchParams]
|
||||
)
|
||||
|
||||
const sortedHotels = useMemo(() => {
|
||||
switch (sortBy) {
|
||||
case SortOrder.Name:
|
||||
return [...hotelData].sort((a, b) =>
|
||||
a.hotelData.name.localeCompare(b.hotelData.name)
|
||||
)
|
||||
case SortOrder.TripAdvisorRating:
|
||||
return [...hotelData].sort(
|
||||
(a, b) =>
|
||||
(b.hotelData.ratings?.tripAdvisor.rating ?? 0) -
|
||||
(a.hotelData.ratings?.tripAdvisor.rating ?? 0)
|
||||
)
|
||||
case SortOrder.Price:
|
||||
return [...hotelData].sort(
|
||||
(a, b) =>
|
||||
parseInt(a.price?.memberAmount ?? "0", 10) -
|
||||
parseInt(b.price?.memberAmount ?? "0", 10)
|
||||
)
|
||||
case SortOrder.Distance:
|
||||
default:
|
||||
return [...hotelData].sort(
|
||||
(a, b) =>
|
||||
a.hotelData.location.distanceToCentre -
|
||||
b.hotelData.location.distanceToCentre
|
||||
)
|
||||
}
|
||||
}, [hotelData, sortBy])
|
||||
|
||||
const hotels = useMemo(() => {
|
||||
const appliedFilters = searchParams.get("filters")?.split(",")
|
||||
if (!appliedFilters || appliedFilters.length === 0) return hotelData
|
||||
if (!appliedFilters || appliedFilters.length === 0) return sortedHotels
|
||||
|
||||
return hotelData.filter((hotel) =>
|
||||
return sortedHotels.filter((hotel) =>
|
||||
appliedFilters.every((appliedFilterId) =>
|
||||
hotel.hotelData.detailedFacilities.some(
|
||||
(facility) => facility.id.toString() === appliedFilterId
|
||||
)
|
||||
)
|
||||
)
|
||||
}, [searchParams, hotelData])
|
||||
}, [searchParams, sortedHotels])
|
||||
|
||||
return (
|
||||
<section className={styles.hotelCards}>
|
||||
{hotels?.length ? (
|
||||
hotels.map((hotel) => (
|
||||
<HotelCard
|
||||
key={hotel.hotelData.name}
|
||||
key={hotel.hotelData.operaId}
|
||||
hotel={hotel}
|
||||
type={type}
|
||||
state={hotel.hotelData.name === activeCard ? "active" : "default"}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname, useSearchParams } from "next/navigation"
|
||||
import { useCallback } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Select from "@/components/TempDesignSystem/Select"
|
||||
|
||||
import {
|
||||
type SortItem,
|
||||
SortOrder,
|
||||
} from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
||||
|
||||
const sortItems: SortItem[] = [
|
||||
{ label: "Distance", value: SortOrder.Distance },
|
||||
{ label: "Name", value: SortOrder.Name },
|
||||
{ label: "Price", value: SortOrder.Price },
|
||||
{ label: "TripAdvisor rating", value: SortOrder.TripAdvisorRating },
|
||||
]
|
||||
|
||||
export const DEFAULT_SORT = SortOrder.Distance
|
||||
|
||||
export default function HotelSorter() {
|
||||
const searchParams = useSearchParams()
|
||||
const pathname = usePathname()
|
||||
const i18n = useIntl()
|
||||
|
||||
const onSelect = useCallback(
|
||||
(value: string | number) => {
|
||||
const newSort = value.toString()
|
||||
if (newSort === searchParams.get("sort")) {
|
||||
return
|
||||
}
|
||||
|
||||
const newSearchParams = new URLSearchParams(searchParams)
|
||||
newSearchParams.set("sort", newSort)
|
||||
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${pathname}?${newSearchParams.toString()}`
|
||||
)
|
||||
},
|
||||
[pathname, searchParams]
|
||||
)
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={sortItems}
|
||||
label={i18n.formatMessage({ id: "Sort by" })}
|
||||
name="sort"
|
||||
showRadioButton
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export default function Select({
|
||||
tabIndex,
|
||||
value,
|
||||
maxHeight,
|
||||
showRadioButton = false,
|
||||
}: SelectProps) {
|
||||
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined)
|
||||
|
||||
@@ -88,7 +89,7 @@ export default function Select({
|
||||
{items.map((item) => (
|
||||
<ListBoxItem
|
||||
aria-label={String(item)}
|
||||
className={styles.listBoxItem}
|
||||
className={`${styles.listBoxItem} ${showRadioButton && styles.showRadioButton}`}
|
||||
id={item.value}
|
||||
key={item.label}
|
||||
data-testid={item.label}
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.listBoxItem {
|
||||
padding: var(--Spacing-x1);
|
||||
margin: var(--Spacing-x0) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.listBoxItem[data-focused="true"],
|
||||
@@ -65,3 +65,23 @@
|
||||
border-radius: var(--Corner-radius-Medium,);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.listBoxItem.showRadioButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.listBoxItem.showRadioButton:before {
|
||||
display: flex;
|
||||
content: "";
|
||||
margin-right: var(--Spacing-x-one-and-half);
|
||||
background-color: white;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 0 2px var(--Base-Border-Normal);
|
||||
}
|
||||
|
||||
.listBoxItem[data-selected="true"].showRadioButton:before {
|
||||
box-shadow: inset 0 0 0 8px var(--UI-Input-Controls-Fill-Selected);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface SelectProps
|
||||
placeholder?: string
|
||||
value?: string | number
|
||||
maxHeight?: number
|
||||
showRadioButton?: boolean
|
||||
}
|
||||
|
||||
export type SelectPortalContainer = HTMLDivElement | undefined
|
||||
|
||||
@@ -313,6 +313,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.",
|
||||
"Something went wrong!": "Noget gik galt!",
|
||||
"Sort by": "Sorter efter",
|
||||
"Sports": "Sport",
|
||||
"Standard price": "Standardpris",
|
||||
"Street": "Gade",
|
||||
|
||||
@@ -312,6 +312,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.",
|
||||
"Something went wrong!": "Etwas ist schief gelaufen!",
|
||||
"Sort by": "Sortieren nach",
|
||||
"Sports": "Sport",
|
||||
"Standard price": "Standardpreis",
|
||||
"Street": "Straße",
|
||||
|
||||
@@ -343,6 +343,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
|
||||
"Something went wrong!": "Something went wrong!",
|
||||
"Sort by": "Sort by",
|
||||
"Sports": "Sports",
|
||||
"Standard price": "Standard price",
|
||||
"Street": "Street",
|
||||
|
||||
@@ -314,6 +314,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.",
|
||||
"Something went wrong!": "Jotain meni pieleen!",
|
||||
"Sort by": "Lajitteluperuste",
|
||||
"Sports": "Urheilu",
|
||||
"Standard price": "Normaali hinta",
|
||||
"Street": "Katu",
|
||||
|
||||
@@ -311,6 +311,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.",
|
||||
"Something went wrong!": "Noe gikk galt!",
|
||||
"Sort by": "Sorter etter",
|
||||
"Sports": "Sport",
|
||||
"Standard price": "Standardpris",
|
||||
"Street": "Gate",
|
||||
|
||||
@@ -311,6 +311,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.",
|
||||
"Something went wrong!": "Något gick fel!",
|
||||
"Sort by": "Sortera efter",
|
||||
"Sports": "Sport",
|
||||
"Standard price": "Standardpris",
|
||||
"Street": "Gata",
|
||||
|
||||
11
types/components/hotelReservation/selectHotel/hotelSorter.ts
Normal file
11
types/components/hotelReservation/selectHotel/hotelSorter.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const enum SortOrder {
|
||||
Distance = "distance",
|
||||
Name = "name",
|
||||
Price = "price",
|
||||
TripAdvisorRating = "tripadvisor",
|
||||
}
|
||||
|
||||
export type SortItem = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
Reference in New Issue
Block a user