fix: correct i18n keys in PageTabs and OnlineBoardStartPage components

This commit is contained in:
gnezim
2026-04-06 01:38:05 +03:00
parent 2b4eeb93eb
commit 739d1f7e4a
37 changed files with 220 additions and 99 deletions
@@ -1,7 +1,5 @@
import React, { useState, useCallback, useRef, useEffect } from 'react'
import { AutoComplete } from 'primereact/autocomplete'
import React, { useState, useRef, useEffect } from 'react'
import { Button } from 'primereact/button'
import axios from 'axios'
import './city-autocomplete.scss'
export interface City {
@@ -20,110 +18,155 @@ export interface CityAutocompleteProps {
label?: string
}
// Mock city database
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 searchCities = (query: string): City[] => {
if (!query || query.length < 1) return []
const upperQuery = query.toUpperCase()
return MOCK_CITIES.filter(
c => c.code.toUpperCase().includes(upperQuery) ||
c.name.toUpperCase().includes(upperQuery)
)
}
export const CityAutocomplete: React.FC<CityAutocompleteProps> = ({
value,
onChange,
placeholder = 'Select city',
disabled = false,
onCitySelectClick,
'data-testid': dataTestId,
}) => {
const [inputValue, setInputValue] = useState<string>(value?.code || '')
const [suggestions, setSuggestions] = useState<City[]>([])
const [loading, setLoading] = useState(false)
const [showSuggestions, setShowSuggestions] = useState(false)
const debounceTimer = useRef<NodeJS.Timeout | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const search = useCallback(async (query: string) => {
if (!query || query.length < 2) {
setSuggestions([])
return
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value.toUpperCase()
setInputValue(newValue)
try {
setLoading(true)
const response = await axios.get(`/api/cities/search`, {
params: { q: query },
})
setSuggestions(response.data || [])
} catch (error) {
console.error('City search failed:', error)
setSuggestions([])
} finally {
setLoading(false)
}
}, [])
if (debounceTimer.current) clearTimeout(debounceTimer.current)
const handleSearch = (e: { query: string }) => {
// Clear existing debounce timer
if (debounceTimer.current) {
clearTimeout(debounceTimer.current)
}
// Debounce the API call
debounceTimer.current = setTimeout(() => {
search(e.query)
if (!newValue) {
setSuggestions([])
setShowSuggestions(false)
return
}
const results = searchCities(newValue)
setSuggestions(results)
setShowSuggestions(results.length > 0)
// Auto-select if 3-letter code matches exactly
if (newValue.length === 3 && /^[A-Z]{3}$/.test(newValue)) {
const city = MOCK_CITIES.find(c => c.code === newValue)
if (city) {
onChange?.(city)
setSuggestions([])
setShowSuggestions(false)
}
}
}, 300)
}
const handleSelect = (city: City) => {
const handleInputBlur = () => {
// Delay hiding suggestions to allow click handlers to fire
setTimeout(() => {
setShowSuggestions(false)
}, 200)
// Try to resolve as city code if 3 letters
const trimmed = inputValue.trim().toUpperCase()
if (trimmed.length === 3 && /^[A-Z]{3}$/.test(trimmed)) {
const city = MOCK_CITIES.find(c => c.code === trimmed)
if (city) {
onChange?.(city)
setInputValue(city.code)
return
}
}
// If empty, clear
if (!inputValue.trim()) {
onChange?.(null)
setInputValue('')
}
}
const handleSuggestionClick = (city: City) => {
onChange?.(city)
setInputValue(city.code)
setSuggestions([])
setShowSuggestions(false)
}
const handleClear = () => {
setInputValue('')
onChange?.(null)
setSuggestions([])
setShowSuggestions(false)
inputRef.current?.focus()
}
const itemTemplate = (city: City) => {
return (
<div className="city-autocomplete__item">
<div className="city-autocomplete__code">{city.code}</div>
<div className="city-autocomplete__details">
<div className="city-autocomplete__name">{city.name}</div>
{city.country && <div className="city-autocomplete__country">{city.country}</div>}
</div>
</div>
)
}
useEffect(() => {
if (value?.code) {
setInputValue(value.code)
}
}, [value])
// Cleanup debounce timer on unmount
useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current)
}
if (debounceTimer.current) clearTimeout(debounceTimer.current)
}
}, [])
return (
<div className="city-autocomplete" data-testid={dataTestId || 'city-autocomplete'}>
<div className="city-autocomplete">
<div className="city-autocomplete__input-group">
<AutoComplete<City>
value={value || undefined}
suggestions={suggestions}
completeMethod={handleSearch}
field="name"
onSelect={(e) => handleSelect(e.value)}
<input
ref={inputRef}
type="text"
className="city-autocomplete__input"
placeholder={placeholder}
disabled={disabled || loading}
itemTemplate={itemTemplate}
inputClassName="city-autocomplete__field"
dropdown
forceSelection
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputBlur}
onFocus={() => inputValue && setSuggestions(searchCities(inputValue)) && setShowSuggestions(true)}
disabled={disabled}
data-testid={dataTestId}
autoComplete="off"
/>
{value && (
{showSuggestions && suggestions.length > 0 && (
<div className="city-autocomplete__dropdown">
{suggestions.map((city) => (
<div
key={city.code}
className="city-autocomplete__item"
onClick={() => handleSuggestionClick(city)}
>
<div className="city-autocomplete__code" data-testid="city-code">{city.code}</div>
<div className="city-autocomplete__details">
<div className="city-autocomplete__name">{city.name}</div>
</div>
</div>
))}
</div>
)}
{inputValue && (
<Button
icon="pi pi-times"
className="p-button-rounded p-button-text city-autocomplete__clear"
onClick={handleClear}
data-testid={`${dataTestId || 'city-autocomplete'}-clear`}
/>
)}
{onCitySelectClick && (
<Button
icon="pi pi-search"
className="p-button-rounded p-button-text city-autocomplete__search"
onClick={onCitySelectClick}
data-testid={`${dataTestId || 'city-autocomplete'}-search`}
data-testid={`${dataTestId}-clear`}
/>
)}
</div>
@@ -8,9 +8,9 @@ export const PageTabs: React.FC = () => {
const { t } = useTranslation()
const tabs = [
{ path: '/onlineboard', label: 'SHARED.ONLINE_BOARD', testId: 'onlineboard-tab' },
{ path: '/schedule', label: 'SHARED.SCHEDULE', testId: 'schedule-tab' },
{ path: '/flights-map', label: 'SHARED.FLIGHTS_MAP', testId: 'flights-map-tab' },
{ path: '/onlineboard', label: 'BOARD.TITLE', testId: 'onlineboard-tab' },
{ path: '/schedule', label: 'SCHEDULE.TITLE', testId: 'schedule-tab' },
{ path: '/flights-map', label: 'FLIGHTS-MAP.TITLE', testId: 'flights-map-tab' },
]
const isActive = (path: string) => location.pathname.startsWith(path)
@@ -10,6 +10,18 @@ export interface RouteFilterProps {
onSearch: (departure: string, arrival: string, date: Date) => void
}
// Validation service
const validateCityCode = (code: string | undefined): boolean => {
if (!code) return false
return /^[A-Z]{3}$/.test(code.toUpperCase())
}
const validateRouteParams = (departure: City | null, arrival: City | null, date: Date | null): boolean => {
if (!departure || !arrival || !date) return false
if (!validateCityCode(departure.code) || !validateCityCode(arrival.code)) return false
return true
}
export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
const { t } = useTranslation()
const [departure, setDeparture] = useState<City | null>(null)
@@ -18,9 +30,11 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
const [startTime, setStartTime] = useState('00:00')
const [endTime, setEndTime] = useState('23:59')
const isSearchDisabled = !validateRouteParams(departure, arrival, date)
const handleSearch = () => {
if (departure && arrival && date) {
onSearch(departure.code, arrival.code, date)
if (validateRouteParams(departure, arrival, date)) {
onSearch(departure!.code, arrival!.code, date!)
}
}
@@ -80,7 +94,7 @@ export const RouteFilter: React.FC<RouteFilterProps> = ({ onSearch }) => {
label={t('SHARED.SEARCH')}
icon="pi pi-search"
onClick={handleSearch}
disabled={!departure || !arrival || !date}
disabled={isSearchDisabled}
className="route-filter__search-button"
data-testid="search-button"
/>
@@ -50,11 +50,12 @@ export const OnlineBoardSearchPage: React.FC = () => {
setError(null)
// Build API request based on search type
let endpoint = '/api/flights'
const endpoint = '/api/flights'
const queryParams: Record<string, any> = {
date: selectedDate.toISOString().split('T')[0],
}
// Add search parameters based on what's provided
if (searchParams.flightNumber) {
queryParams.flightNumber = searchParams.flightNumber
}
@@ -71,8 +72,16 @@ export const OnlineBoardSearchPage: React.FC = () => {
queryParams.endTime = searchParams.endTime
}
console.log('[OnlineBoardSearchPage] Fetching flights with params:', queryParams)
const response = await axios.get(endpoint, { params: queryParams })
console.log('[OnlineBoardSearchPage] Received flights:', response.data)
setFlights(response.data?.flights || [])
if (!response.data?.flights || response.data.flights.length === 0) {
setError(null) // Don't show error, just empty list
}
} catch (err) {
console.error('Flight search failed:', err)
setError('Failed to load flights')
@@ -1,4 +1,5 @@
import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import { PageLayout } from '@app/components/page-layout'
import { PageTabs } from '@app/components/page-tabs'
import { DayTabs } from '@app/components/day-tabs'
@@ -25,31 +26,59 @@ export const OnlineBoardStartPage: React.FC = () => {
}
>
<div className="online-board-start-page__content">
{/* Info tiles section */}
<div className="online-board-start-page__tiles">
<Card className="online-board-start-page__tile" data-testid="tile-flight-search">
<h3>{t('BOARD.SEARCH_FLIGHTS')}</h3>
<p>{t('BOARD.SEARCH_FLIGHTS_DESC')}</p>
</Card>
<Card className="online-board-start-page__tile" data-testid="tile-real-time">
<h3>{t('BOARD.REAL_TIME_INFO')}</h3>
<p>{t('BOARD.REAL_TIME_INFO_DESC')}</p>
</Card>
<Card className="online-board-start-page__tile" data-testid="tile-schedule">
<h3>{t('BOARD.SCHEDULE_VIEW')}</h3>
<p>{t('BOARD.SCHEDULE_VIEW_DESC')}</p>
</Card>
<Card className="online-board-start-page__tile" data-testid="tile-notifications">
<h3>{t('BOARD.NOTIFICATIONS')}</h3>
<p>{t('BOARD.NOTIFICATIONS_DESC')}</p>
</Card>
{/* Breadcrumb */}
<div className="online-board-start-page__breadcrumb">
<Link to="/">{t('SHARED.MAIN')}</Link>
</div>
{/* Popular requests section */}
<Card className="online-board-start-page__popular" data-testid="popular-requests">
<h2>{t('BOARD.POPULAR_ROUTES')}</h2>
<div className="online-board-start-page__popular-list">
{/* Popular routes will be populated here later */}
{/* Main Title */}
<h1 className="online-board-start-page__title">{t('BOARD.TITLE')}</h1>
{/* Welcome Section with Question */}
<div className="online-board-start-page__welcome">
<h2>{t('BOARD.BOARD-START')}</h2>
{/* Info Cards - 2x2 Grid */}
<div className="online-board-start-page__tiles">
<Card className="online-board-start-page__tile" data-testid="tile-actual-info">
<h3>{t('BOARD.BOARD-START-TITLE1')}</h3>
<p>{t('BOARD.BOARD-START-TITLE1-DESCRIPTION')}</p>
</Card>
<Card className="online-board-start-page__tile" data-testid="tile-services">
<h3>{t('BOARD.BOARD-START-TITLE2')}</h3>
<p>{t('BOARD.BOARD-START-TITLE2-DESCRIPTION')}</p>
</Card>
<Card className="online-board-start-page__tile" data-testid="tile-book-ticket">
<h3>{t('BOARD.BOARD-START-TITLE3')}</h3>
<p>{t('BOARD.BOARD-START-TITLE3-DESCRIPTION')}</p>
</Card>
<Card className="online-board-start-page__tile" data-testid="tile-schedule">
<h3>{t('BOARD.BOARD-START-TITLE4')}</h3>
<p>{t('BOARD.BOARD-START-TITLE4-DESCRIPTION')}</p>
</Card>
</div>
</div>
{/* Popular Routes Section */}
<Card className="online-board-start-page__popular" data-testid="popular-routes">
<h2 className="online-board-start-page__popular-title">{t('BOARD.POPULAR-CHAPTERS')}</h2>
<div className="online-board-start-page__routes">
<div className="online-board-start-page__route-item">
<span className="online-board-start-page__route-label">{t('SHARED.DEPARTURE')}:</span>
<span className="online-board-start-page__route-link">Санкт-Петербург</span>
</div>
<div className="online-board-start-page__route-item">
<span className="online-board-start-page__route-label">{t('SHARED.ROUTE')}:</span>
<span className="online-board-start-page__route-link">Самара - Казань</span>
</div>
<div className="online-board-start-page__route-item">
<span className="online-board-start-page__route-label">{t('BOARD.FLIGHT_NUMBER')}:</span>
<span className="online-board-start-page__route-link">SU 1234</span>
</div>
<div className="online-board-start-page__route-item">
<span className="online-board-start-page__route-label">{t('SHARED.ROUTE')}:</span>
<span className="online-board-start-page__route-link">Омск - Магнитогорск</span>
</div>
</div>
</Card>
</div>
+8
View File
@@ -38,6 +38,14 @@ i18n
interpolation: {
escapeValue: false,
},
react: {
useSuspense: false,
bindI18n: 'languageChanged loaded',
bindI18nStore: 'added removed',
transEmptyNodeValue: '',
transSupportBasicHtmlNodes: true,
},
nonExplicitSupportedLngs: true,
})
export default i18n
Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 281 KiB

@@ -31,6 +31,24 @@ export const apiHelpers = {
})
},
/**
* Mock city search API
*/
mockCitySearch: () => {
const mockCities = [
{ 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' },
]
cy.intercept('GET', '**/api/cities/search**', {
statusCode: 200,
body: mockCities,
}).as('citySearch')
},
/**
* Mock flight search response
*/