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:
gnezim
2026-04-06 03:13:17 +03:00
parent 1a3256147a
commit 4c1a1e0b66
35 changed files with 277 additions and 25 deletions
@@ -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}