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;
|
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 {
|
.sideBar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
||||||
|
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
|
||||||
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
||||||
import {
|
import {
|
||||||
generateChildrenString,
|
generateChildrenString,
|
||||||
@@ -61,33 +62,39 @@ export default async function SelectHotelPage({
|
|||||||
const filterList = getFiltersFromHotels(hotels)
|
const filterList = getFiltersFromHotels(hotels)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<>
|
||||||
<div className={styles.sideBar}>
|
<header className={styles.header}>
|
||||||
<Link
|
<div>{city.name}</div>
|
||||||
className={styles.link}
|
<HotelSorter />
|
||||||
color="burgundy"
|
</header>
|
||||||
href={selectHotelMap[params.lang]}
|
<main className={styles.main}>
|
||||||
keepSearchParams
|
<div className={styles.sideBar}>
|
||||||
>
|
<Link
|
||||||
<div className={styles.mapContainer}>
|
className={styles.link}
|
||||||
<StaticMap
|
color="burgundy"
|
||||||
city={searchParams.city}
|
href={selectHotelMap[params.lang]}
|
||||||
width={340}
|
keepSearchParams
|
||||||
height={180}
|
>
|
||||||
zoomLevel={11}
|
<div className={styles.mapContainer}>
|
||||||
mapType="roadmap"
|
<StaticMap
|
||||||
altText={`Map of ${searchParams.city} city center`}
|
city={searchParams.city}
|
||||||
/>
|
width={340}
|
||||||
<div className={styles.mapLinkText}>
|
height={180}
|
||||||
{intl.formatMessage({ id: "Show map" })}
|
zoomLevel={11}
|
||||||
<ChevronRightIcon color="burgundy" width={20} height={20} />
|
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>
|
||||||
</div>
|
</Link>
|
||||||
</Link>
|
<MobileMapButtonContainer city={searchParams.city} />
|
||||||
<MobileMapButtonContainer city={searchParams.city} />
|
<HotelFilter filters={filterList} />
|
||||||
<HotelFilter filters={filterList} />
|
</div>
|
||||||
</div>
|
<HotelCardListing hotelData={hotels} />
|
||||||
<HotelCardListing hotelData={hotels} />
|
</main>
|
||||||
</main>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useMemo } from "react"
|
|||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
|
||||||
import HotelCard from "../HotelCard"
|
import HotelCard from "../HotelCard"
|
||||||
|
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
|
||||||
|
|
||||||
import styles from "./hotelCardListing.module.css"
|
import styles from "./hotelCardListing.module.css"
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
type HotelCardListingProps,
|
type HotelCardListingProps,
|
||||||
HotelCardListingTypeEnum,
|
HotelCardListingTypeEnum,
|
||||||
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
|
import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
||||||
|
|
||||||
export default function HotelCardListing({
|
export default function HotelCardListing({
|
||||||
hotelData,
|
hotelData,
|
||||||
@@ -21,25 +23,58 @@ export default function HotelCardListing({
|
|||||||
}: HotelCardListingProps) {
|
}: HotelCardListingProps) {
|
||||||
const searchParams = useSearchParams()
|
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 hotels = useMemo(() => {
|
||||||
const appliedFilters = searchParams.get("filters")?.split(",")
|
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) =>
|
appliedFilters.every((appliedFilterId) =>
|
||||||
hotel.hotelData.detailedFacilities.some(
|
hotel.hotelData.detailedFacilities.some(
|
||||||
(facility) => facility.id.toString() === appliedFilterId
|
(facility) => facility.id.toString() === appliedFilterId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, [searchParams, hotelData])
|
}, [searchParams, sortedHotels])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.hotelCards}>
|
<section className={styles.hotelCards}>
|
||||||
{hotels?.length ? (
|
{hotels?.length ? (
|
||||||
hotels.map((hotel) => (
|
hotels.map((hotel) => (
|
||||||
<HotelCard
|
<HotelCard
|
||||||
key={hotel.hotelData.name}
|
key={hotel.hotelData.operaId}
|
||||||
hotel={hotel}
|
hotel={hotel}
|
||||||
type={type}
|
type={type}
|
||||||
state={hotel.hotelData.name === activeCard ? "active" : "default"}
|
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,
|
tabIndex,
|
||||||
value,
|
value,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
|
showRadioButton = false,
|
||||||
}: SelectProps) {
|
}: SelectProps) {
|
||||||
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined)
|
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined)
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ export default function Select({
|
|||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<ListBoxItem
|
<ListBoxItem
|
||||||
aria-label={String(item)}
|
aria-label={String(item)}
|
||||||
className={styles.listBoxItem}
|
className={`${styles.listBoxItem} ${showRadioButton && styles.showRadioButton}`}
|
||||||
id={item.value}
|
id={item.value}
|
||||||
key={item.label}
|
key={item.label}
|
||||||
data-testid={item.label}
|
data-testid={item.label}
|
||||||
|
|||||||
@@ -52,11 +52,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
|
padding: var(--Spacing-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.listBoxItem {
|
.listBoxItem {
|
||||||
padding: var(--Spacing-x1);
|
padding: var(--Spacing-x1);
|
||||||
margin: var(--Spacing-x0) var(--Spacing-x2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.listBoxItem[data-focused="true"],
|
.listBoxItem[data-focused="true"],
|
||||||
@@ -65,3 +65,23 @@
|
|||||||
border-radius: var(--Corner-radius-Medium,);
|
border-radius: var(--Corner-radius-Medium,);
|
||||||
outline: none;
|
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
|
placeholder?: string
|
||||||
value?: string | number
|
value?: string | number
|
||||||
maxHeight?: number
|
maxHeight?: number
|
||||||
|
showRadioButton?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SelectPortalContainer = HTMLDivElement | undefined
|
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 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 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!",
|
"Something went wrong!": "Noget gik galt!",
|
||||||
|
"Sort by": "Sorter efter",
|
||||||
"Sports": "Sport",
|
"Sports": "Sport",
|
||||||
"Standard price": "Standardpris",
|
"Standard price": "Standardpris",
|
||||||
"Street": "Gade",
|
"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 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 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!",
|
"Something went wrong!": "Etwas ist schief gelaufen!",
|
||||||
|
"Sort by": "Sortieren nach",
|
||||||
"Sports": "Sport",
|
"Sports": "Sport",
|
||||||
"Standard price": "Standardpreis",
|
"Standard price": "Standardpreis",
|
||||||
"Street": "Straße",
|
"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 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 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!",
|
"Something went wrong!": "Something went wrong!",
|
||||||
|
"Sort by": "Sort by",
|
||||||
"Sports": "Sports",
|
"Sports": "Sports",
|
||||||
"Standard price": "Standard price",
|
"Standard price": "Standard price",
|
||||||
"Street": "Street",
|
"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 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 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!",
|
"Something went wrong!": "Jotain meni pieleen!",
|
||||||
|
"Sort by": "Lajitteluperuste",
|
||||||
"Sports": "Urheilu",
|
"Sports": "Urheilu",
|
||||||
"Standard price": "Normaali hinta",
|
"Standard price": "Normaali hinta",
|
||||||
"Street": "Katu",
|
"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 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 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!",
|
"Something went wrong!": "Noe gikk galt!",
|
||||||
|
"Sort by": "Sorter etter",
|
||||||
"Sports": "Sport",
|
"Sports": "Sport",
|
||||||
"Standard price": "Standardpris",
|
"Standard price": "Standardpris",
|
||||||
"Street": "Gate",
|
"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 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 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!",
|
"Something went wrong!": "Något gick fel!",
|
||||||
|
"Sort by": "Sortera efter",
|
||||||
"Sports": "Sport",
|
"Sports": "Sport",
|
||||||
"Standard price": "Standardpris",
|
"Standard price": "Standardpris",
|
||||||
"Street": "Gata",
|
"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