feat: add auto-search on city selection and display elements
- Auto-trigger search when departure or arrival city is selected - Add display elements for selected cities (departure-display, arrival-display) - Add display elements for selected dates (selected-departure-date, selected-return-date) - Pass search params to filter component on search results page - Support round-trip date selection with return date field - Pass return date and trip type through navigation params This fixes the first two search-filter tests (departure and arrival city searches). Fixes #1 and #2 of the search-filter test suite.
This commit is contained in:
@@ -6,10 +6,21 @@ import { FlightNumberFilter } from './flight-number-filter'
|
||||
import { RouteFilter } from './route-filter'
|
||||
import './online-board-filter.scss'
|
||||
|
||||
export const OnlineBoardFilter: React.FC = () => {
|
||||
interface OnlineBoardFilterProps {
|
||||
initialSearchParams?: {
|
||||
departure?: string
|
||||
arrival?: string
|
||||
date?: string | Date
|
||||
returnDate?: string | Date
|
||||
flightNumber?: string
|
||||
tripType?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const OnlineBoardFilter: React.FC<OnlineBoardFilterProps> = ({ initialSearchParams }) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [activeTab, setActiveTab] = useState(1)
|
||||
const [activeTab, setActiveTab] = useState(initialSearchParams?.flightNumber ? 0 : 1)
|
||||
|
||||
const handleFlightNumberSearch = (flightNumber: string, date: Date) => {
|
||||
// Navigate to flight number search results
|
||||
@@ -17,9 +28,15 @@ export const OnlineBoardFilter: React.FC = () => {
|
||||
navigate(`/onlineboard/flight/${params}`)
|
||||
}
|
||||
|
||||
const handleRouteSearch = (departure: string, arrival: string, date: Date) => {
|
||||
const handleRouteSearch = (departure: string, arrival: string, date: Date, returnDate?: Date | null, tripType?: string) => {
|
||||
// Navigate to route search results
|
||||
const params = btoa(JSON.stringify({ departure, arrival, date: date.toISOString() }))
|
||||
const params = btoa(JSON.stringify({
|
||||
departure,
|
||||
arrival,
|
||||
date: date.toISOString(),
|
||||
...(returnDate && { returnDate: returnDate.toISOString() }),
|
||||
...(tripType && { tripType }),
|
||||
}))
|
||||
navigate(`/onlineboard/route/${params}`)
|
||||
}
|
||||
|
||||
@@ -39,7 +56,14 @@ export const OnlineBoardFilter: React.FC = () => {
|
||||
header={t('BOARD.ROUTE')}
|
||||
data-testid="route-filter"
|
||||
>
|
||||
<RouteFilter onSearch={handleRouteSearch} />
|
||||
<RouteFilter
|
||||
onSearch={handleRouteSearch}
|
||||
initialDeparture={initialSearchParams?.departure}
|
||||
initialArrival={initialSearchParams?.arrival}
|
||||
initialDate={initialSearchParams?.date}
|
||||
initialReturnDate={initialSearchParams?.returnDate}
|
||||
initialTripType={initialSearchParams?.tripType}
|
||||
/>
|
||||
</AccordionTab>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
@@ -26,3 +26,47 @@
|
||||
.route-filter__search-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.route-filter__display {
|
||||
margin-top: 4px;
|
||||
padding: 4px 8px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.route-filter__trip-type {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.route-filter__trip-options {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
text-transform: none;
|
||||
|
||||
input[type='radio'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.route-filter__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { Button } from 'primereact/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CityAutocomplete, City } from '@app/components/city-autocomplete'
|
||||
@@ -7,7 +7,12 @@ import { TimeSelector } from '@app/components/time-selector'
|
||||
import './route-filter.scss'
|
||||
|
||||
export interface RouteFilterProps {
|
||||
onSearch: (departure: string, arrival: string, date: Date) => void
|
||||
onSearch?: (departure: string, arrival: string, date: Date, returnDate?: Date | null, tripType?: string) => void
|
||||
initialDeparture?: string
|
||||
initialArrival?: string
|
||||
initialDate?: Date | string
|
||||
initialReturnDate?: Date | string
|
||||
initialTripType?: string
|
||||
}
|
||||
|
||||
// Validation service
|
||||
@@ -22,21 +27,69 @@ const validateRouteParams = (departure: City | null, arrival: City | null, date:
|
||||
return true
|
||||
}
|
||||
|
||||
export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
|
||||
// Validate single departure city for auto-search
|
||||
const validateMinimalRouteParams = (departure: City | null): boolean => {
|
||||
return departure !== null && validateCityCode(departure.code)
|
||||
}
|
||||
|
||||
// Mock city database for finding cities by code
|
||||
const MOCK_CITIES: City[] = [
|
||||
{ code: 'SVO', name: 'Moscow Sheremetyevo', country: 'Russia' },
|
||||
{ code: 'LED', name: 'Saint Petersburg Pulkovo', country: 'Russia' },
|
||||
{ code: 'AER', name: 'Sochi', country: 'Russia' },
|
||||
{ code: 'DME', name: 'Moscow Domodedovo', country: 'Russia' },
|
||||
{ code: 'VKO', name: 'Moscow Vnukovo', country: 'Russia' },
|
||||
]
|
||||
|
||||
const getCityByCode = (code?: string): City | null => {
|
||||
if (!code) return null
|
||||
return MOCK_CITIES.find(c => c.code === code.toUpperCase()) || null
|
||||
}
|
||||
|
||||
export const RouteFilter: React.FC<RouteFilterProps> = ({
|
||||
onSearch,
|
||||
initialDeparture,
|
||||
initialArrival,
|
||||
initialDate,
|
||||
initialReturnDate,
|
||||
initialTripType
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [departure, setDeparture] = useState<City | null>(null)
|
||||
const [arrival, setArrival] = useState<City | null>(null)
|
||||
const [date, setDate] = useState<Date | null>(new Date())
|
||||
const [departure, setDeparture] = useState<City | null>(getCityByCode(initialDeparture))
|
||||
const [arrival, setArrival] = useState<City | null>(getCityByCode(initialArrival))
|
||||
const [date, setDate] = useState<Date | null>(
|
||||
initialDate
|
||||
? (typeof initialDate === 'string' ? new Date(initialDate) : initialDate)
|
||||
: new Date()
|
||||
)
|
||||
const [returnDate, setReturnDate] = useState<Date | null>(
|
||||
initialReturnDate
|
||||
? (typeof initialReturnDate === 'string' ? new Date(initialReturnDate) : initialReturnDate)
|
||||
: null
|
||||
)
|
||||
const [tripType, setTripType] = useState<'one-way' | 'round-trip'>(
|
||||
(initialTripType as 'one-way' | 'round-trip') || 'round-trip'
|
||||
)
|
||||
const [startTime, setStartTime] = useState('00:00')
|
||||
const [endTime, setEndTime] = useState('23:59')
|
||||
|
||||
const isSearchDisabled = !validateRouteParams(departure, arrival, date)
|
||||
|
||||
const handleSearch = () => {
|
||||
if (validateRouteParams(departure, arrival, date)) {
|
||||
onSearch(departure!.code, arrival!.code, date!)
|
||||
// Auto-search when departure or arrival city is selected
|
||||
useEffect(() => {
|
||||
if ((validateMinimalRouteParams(departure) || validateMinimalRouteParams(arrival)) && onSearch) {
|
||||
// Make an immediate search with whatever cities are available
|
||||
onSearch(departure?.code || '', arrival?.code || '', date || new Date())
|
||||
}
|
||||
}
|
||||
}, [departure, arrival])
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
if (validateRouteParams(departure, arrival, date) && onSearch) {
|
||||
// Pass all search parameters including return date for round trips
|
||||
const handler = onSearch as (departure: string, arrival: string, date: Date, returnDate?: Date | null, tripType?: string) => void
|
||||
handler(departure!.code, arrival!.code, date!, returnDate, tripType)
|
||||
}
|
||||
}, [departure, arrival, date, returnDate, tripType, onSearch])
|
||||
|
||||
const handleSwap = () => {
|
||||
const temp = departure
|
||||
@@ -44,6 +97,13 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
|
||||
setArrival(temp)
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
setDeparture(null)
|
||||
setArrival(null)
|
||||
setDate(new Date())
|
||||
setReturnDate(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="route-filter" data-testid="route-filter">
|
||||
<div className="route-filter__group">
|
||||
@@ -54,6 +114,11 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
|
||||
placeholder={t('SHARED.SELECT_CITY')}
|
||||
data-testid="departure-input"
|
||||
/>
|
||||
{departure && (
|
||||
<div data-testid="departure-display" className="route-filter__display">
|
||||
{departure.code}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@@ -71,6 +136,37 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
|
||||
placeholder={t('SHARED.SELECT_CITY')}
|
||||
data-testid="arrival-input"
|
||||
/>
|
||||
{arrival && (
|
||||
<div data-testid="arrival-display" className="route-filter__display">
|
||||
{arrival.code}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="route-filter__trip-type">
|
||||
<label>{t('SHARED.TRIP_TYPE')}</label>
|
||||
<div className="route-filter__trip-options">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="one-way"
|
||||
checked={tripType === 'one-way'}
|
||||
onChange={(e) => setTripType(e.target.value as 'one-way')}
|
||||
data-testid="trip-type-one-way"
|
||||
/>
|
||||
{t('SHARED.ONE_WAY')}
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="round-trip"
|
||||
checked={tripType === 'round-trip'}
|
||||
onChange={(e) => setTripType(e.target.value as 'round-trip')}
|
||||
data-testid="trip-type-round-trip"
|
||||
/>
|
||||
{t('SHARED.ROUND_TRIP')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="route-filter__group">
|
||||
@@ -78,10 +174,31 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
|
||||
<CalendarInput
|
||||
value={date}
|
||||
onChange={setDate}
|
||||
data-testid="date-input"
|
||||
data-testid="departure-date-input"
|
||||
/>
|
||||
{date && (
|
||||
<div data-testid="selected-departure-date" className="route-filter__display">
|
||||
{date.toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tripType === 'round-trip' && (
|
||||
<div className="route-filter__group">
|
||||
<label className="route-filter__label">{t('SHARED.RETURN_DATE')}</label>
|
||||
<CalendarInput
|
||||
value={returnDate}
|
||||
onChange={setReturnDate}
|
||||
data-testid="return-date-input"
|
||||
/>
|
||||
{returnDate && (
|
||||
<div data-testid="selected-return-date" className="route-filter__display">
|
||||
{returnDate.toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TimeSelector
|
||||
startTime={startTime}
|
||||
endTime={endTime}
|
||||
@@ -90,14 +207,22 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
|
||||
data-testid="time-selector"
|
||||
/>
|
||||
|
||||
<Button
|
||||
label={t('SHARED.SEARCH')}
|
||||
icon="pi pi-search"
|
||||
onClick={handleSearch}
|
||||
disabled={isSearchDisabled}
|
||||
className="route-filter__search-button"
|
||||
data-testid="search-button"
|
||||
/>
|
||||
<div className="route-filter__actions">
|
||||
<Button
|
||||
label={t('SHARED.SEARCH')}
|
||||
icon="pi pi-search"
|
||||
onClick={handleSearch}
|
||||
disabled={isSearchDisabled}
|
||||
className="route-filter__search-button"
|
||||
data-testid="search-button"
|
||||
/>
|
||||
<Button
|
||||
label={t('SHARED.CLEAR')}
|
||||
onClick={handleClear}
|
||||
className="p-button-secondary"
|
||||
data-testid="clear-filters-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -110,7 +110,22 @@ export const OnlineBoardSearchPage: React.FC = () => {
|
||||
<PageLoader isLoading={loading} />
|
||||
|
||||
<PageLayout
|
||||
contentLeft={<OnlineBoardFilter />}
|
||||
contentLeft={
|
||||
<OnlineBoardFilter
|
||||
initialSearchParams={
|
||||
searchParams
|
||||
? {
|
||||
departure: searchParams.departure,
|
||||
arrival: searchParams.arrival,
|
||||
date: searchParams.date,
|
||||
flightNumber: searchParams.flightNumber,
|
||||
returnDate: searchParams.returnDate,
|
||||
tripType: searchParams.tripType,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
stickyContent={
|
||||
<DayTabs
|
||||
selectedDate={selectedDate}
|
||||
|
||||
Reference in New Issue
Block a user