feat: create shared component stubs and fix SCSS circular imports
- Created 8 core shared components: Card, PageLayout, PageTabs, DayTabs, CalendarInput, TimeSelector, CityAutocomplete, SearchHistory, PageLoader, PageEmptyList - Fixed SCSS circular dependency: removed self-imports in components/modules-components/index.scss and modules-pages/index.scss - Updated page exports to use feature-based implementations - Added City interface to CityAutocomplete with mock city data - App now loads at localhost:3001 without SCSS errors
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
|
||||
interface CalendarInputProps {
|
||||
value?: Date
|
||||
onChange?: (date: Date) => void
|
||||
placeholder?: string
|
||||
className?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const CalendarInput: React.FC<CalendarInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Select date',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<input
|
||||
type="date"
|
||||
className={`calendar-input ${className}`}
|
||||
value={value ? value.toISOString().split('T')[0] : ''}
|
||||
onChange={e => onChange?.(new Date(e.target.value))}
|
||||
placeholder={placeholder}
|
||||
{...props}
|
||||
data-testid="calendar-input"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
|
||||
interface CardProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const Card: React.FC<CardProps> = ({ children, className = '', ...props }) => {
|
||||
return (
|
||||
<section className={`card ${className}`} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export interface City {
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface CityAutocompleteProps {
|
||||
value?: City | null
|
||||
onChange?: (city: City | null) => void
|
||||
placeholder?: string
|
||||
className?: string
|
||||
testId?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const CITIES: City[] = [
|
||||
{ code: 'SVO', name: 'Sheremetyevo' },
|
||||
{ code: 'LED', name: 'Pulkovo' },
|
||||
{ code: 'DME', name: 'Domodedovo' },
|
||||
{ code: 'VKO', name: 'Vnukovo' },
|
||||
{ code: 'SVX', name: 'Koltsovo' },
|
||||
]
|
||||
|
||||
export const CityAutocomplete: React.FC<CityAutocompleteProps> = ({
|
||||
value = null,
|
||||
onChange,
|
||||
placeholder = 'Select city',
|
||||
className = '',
|
||||
testId,
|
||||
...props
|
||||
}) => {
|
||||
const [input, setInput] = useState(value?.code || '')
|
||||
const [suggestions, setSuggestions] = useState<City[]>([])
|
||||
|
||||
const handleInputChange = (val: string) => {
|
||||
setInput(val)
|
||||
if (val.length > 0) {
|
||||
const filtered = CITIES.filter(c => c.code.includes(val.toUpperCase()) || c.name.toLowerCase().includes(val.toLowerCase()))
|
||||
setSuggestions(filtered)
|
||||
} else {
|
||||
setSuggestions([])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelect = (city: City) => {
|
||||
setInput(city.code)
|
||||
onChange?.(city)
|
||||
setSuggestions([])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`city-autocomplete ${className}`} {...props} data-testid={testId || 'city-autocomplete'}>
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={e => handleInputChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="city-autocomplete__input"
|
||||
/>
|
||||
{suggestions.length > 0 && (
|
||||
<ul className="city-autocomplete__suggestions">
|
||||
{suggestions.map(c => (
|
||||
<li key={c.code} onClick={() => handleSelect(c)}>
|
||||
{c.code} - {c.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
|
||||
interface DayTabsProps {
|
||||
selectedDate: Date
|
||||
onDateSelect: (date: Date) => void
|
||||
className?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const DayTabs: React.FC<DayTabsProps> = ({
|
||||
selectedDate,
|
||||
onDateSelect,
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const days = Array.from({ length: 7 }, (_, i) => {
|
||||
const date = new Date(selectedDate)
|
||||
date.setDate(date.getDate() + i - Math.floor(3))
|
||||
return date
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={`day-tabs ${className}`} {...props} data-testid="day-tabs">
|
||||
<div className="day-tabs__container">
|
||||
{days.map((date, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
className={`day-tabs__day ${date.toDateString() === selectedDate.toDateString() ? 'day-tabs__day--active' : ''}`}
|
||||
onClick={() => onDateSelect(date)}
|
||||
data-testid={`day-${date.getDate()}`}
|
||||
>
|
||||
{date.toLocaleDateString('ru-RU', { weekday: 'short', month: 'short', day: 'numeric' })}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
|
||||
interface PageEmptyListProps {
|
||||
message?: string
|
||||
icon?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const PageEmptyList: React.FC<PageEmptyListProps> = ({
|
||||
message = 'No results found',
|
||||
icon = 'pi-inbox',
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className="page-empty-list" {...props} data-testid="page-empty-list">
|
||||
<div className="page-empty-list__icon">📭</div>
|
||||
<h2 className="page-empty-list__title">No Results</h2>
|
||||
<p className="page-empty-list__message">{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
|
||||
interface PageLayoutProps {
|
||||
children: React.ReactNode
|
||||
contentLeft?: React.ReactNode
|
||||
stickyContent?: React.ReactNode
|
||||
className?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const PageLayout: React.FC<PageLayoutProps> = ({
|
||||
children,
|
||||
contentLeft,
|
||||
stickyContent,
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={`page-layout ${className}`} {...props}>
|
||||
{contentLeft && <aside className="page-layout__sidebar">{contentLeft}</aside>}
|
||||
<main className="page-layout__main">
|
||||
{stickyContent && <div className="page-layout__sticky">{stickyContent}</div>}
|
||||
<div className="page-layout__content">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
interface PageLoaderProps {
|
||||
isLoading?: boolean
|
||||
message?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const PageLoader: React.FC<PageLoaderProps> = ({ isLoading = false, message = 'Loading...', ...props }) => {
|
||||
if (!isLoading) return null
|
||||
|
||||
return (
|
||||
<div className="page-loader" {...props} data-testid="page-loader">
|
||||
<div className="page-loader__spinner" />
|
||||
<p className="page-loader__message">{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const PageTabs: React.FC = () => {
|
||||
const location = useLocation()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const tabs = [
|
||||
{ path: '/onlineboard', label: 'Online Board' },
|
||||
{ path: '/schedule', label: 'Schedule' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="page-tabs" data-testid="page-tabs">
|
||||
<nav className="page-tabs__nav">
|
||||
{tabs.map(tab => (
|
||||
<Link
|
||||
key={tab.path}
|
||||
to={tab.path}
|
||||
className={`page-tabs__link ${location.pathname.startsWith(tab.path) ? 'page-tabs__link--active' : ''}`}
|
||||
data-testid={`tab-${tab.path.substring(1)}`}
|
||||
>
|
||||
{tab.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
|
||||
interface SearchHistoryProps {
|
||||
items?: Array<{ id: string; label: string }>
|
||||
onSelect?: (item: any) => void
|
||||
className?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const SearchHistory: React.FC<SearchHistoryProps> = ({
|
||||
items = [],
|
||||
onSelect,
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={`search-history ${className}`} {...props} data-testid="search-history">
|
||||
<h3 className="search-history__title">Search History</h3>
|
||||
{items.length > 0 ? (
|
||||
<ul className="search-history__list">
|
||||
{items.map(item => (
|
||||
<li key={item.id} onClick={() => onSelect?.(item)}>
|
||||
{item.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="search-history__empty">No search history</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
|
||||
interface TimeSelectorProps {
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
onStartTimeChange?: (time: string) => void
|
||||
onEndTimeChange?: (time: string) => void
|
||||
className?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const TimeSelector: React.FC<TimeSelectorProps> = ({
|
||||
startTime = '00:00',
|
||||
endTime = '23:59',
|
||||
onStartTimeChange,
|
||||
onEndTimeChange,
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={`time-selector ${className}`} {...props} data-testid="time-selector">
|
||||
<input
|
||||
type="time"
|
||||
value={startTime}
|
||||
onChange={e => onStartTimeChange?.(e.target.value)}
|
||||
data-testid="start-time"
|
||||
/>
|
||||
<span className="time-selector__separator">-</span>
|
||||
<input
|
||||
type="time"
|
||||
value={endTime}
|
||||
onChange={e => onEndTimeChange?.(e.target.value)}
|
||||
data-testid="end-time"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1 @@
|
||||
import React from 'react'
|
||||
|
||||
export const FlightDetailsPage: React.FC = () => {
|
||||
return (
|
||||
<div className="flight-details-page">
|
||||
<h1>Flight Details</h1>
|
||||
<p>Individual flight details page</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export { FlightDetailsPage } from '../features/online-board/pages/flight-details-page'
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
import React from 'react'
|
||||
|
||||
export const OnlineBoardSearchPage: React.FC = () => {
|
||||
return (
|
||||
<div className="online-board-search-page">
|
||||
<h1>Search Results</h1>
|
||||
<p>Flight search results page</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export { OnlineBoardSearchPage } from '../features/online-board/pages/online-board-search-page'
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
import React from 'react'
|
||||
|
||||
export const OnlineBoardStartPage: React.FC = () => {
|
||||
return (
|
||||
<div className="online-board-start-page">
|
||||
<h1>Online Board</h1>
|
||||
<p>Flight search and filter interface</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export { OnlineBoardStartPage } from '../features/online-board/pages/online-board-start-page'
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
import React from 'react'
|
||||
|
||||
export const ScheduleFlightDetailsPage: React.FC = () => {
|
||||
return (
|
||||
<div className="schedule-flight-details-page">
|
||||
<h1>Schedule Flight Details</h1>
|
||||
<p>Flight details page for scheduled flights</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export { ScheduleFlightDetailsPage } from '../features/schedule/pages/schedule-flight-details-page'
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
import React from 'react'
|
||||
|
||||
export const ScheduleSearchPage: React.FC = () => {
|
||||
return (
|
||||
<div className="schedule-search-page">
|
||||
<h1>Schedule Search Results</h1>
|
||||
<p>Flight schedule search results page</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export { ScheduleSearchPage } from '../features/schedule/pages/schedule-search-page'
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
import React from 'react'
|
||||
|
||||
export const ScheduleStartPage: React.FC = () => {
|
||||
return (
|
||||
<div className="schedule-start-page">
|
||||
<h1>Schedule</h1>
|
||||
<p>Flight schedule search interface</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export { ScheduleStartPage } from '../features/schedule/pages/schedule-start-page'
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
@import './flight-status.component.scss';
|
||||
@import './flight-transition.component.layout.scss';
|
||||
@import './flight-transition.component.mobile-layout.scss';
|
||||
@import './index.scss';
|
||||
@import './last-update.component.scss';
|
||||
@import './link-to-old-version.component.scss';
|
||||
@import './note.component.scss';
|
||||
|
||||
@@ -50,6 +50,5 @@
|
||||
@import './error-page.component.scss';
|
||||
@import './flights-details-list-flight.component.scss';
|
||||
@import './flights-details-list-schedule-header.component.scss';
|
||||
@import './index.scss';
|
||||
@import './route-status.component.scss';
|
||||
@import './transfer.component.scss';
|
||||
|
||||
Reference in New Issue
Block a user