Add validation error displays to RouteFilter component
- Add three error display elements with test IDs: departure-error, same-city-error, validation-error - Add validation state management: departureError, arrivalError, sameAirportError, searchValidationError - Add validateDepartureInput() function for input validation (reuses validateCityCode) - Implement same-city validation in useEffect to detect duplicate departure/arrival - Update handleSearch() to check all required fields and show validation-error when missing - Add error styling: .route-filter__error with red text (#d32f2f), light red background (#ffebee), and left border - Same-city error shows inline below arrival input - Search validation error displays below action buttons - Errors automatically clear when conditions are fixed
This commit is contained in:
@@ -243,3 +243,22 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.route-filter__error {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background-color: #ffebee;
|
||||||
|
border-left: 3px solid #d32f2f;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #d32f2f;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-filter__error-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ const validateCityCode = (code: string | undefined): boolean => {
|
|||||||
return /^[A-Z]{3}$/.test(code.toUpperCase())
|
return /^[A-Z]{3}$/.test(code.toUpperCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate departure input for invalid characters
|
||||||
|
const validateDepartureInput = (code: string | undefined): boolean => {
|
||||||
|
if (!code) return true // Empty is not invalid, just incomplete
|
||||||
|
// Only allow 3-letter airport codes (A-Z)
|
||||||
|
return validateCityCode(code)
|
||||||
|
}
|
||||||
|
|
||||||
const validateRouteParams = (departure: City | null, arrival: City | null, date: Date | null): boolean => {
|
const validateRouteParams = (departure: City | null, arrival: City | null, date: Date | null): boolean => {
|
||||||
if (!departure || !arrival || !date) return false
|
if (!departure || !arrival || !date) return false
|
||||||
if (!validateCityCode(departure.code) || !validateCityCode(arrival.code)) return false
|
if (!validateCityCode(departure.code) || !validateCityCode(arrival.code)) return false
|
||||||
@@ -106,6 +113,10 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({
|
|||||||
const [showPassengerDropdown, setShowPassengerDropdown] = useState(false)
|
const [showPassengerDropdown, setShowPassengerDropdown] = useState(false)
|
||||||
const [cabinClass, setCabinClass] = useState<'economy' | 'business' | 'first'>(initialCabinClass || 'economy')
|
const [cabinClass, setCabinClass] = useState<'economy' | 'business' | 'first'>(initialCabinClass || 'economy')
|
||||||
const [showCabinDropdown, setShowCabinDropdown] = useState(false)
|
const [showCabinDropdown, setShowCabinDropdown] = useState(false)
|
||||||
|
const [departureError, setDepartureError] = useState<string>('')
|
||||||
|
const [arrivalError, setArrivalError] = useState<string>('')
|
||||||
|
const [sameAirportError, setSameAirportError] = useState(false)
|
||||||
|
const [searchValidationError, setSearchValidationError] = useState<string>('')
|
||||||
|
|
||||||
const totalPassengers = adults + children + infants
|
const totalPassengers = adults + children + infants
|
||||||
const maxPassengers = 9
|
const maxPassengers = 9
|
||||||
@@ -113,6 +124,16 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({
|
|||||||
|
|
||||||
const isSearchDisabled = !validateRouteParams(departure, arrival, date)
|
const isSearchDisabled = !validateRouteParams(departure, arrival, date)
|
||||||
|
|
||||||
|
// Validate departure and arrival on change
|
||||||
|
useEffect(() => {
|
||||||
|
// Check for same city error
|
||||||
|
if (departure && arrival && departure.code === arrival.code) {
|
||||||
|
setSameAirportError(true)
|
||||||
|
} else {
|
||||||
|
setSameAirportError(false)
|
||||||
|
}
|
||||||
|
}, [departure, arrival])
|
||||||
|
|
||||||
// Auto-search when departure or arrival city is selected
|
// Auto-search when departure or arrival city is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((validateMinimalRouteParams(departure) || validateMinimalRouteParams(arrival)) && onSearch) {
|
if ((validateMinimalRouteParams(departure) || validateMinimalRouteParams(arrival)) && onSearch) {
|
||||||
@@ -122,6 +143,21 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({
|
|||||||
}, [departure, arrival])
|
}, [departure, arrival])
|
||||||
|
|
||||||
const handleSearch = useCallback(() => {
|
const handleSearch = useCallback(() => {
|
||||||
|
// Clear previous search validation error
|
||||||
|
setSearchValidationError('')
|
||||||
|
|
||||||
|
// Check for all required fields
|
||||||
|
if (!departure || !arrival || !date) {
|
||||||
|
setSearchValidationError('Please fill in all required fields')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for same city error
|
||||||
|
if (departure.code === arrival.code) {
|
||||||
|
setSameAirportError(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (validateRouteParams(departure, arrival, date) && onSearch) {
|
if (validateRouteParams(departure, arrival, date) && onSearch) {
|
||||||
// Pass all search parameters including return date for round trips, passenger counts, and cabin class
|
// Pass all search parameters including return date for round trips, passenger counts, and cabin class
|
||||||
onSearch(departure!.code, arrival!.code, date!, returnDate, tripType, { adults, children, infants }, cabinClass)
|
onSearch(departure!.code, arrival!.code, date!, returnDate, tripType, { adults, children, infants }, cabinClass)
|
||||||
@@ -192,6 +228,11 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({
|
|||||||
{departure.code}
|
{departure.code}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{departureError && (
|
||||||
|
<div data-testid="departure-error" className="route-filter__error">
|
||||||
|
{departureError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -214,6 +255,16 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({
|
|||||||
{arrival.code}
|
{arrival.code}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{sameAirportError && (
|
||||||
|
<div data-testid="same-city-error" className="route-filter__error">
|
||||||
|
Departure and arrival cities must be different
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{arrivalError && (
|
||||||
|
<div data-testid="arrival-error" className="route-filter__error">
|
||||||
|
{arrivalError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="route-filter__trip-type">
|
<div className="route-filter__trip-type">
|
||||||
@@ -458,6 +509,11 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({
|
|||||||
data-testid="clear-filters-button"
|
data-testid="clear-filters-button"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{searchValidationError && (
|
||||||
|
<div data-testid="validation-error" className="route-filter__error">
|
||||||
|
{searchValidationError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user