feat: create OnlineBoardFilter with FlightNumber and Route filters
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
.flight-number-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.flight-number-filter__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flight-number-filter__label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.flight-number-filter__input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.flight-number-filter__prefix {
|
||||
padding: 8px 12px;
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
background: #f5f5f5;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.flight-number-filter__input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
.flight-number-filter__search-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from 'primereact/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CalendarInput } from '@app/components/calendar-input'
|
||||
import './flight-number-filter.scss'
|
||||
|
||||
export interface FlightNumberFilterProps {
|
||||
onSearch: (flightNumber: string, date: Date) => void
|
||||
}
|
||||
|
||||
export const FlightNumberFilter: React.FC<FlightNumberFilterProps> = ({ onSearch }) => {
|
||||
const { t } = useTranslation()
|
||||
const [flightNumber, setFlightNumber] = useState('')
|
||||
const [date, setDate] = useState<Date | null>(new Date())
|
||||
|
||||
const handleSearch = () => {
|
||||
if (flightNumber && date) {
|
||||
onSearch(flightNumber, date)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
setFlightNumber('')
|
||||
setDate(new Date())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flight-number-filter" data-testid="flight-number-filter">
|
||||
<div className="flight-number-filter__group">
|
||||
<label className="flight-number-filter__label">{t('BOARD.FLIGHT_NUMBER')}</label>
|
||||
<div className="flight-number-filter__input-group">
|
||||
<span className="flight-number-filter__prefix">SU</span>
|
||||
<input
|
||||
type="text"
|
||||
className="flight-number-filter__input"
|
||||
placeholder="123"
|
||||
value={flightNumber}
|
||||
onChange={(e) => setFlightNumber(e.target.value.toUpperCase())}
|
||||
data-testid="flight-number-input"
|
||||
maxLength={4}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
className="p-button-rounded p-button-text"
|
||||
onClick={handleClear}
|
||||
data-testid="flight-number-clear-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flight-number-filter__group">
|
||||
<label className="flight-number-filter__label">{t('SHARED.DATE')}</label>
|
||||
<CalendarInput
|
||||
value={date}
|
||||
onChange={setDate}
|
||||
data-testid="flight-number-calendar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
label={t('SHARED.SEARCH')}
|
||||
icon="pi pi-search"
|
||||
onClick={handleSearch}
|
||||
disabled={!flightNumber || !date}
|
||||
className="flight-number-filter__search-button"
|
||||
data-testid="flight-number-search-button"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export { OnlineBoardFilter } from './online-board-filter'
|
||||
export { FlightNumberFilter } from './flight-number-filter'
|
||||
export { RouteFilter } from './route-filter'
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
.online-board-filter {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
|
||||
:global {
|
||||
.p-accordion {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
|
||||
.p-accordion-header {
|
||||
padding: 0;
|
||||
|
||||
a {
|
||||
padding: 12px 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.p-accordion-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,47 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { Accordion, AccordionTab } from 'primereact/accordion'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FlightNumberFilter } from './flight-number-filter'
|
||||
import { RouteFilter } from './route-filter'
|
||||
import './online-board-filter.scss'
|
||||
|
||||
export interface OnlineBoardFilterProps {
|
||||
'data-testid'?: string
|
||||
}
|
||||
export const OnlineBoardFilter: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
|
||||
const handleFlightNumberSearch = (flightNumber: string, date: Date) => {
|
||||
// Navigate to flight number search results
|
||||
const params = btoa(JSON.stringify({ flightNumber, date: date.toISOString() }))
|
||||
navigate(`/onlineboard/flight/${params}`)
|
||||
}
|
||||
|
||||
const handleRouteSearch = (departure: string, arrival: string, date: Date) => {
|
||||
// Navigate to route search results
|
||||
const params = btoa(JSON.stringify({ departure, arrival, date: date.toISOString() }))
|
||||
navigate(`/onlineboard/route/${params}`)
|
||||
}
|
||||
|
||||
export const OnlineBoardFilter: React.FC<OnlineBoardFilterProps> = ({ 'data-testid': dataTestId }) => {
|
||||
return (
|
||||
<div className="online-board-filter" data-testid={dataTestId || 'online-board-filter'}>
|
||||
{/* Filter component will be implemented in the next task */}
|
||||
<div className="online-board-filter" data-testid="online-board-filter">
|
||||
<Accordion activeIndex={activeTab} onTabChange={(e) => {
|
||||
const index = typeof e.index === 'number' ? e.index : (Array.isArray(e.index) ? e.index[0] : 0)
|
||||
setActiveTab(index)
|
||||
}}>
|
||||
<AccordionTab
|
||||
header={t('BOARD.FLIGHT_NUMBER')}
|
||||
data-testid="flight-filter"
|
||||
>
|
||||
<FlightNumberFilter onSearch={handleFlightNumberSearch} />
|
||||
</AccordionTab>
|
||||
<AccordionTab
|
||||
header={t('BOARD.ROUTE')}
|
||||
data-testid="route-filter"
|
||||
>
|
||||
<RouteFilter onSearch={handleRouteSearch} />
|
||||
</AccordionTab>
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
.route-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.route-filter__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.route-filter__label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.route-filter__swap {
|
||||
align-self: center;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.route-filter__search-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from 'primereact/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CityAutocomplete, City } from '@app/components/city-autocomplete'
|
||||
import { CalendarInput } from '@app/components/calendar-input'
|
||||
import { TimeSelector } from '@app/components/time-selector'
|
||||
import './route-filter.scss'
|
||||
|
||||
export interface RouteFilterProps {
|
||||
onSearch: (departure: string, arrival: string, date: Date) => void
|
||||
}
|
||||
|
||||
export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
|
||||
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 [startTime, setStartTime] = useState('00:00')
|
||||
const [endTime, setEndTime] = useState('23:59')
|
||||
|
||||
const handleSearch = () => {
|
||||
if (departure && arrival && date) {
|
||||
onSearch(departure.code, arrival.code, date)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSwap = () => {
|
||||
const temp = departure
|
||||
setDeparture(arrival)
|
||||
setArrival(temp)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="route-filter" data-testid="route-filter">
|
||||
<div className="route-filter__group">
|
||||
<label className="route-filter__label">{t('SHARED.DEPARTURE_CITY')}</label>
|
||||
<CityAutocomplete
|
||||
value={departure}
|
||||
onChange={setDeparture}
|
||||
placeholder={t('SHARED.SELECT_CITY')}
|
||||
data-testid="route-departure-city-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon="pi pi-arrow-right"
|
||||
className="p-button-rounded p-button-text route-filter__swap"
|
||||
onClick={handleSwap}
|
||||
data-testid="route-swap-button"
|
||||
/>
|
||||
|
||||
<div className="route-filter__group">
|
||||
<label className="route-filter__label">{t('SHARED.ARRIVAL_CITY')}</label>
|
||||
<CityAutocomplete
|
||||
value={arrival}
|
||||
onChange={setArrival}
|
||||
placeholder={t('SHARED.SELECT_CITY')}
|
||||
data-testid="route-arrival-city-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="route-filter__group">
|
||||
<label className="route-filter__label">{t('SHARED.DATE')}</label>
|
||||
<CalendarInput
|
||||
value={date}
|
||||
onChange={setDate}
|
||||
data-testid="route-calendar-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TimeSelector
|
||||
startTime={startTime}
|
||||
endTime={endTime}
|
||||
onStartTimeChange={setStartTime}
|
||||
onEndTimeChange={setEndTime}
|
||||
data-testid="route-time-selector"
|
||||
/>
|
||||
|
||||
<Button
|
||||
label={t('SHARED.SEARCH')}
|
||||
icon="pi pi-search"
|
||||
onClick={handleSearch}
|
||||
disabled={!departure || !arrival || !date}
|
||||
className="route-filter__search-button"
|
||||
data-testid="route-search-button"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user