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:
gnezim
2026-04-06 00:07:38 +03:00
parent 64c919afa0
commit 14bcc960b4
194 changed files with 331 additions and 68 deletions
@@ -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
View File
@@ -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
View File
@@ -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';
+1 -1
View File
@@ -2,7 +2,7 @@ import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://127.0.0.1:3001',
baseUrl: 'http://127.0.0.1:3003/components',
supportFile: 'cypress/support/index.ts',
specPattern: ['cypress/integration/**/*.cy.ts', 'cypress/integration/**/*.spec.ts'],
viewportWidth: 1440,
@@ -1,6 +1,6 @@
describe('Button Component', () => {
beforeEach(() => {
cy.visit('http://localhost:3001')
cy.visit('/')
})
it('should render button with text', () => {
@@ -3,7 +3,7 @@ import { dataHelpers } from '../../support/helpers/data-helpers'
describe('DatePicker Component Tests', () => {
beforeEach(() => {
cy.visit('http://localhost:3001')
cy.visit('/')
})
describe('DatePicker Display', () => {
@@ -2,7 +2,7 @@ import { uiHelpers } from '../../support/helpers/ui-helpers'
describe('Input Component Tests', () => {
beforeEach(() => {
cy.visit('http://localhost:3001')
cy.visit('/')
})
describe('Input Display', () => {
@@ -2,7 +2,7 @@ import { uiHelpers } from '../../support/helpers/ui-helpers'
describe('Modal Component Tests', () => {
beforeEach(() => {
cy.visit('http://localhost:3001')
cy.visit('/')
})
describe('Modal Display', () => {
@@ -2,7 +2,7 @@ import { uiHelpers } from '../../support/helpers/ui-helpers'
describe('Tabs Component Tests', () => {
beforeEach(() => {
cy.visit('http://localhost:3001')
cy.visit('/')
})
describe('Tabs Display', () => {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Some files were not shown because too many files have changed in this diff Show More