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,9 +174,30 @@ 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}
@@ -90,6 +207,7 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
data-testid="time-selector"
/>
<div className="route-filter__actions">
<Button
label={t('SHARED.SEARCH')}
icon="pi pi-search"
@@ -98,6 +216,13 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
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}
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 579 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 KiB

After

Width:  |  Height:  |  Size: 211 KiB

+44
View File
@@ -0,0 +1,44 @@
DevTools listening on ws://127.0.0.1:50234/devtools/browser/07717738-d524-4147-9900-ce7f220b5268
Missing baseUrl in compilerOptions. tsconfig-paths will be skipped
Opening `/dev/tty` failed (6): Device not configured
================================================================================
(Run Starting)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Cypress: 13.17.0 │
│ Browser: Chrome 146 (headless) │
│ Node Version: v22.22.2 (/opt/homebrew/Cellar/node@22/22.22.2/bin/node) │
│ Specs: 1 found (search-filter.cy.ts) │
│ Searched: cypress/integration/online-board/search-filter.cy.ts │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
────────────────────────────────────────────────────────────────────────────────────────────────────
Running: search-filter.cy.ts (1 of 1)
Online Board - Search & Filter
Happy Path - Search Functionality
1) should search flights by departure city
2) should search flights by arrival city
3) should search flights with round trip dates
4) should perform search with all filters applied
✓ should clear search filters (747ms)
Filter - Departure Cities
5) should filter by single departure city
6) should validate departure city input
7) should show autocomplete suggestions for departure
8) should select departure from autocomplete
Filter - Arrival Cities
9) should filter by single arrival city
10) should show different suggestions for arrival vs departure
11) should validate arrival city not same as departure
Filter - Passenger Count
12) should select single adult passenger
13) should add multiple adult passengers
14) should add children passengers
15) should add infant passengers