feat: create ScheduleSearchPage with week day tabs and flight schedule display
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
export { ScheduleStartPage } from './schedule-start-page'
|
export { ScheduleStartPage } from './schedule-start-page'
|
||||||
|
export { ScheduleSearchPage } from './schedule-search-page'
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<ScheduleFlight[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [searchParams, setSearchParams] = useState<any>(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<string, any> = {
|
||||||
|
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 (
|
||||||
|
<div className="schedule-search-page" data-testid="schedule-search-page">
|
||||||
|
<PageTabs />
|
||||||
|
<PageEmptyList message={error} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="schedule-search-page" data-testid="schedule-search-page">
|
||||||
|
<PageTabs />
|
||||||
|
|
||||||
|
<PageLoader isLoading={loading} />
|
||||||
|
|
||||||
|
<PageLayout
|
||||||
|
contentLeft={<ScheduleFilter />}
|
||||||
|
stickyContent={
|
||||||
|
<div className="schedule-search-page__week-tabs" data-testid="week-tabs">
|
||||||
|
{DAYS_OF_WEEK.map((day) => (
|
||||||
|
<button
|
||||||
|
key={day}
|
||||||
|
className={`schedule-search-page__day-tab ${selectedDay === day ? 'active' : ''}`}
|
||||||
|
onClick={() => setSelectedDay(day)}
|
||||||
|
data-testid={`day-tab-${day}`}
|
||||||
|
>
|
||||||
|
{t(`SCHEDULE.DAY_${day}`)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="schedule-search-page__content">
|
||||||
|
{flights.length === 0 && !loading ? (
|
||||||
|
<PageEmptyList message="No flights scheduled for this day" />
|
||||||
|
) : (
|
||||||
|
<div className="schedule-search-page__flights">
|
||||||
|
{flights.map((flight) => (
|
||||||
|
<Card
|
||||||
|
key={flight.id}
|
||||||
|
className="schedule-search-page__flight-item"
|
||||||
|
data-testid={`schedule-flight-${flight.id}`}
|
||||||
|
>
|
||||||
|
<div className="schedule-search-page__flight">
|
||||||
|
<div className="schedule-search-page__flight-number">{flight.flightNumber}</div>
|
||||||
|
<div className="schedule-search-page__flight-times">
|
||||||
|
<div className="schedule-search-page__time">
|
||||||
|
<div className="schedule-search-page__time-label">Dep</div>
|
||||||
|
<div className="schedule-search-page__time-value">{flight.departure.time}</div>
|
||||||
|
</div>
|
||||||
|
<div className="schedule-search-page__time-arrow">→</div>
|
||||||
|
<div className="schedule-search-page__time">
|
||||||
|
<div className="schedule-search-page__time-label">Arr</div>
|
||||||
|
<div className="schedule-search-page__time-value">{flight.arrival.time}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="schedule-search-page__flight-route">
|
||||||
|
{flight.departure.airport} → {flight.arrival.airport}
|
||||||
|
</div>
|
||||||
|
{flight.aircraft && (
|
||||||
|
<div className="schedule-search-page__flight-aircraft">{flight.aircraft}</div>
|
||||||
|
)}
|
||||||
|
{flight.frequency && (
|
||||||
|
<div className="schedule-search-page__flight-frequency">
|
||||||
|
{flight.frequency.join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user