Merged master into feat/SW-888-skeleton-loaders
This commit is contained in:
@@ -46,7 +46,7 @@ export default async function HotelListingItem({
|
||||
</div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage(
|
||||
{ id: "Distance to city centre" },
|
||||
{ id: "Distance in km to city centre" },
|
||||
{ number: distanceToCentre }
|
||||
)}
|
||||
</Caption>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default async function IntroSection({
|
||||
const { streetAddress, city } = address
|
||||
const { distanceToCentre } = location
|
||||
const formattedDistanceText = intl.formatMessage(
|
||||
{ id: "Distance to city centre" },
|
||||
{ id: "Distance in km to city centre" },
|
||||
{ number: distanceToCentre }
|
||||
)
|
||||
const lang = getLang()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.header {
|
||||
display: grid;
|
||||
background-color: var(--Main-Grey-White);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1366px) {
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function HotelCard({
|
||||
</div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage(
|
||||
{ id: "Distance to city centre" },
|
||||
{ id: "Distance in km to city centre" },
|
||||
{ number: hotelData.location.distanceToCentre }
|
||||
)}
|
||||
</Caption>
|
||||
|
||||
@@ -71,19 +71,17 @@ export default function HotelCardListing({
|
||||
|
||||
return (
|
||||
<section className={styles.hotelCards}>
|
||||
{hotels?.length ? (
|
||||
hotels.map((hotel) => (
|
||||
<HotelCard
|
||||
key={hotel.hotelData.operaId}
|
||||
hotel={hotel}
|
||||
type={type}
|
||||
state={hotel.hotelData.name === activeCard ? "active" : "default"}
|
||||
onHotelCardHover={onHotelCardHover}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Title>No hotels found</Title>
|
||||
)}
|
||||
{hotels?.length
|
||||
? hotels.map((hotel) => (
|
||||
<HotelCard
|
||||
key={hotel.hotelData.operaId}
|
||||
hotel={hotel}
|
||||
type={type}
|
||||
state={hotel.hotelData.name === activeCard ? "active" : "default"}
|
||||
onHotelCardHover={onHotelCardHover}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function HotelSelectionHeader({
|
||||
</div>
|
||||
<Caption color="textMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Distance to city centre" },
|
||||
{ id: "Distance in km to city centre" },
|
||||
{ number: hotel.location.distanceToCentre }
|
||||
)}
|
||||
</Caption>
|
||||
|
||||
@@ -3,16 +3,30 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.facilities {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
margin-bottom: var(--Spacing-x3);
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.facilities:first-of-type {
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
}
|
||||
|
||||
.facilities ul {
|
||||
margin-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(min-content, max-content));
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
margin-bottom: var(--Spacing-x-one-and-half);
|
||||
margin-bottom: var(--Spacing-x1);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,26 +2,29 @@
|
||||
|
||||
import { usePathname, useSearchParams } from "next/navigation"
|
||||
import { useCallback, useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./hotelFilter.module.css"
|
||||
|
||||
import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
import type { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
|
||||
export default function HotelFilter({ filters }: HotelFiltersProps) {
|
||||
const intl = useIntl()
|
||||
const searchParams = useSearchParams()
|
||||
const pathname = usePathname()
|
||||
|
||||
const { watch, handleSubmit, getValues, register } = useForm<
|
||||
Record<string, boolean | undefined>
|
||||
>({
|
||||
const methods = useForm<Record<string, boolean | undefined>>({
|
||||
defaultValues: searchParams
|
||||
?.get("filters")
|
||||
?.split(",")
|
||||
.reduce((acc, curr) => ({ ...acc, [curr]: true }), {}),
|
||||
})
|
||||
const { watch, handleSubmit, getValues, register } = methods
|
||||
|
||||
const submitFilter = useCallback(() => {
|
||||
const newSearchParams = new URLSearchParams(searchParams)
|
||||
@@ -50,43 +53,42 @@ export default function HotelFilter({ filters }: HotelFiltersProps) {
|
||||
return () => subscription.unsubscribe()
|
||||
}, [handleSubmit, watch, submitFilter])
|
||||
|
||||
if (!filters.facilityFilters.length && !filters.surroundingsFilters.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className={styles.container}>
|
||||
<div className={styles.facilities}>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(submitFilter)}>
|
||||
{intl.formatMessage({ id: "Hotel facilities" })}
|
||||
<ul>
|
||||
{filters.facilityFilters.map((filter) => (
|
||||
<li key={`li-${filter.id}`} className={styles.filter}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`checkbox-${filter.id.toString()}`}
|
||||
{...register(filter.id.toString())}
|
||||
/>
|
||||
<label htmlFor={`checkbox-${filter.id.toString()}`}>
|
||||
{filter.name}
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Title as="h4">{intl.formatMessage({ id: "Filter by" })}</Title>
|
||||
<div className={styles.facilities}>
|
||||
<Subtitle>
|
||||
{intl.formatMessage({ id: "Hotel facilities" })}
|
||||
</Subtitle>
|
||||
<ul>
|
||||
{filters.facilityFilters.map((filter) => (
|
||||
<li key={`li-${filter.id}`} className={styles.filter}>
|
||||
<Checkbox name={filter.id.toString()}>{filter.name}</Checkbox>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{intl.formatMessage({ id: "Hotel surroundings" })}
|
||||
<ul>
|
||||
{filters.surroundingsFilters.map((filter) => (
|
||||
<li key={`li-${filter.id}`} className={styles.filter}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`checkbox-${filter.id.toString()}`}
|
||||
{...register(filter.id.toString())}
|
||||
/>
|
||||
<label htmlFor={`checkbox-${filter.id.toString()}`}>
|
||||
{filter.name}
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.facilities}>
|
||||
<Subtitle>
|
||||
{intl.formatMessage({ id: "Hotel surroundings" })}
|
||||
</Subtitle>
|
||||
<ul>
|
||||
{filters.surroundingsFilters.map((filter) => (
|
||||
<li key={`li-${filter.id}`} className={styles.filter}>
|
||||
<Checkbox name={filter.id.toString()}>{filter.name}</Checkbox>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
width: 339px;
|
||||
}
|
||||
@@ -6,24 +6,19 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import Select from "@/components/TempDesignSystem/Select"
|
||||
|
||||
import styles from "./hotelSorter.module.css"
|
||||
|
||||
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 intl = useIntl()
|
||||
|
||||
const onSelect = useCallback(
|
||||
(value: string | number) => {
|
||||
@@ -43,14 +38,30 @@ export default function HotelSorter() {
|
||||
},
|
||||
[pathname, searchParams]
|
||||
)
|
||||
const sortItems: SortItem[] = [
|
||||
{
|
||||
label: intl.formatMessage({ id: "Distance to city center" }),
|
||||
value: SortOrder.Distance,
|
||||
},
|
||||
{ label: intl.formatMessage({ id: "Name" }), value: SortOrder.Name },
|
||||
{ label: intl.formatMessage({ id: "Price" }), value: SortOrder.Price },
|
||||
{
|
||||
label: intl.formatMessage({ id: "TripAdvisor rating" }),
|
||||
value: SortOrder.TripAdvisorRating,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={sortItems}
|
||||
label={i18n.formatMessage({ id: "Sort by" })}
|
||||
name="sort"
|
||||
showRadioButton
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<Select
|
||||
items={sortItems}
|
||||
defaultSelectedKey={searchParams.get("sort") ?? DEFAULT_SORT}
|
||||
label={intl.formatMessage({ id: "Sort by" })}
|
||||
name="sort"
|
||||
showRadioButton
|
||||
discreet
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@ span.regular {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
span.discreet {
|
||||
color: var(--Base-Text-High-contrast);
|
||||
font-weight: 500;
|
||||
order: unset;
|
||||
}
|
||||
|
||||
input:active ~ .label,
|
||||
input:not(:placeholder-shown) ~ .label {
|
||||
display: block;
|
||||
|
||||
@@ -7,6 +7,7 @@ export const labelVariants = cva(styles.label, {
|
||||
size: {
|
||||
small: styles.small,
|
||||
regular: styles.regular,
|
||||
discreet: styles.discreet,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -2,10 +2,12 @@ import { ChevronDownIcon } from "@/components/Icons"
|
||||
|
||||
import styles from "./chevron.module.css"
|
||||
|
||||
export default function SelectChevron() {
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function SelectChevron(props: IconProps) {
|
||||
return (
|
||||
<span aria-hidden="true" className={styles.chevron}>
|
||||
<ChevronDownIcon color="grey80" width={20} height={20} />
|
||||
<ChevronDownIcon color="grey80" width={20} height={20} {...props} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export default function Select({
|
||||
value,
|
||||
maxHeight,
|
||||
showRadioButton = false,
|
||||
discreet = false,
|
||||
}: SelectProps) {
|
||||
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined)
|
||||
|
||||
@@ -53,7 +54,7 @@ export default function Select({
|
||||
<div className={styles.container} ref={setRef}>
|
||||
<ReactAriaSelect
|
||||
aria-label={ariaLabel}
|
||||
className={styles.select}
|
||||
className={`${styles.select} ${discreet && styles.discreet}`}
|
||||
defaultSelectedKey={defaultSelectedKey}
|
||||
name={name}
|
||||
onSelectionChange={handleOnSelect}
|
||||
@@ -63,12 +64,15 @@ export default function Select({
|
||||
<Body asChild fontOnly>
|
||||
<Button className={styles.input} data-testid={name}>
|
||||
<span className={styles.inputContentWrapper} tabIndex={tabIndex}>
|
||||
<Label required={required} size="small">
|
||||
<Label required={required} size={discreet ? "discreet" : "small"}>
|
||||
{label}
|
||||
{discreet && `:`}
|
||||
</Label>
|
||||
<SelectValue />
|
||||
</span>
|
||||
<SelectChevron />
|
||||
<SelectChevron
|
||||
{...(discreet ? { color: "baseButtonTextOnFillNormal" } : {})}
|
||||
/>
|
||||
</Button>
|
||||
</Body>
|
||||
<Body asChild fontOnly>
|
||||
|
||||
@@ -10,11 +10,29 @@
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.select[data-focused="true"] {
|
||||
.select[data-focused="true"],
|
||||
.select[data-focused="true"].discreet {
|
||||
border: 1px solid var(--Scandic-Blue-90);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.select.discreet {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.select.discreet .input {
|
||||
background-color: unset;
|
||||
color: var(--Base-Text-High-contrast);
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.select.discreet .inputContentWrapper {
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-direction: row;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input {
|
||||
align-items: center;
|
||||
background-color: var(--UI-Opacity-White-100);
|
||||
@@ -72,7 +90,7 @@
|
||||
}
|
||||
|
||||
.listBoxItem.showRadioButton:before {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
content: "";
|
||||
margin-right: var(--Spacing-x-one-and-half);
|
||||
background-color: white;
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface SelectProps
|
||||
value?: string | number
|
||||
maxHeight?: number
|
||||
showRadioButton?: boolean
|
||||
discreet?: boolean
|
||||
}
|
||||
|
||||
export type SelectPortalContainer = HTMLDivElement | undefined
|
||||
|
||||
Reference in New Issue
Block a user