From 0dc6732947c3de54e05b8b5ac67038c9aa6e9688 Mon Sep 17 00:00:00 2001 From: gnezim Date: Sun, 5 Apr 2026 21:20:17 +0300 Subject: [PATCH] feat: create ScheduleSearchPage with week day tabs and flight schedule display --- .../src/app/features/schedule/pages/index.ts | 1 + .../schedule/pages/schedule-search-page.scss | 134 ++++++++++++++ .../schedule/pages/schedule-search-page.tsx | 166 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 apps/react/src/app/features/schedule/pages/schedule-search-page.scss create mode 100644 apps/react/src/app/features/schedule/pages/schedule-search-page.tsx diff --git a/apps/react/src/app/features/schedule/pages/index.ts b/apps/react/src/app/features/schedule/pages/index.ts index dbb7659d0..b8c804bb1 100644 --- a/apps/react/src/app/features/schedule/pages/index.ts +++ b/apps/react/src/app/features/schedule/pages/index.ts @@ -1 +1,2 @@ export { ScheduleStartPage } from './schedule-start-page' +export { ScheduleSearchPage } from './schedule-search-page' diff --git a/apps/react/src/app/features/schedule/pages/schedule-search-page.scss b/apps/react/src/app/features/schedule/pages/schedule-search-page.scss new file mode 100644 index 000000000..cfc36a199 --- /dev/null +++ b/apps/react/src/app/features/schedule/pages/schedule-search-page.scss @@ -0,0 +1,134 @@ +.schedule-search-page { + width: 100%; +} + +.schedule-search-page__content { + display: flex; + flex-direction: column; + gap: 16px; +} + +.schedule-search-page__week-tabs { + display: flex; + gap: 8px; + overflow-x: auto; + padding: 8px 0; + margin-bottom: 12px; + border-bottom: 1px solid #e0e0e0; + + &::-webkit-scrollbar { + height: 4px; + } + + &::-webkit-scrollbar-track { + background: #f5f5f5; + } + + &::-webkit-scrollbar-thumb { + background: #ccc; + border-radius: 2px; + } +} + +.schedule-search-page__day-tab { + padding: 8px 12px; + border: none; + background: none; + color: #666; + font-size: 13px; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + border-bottom: 3px solid transparent; + transition: all 0.2s ease; + + &:hover { + color: #333; + } + + &.active { + color: #1976d2; + border-bottom-color: #1976d2; + } +} + +.schedule-search-page__flights { + display: flex; + flex-direction: column; + gap: 12px; +} + +.schedule-search-page__flight-item { + padding: 16px; +} + +.schedule-search-page__flight { + display: grid; + grid-template-columns: 80px 1fr 120px; + gap: 20px; + align-items: start; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + gap: 12px; + } +} + +.schedule-search-page__flight-number { + font-weight: 700; + font-size: 18px; + color: #1976d2; +} + +.schedule-search-page__flight-times { + display: flex; + align-items: center; + gap: 12px; +} + +.schedule-search-page__time { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + min-width: 60px; +} + +.schedule-search-page__time-label { + font-size: 10px; + color: #999; + text-transform: uppercase; +} + +.schedule-search-page__time-value { + font-weight: 600; + font-size: 14px; + color: #333; +} + +.schedule-search-page__time-arrow { + color: #ccc; + font-size: 14px; +} + +.schedule-search-page__flight-route { + font-size: 13px; + color: #666; + margin-top: 4px; +} + +.schedule-search-page__flight-aircraft { + font-size: 12px; + color: #999; + margin-top: 4px; + padding: 4px 8px; + background: #f5f5f5; + border-radius: 3px; + display: inline-block; +} + +.schedule-search-page__flight-frequency { + font-size: 12px; + color: #999; + margin-top: 4px; +} diff --git a/apps/react/src/app/features/schedule/pages/schedule-search-page.tsx b/apps/react/src/app/features/schedule/pages/schedule-search-page.tsx new file mode 100644 index 000000000..cb4132a8f --- /dev/null +++ b/apps/react/src/app/features/schedule/pages/schedule-search-page.tsx @@ -0,0 +1,166 @@ +import React, { useState, useEffect } from 'react' +import { useParams } from 'react-router-dom' +import axios from 'axios' +import { PageLayout } from '@app/components/page-layout' +import { PageTabs } from '@app/components/page-tabs' +import { PageLoader } from '@app/components/page-loader' +import { PageEmptyList } from '@app/components/page-empty-list' +import { Card } from '@app/components/card' +import { useTranslation } from 'react-i18next' +import { ScheduleFilter } from '../components/schedule-filter' +import './schedule-search-page.scss' + +interface ScheduleFlight { + id: string + flightNumber: string + departure: { + airport: string + city: string + time: string + } + arrival: { + airport: string + city: string + time: string + } + aircraft?: string + frequency?: string[] + daysOfWeek?: string[] +} + +const DAYS_OF_WEEK = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] + +export const ScheduleSearchPage: React.FC = () => { + const { params: encodedParams } = useParams<{ params: string }>() + const { t } = useTranslation() + const [selectedDay, setSelectedDay] = useState('MON') + const [flights, setFlights] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [searchParams, setSearchParams] = useState(null) + + useEffect(() => { + if (!encodedParams) return + + try { + const decoded = JSON.parse(atob(encodedParams)) + setSearchParams(decoded) + } catch (err) { + console.error('Failed to decode params:', err) + setError('Invalid search parameters') + } + }, [encodedParams]) + + useEffect(() => { + if (!searchParams) return + + const fetchSchedules = async () => { + try { + setLoading(true) + setError(null) + + const queryParams: Record = { + departure: searchParams.departure, + arrival: searchParams.arrival, + day: selectedDay, + } + + if (searchParams.days && searchParams.days.length > 0) { + if (!searchParams.days.includes(selectedDay)) { + setFlights([]) + setLoading(false) + return + } + } + + const response = await axios.get('/api/schedule', { params: queryParams }) + setFlights(response.data?.flights || []) + } catch (err) { + console.error('Schedule fetch failed:', err) + setError('Failed to load schedule') + setFlights([]) + } finally { + setLoading(false) + } + } + + fetchSchedules() + }, [searchParams, selectedDay]) + + if (error) { + return ( +
+ + +
+ ) + } + + return ( +
+ + + + + } + stickyContent={ +
+ {DAYS_OF_WEEK.map((day) => ( + + ))} +
+ } + > +
+ {flights.length === 0 && !loading ? ( + + ) : ( +
+ {flights.map((flight) => ( + +
+
{flight.flightNumber}
+
+
+
Dep
+
{flight.departure.time}
+
+
+
+
Arr
+
{flight.arrival.time}
+
+
+
+ {flight.departure.airport} → {flight.arrival.airport} +
+ {flight.aircraft && ( +
{flight.aircraft}
+ )} + {flight.frequency && ( +
+ {flight.frequency.join(', ')} +
+ )} +
+
+ ))} +
+ )} +
+
+
+ ) +}