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 { 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