feat: create FlightDetailsPage with full flight information
This commit is contained in:
@@ -0,0 +1,180 @@
|
|||||||
|
.flight-details-page {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__mini-list {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__header {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__header-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__header-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__header-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__header-value {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__details {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__crew {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__crew-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__crew-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__crew-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__seats {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__seats-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__seats-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__seats-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__seats-value {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__amenities {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__amenities-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-details-page__amenity-tag {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1565c0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Button } from 'primereact/button'
|
||||||
|
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 { BoardFlightBody } from '../components/board-flight-body'
|
||||||
|
import './flight-details-page.scss'
|
||||||
|
|
||||||
|
interface FlightDetails {
|
||||||
|
id: string
|
||||||
|
flightNumber: string
|
||||||
|
status: string
|
||||||
|
operator?: string
|
||||||
|
aircraft?: string
|
||||||
|
departure: any
|
||||||
|
arrival: any
|
||||||
|
boarding?: any
|
||||||
|
deboarding?: any
|
||||||
|
codesharing?: string[]
|
||||||
|
amenities?: string[]
|
||||||
|
crew?: {
|
||||||
|
captain?: string
|
||||||
|
firstOfficer?: string
|
||||||
|
flightAttendants?: number
|
||||||
|
}
|
||||||
|
seats?: {
|
||||||
|
total: number
|
||||||
|
available: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlightDetailsPage: React.FC = () => {
|
||||||
|
const { params: encodedParams } = useParams<{ params: string }>()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [flight, setFlight] = useState<FlightDetails | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!encodedParams) return
|
||||||
|
|
||||||
|
const fetchFlightDetails = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
const decoded = JSON.parse(atob(encodedParams))
|
||||||
|
const flightId = decoded.id
|
||||||
|
|
||||||
|
// Fetch flight details from API
|
||||||
|
const response = await axios.get(`/api/flights/${flightId}`)
|
||||||
|
setFlight(response.data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load flight details:', err)
|
||||||
|
setError('Failed to load flight information')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchFlightDetails()
|
||||||
|
}, [encodedParams])
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <PageLoader isLoading={true} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !flight) {
|
||||||
|
return (
|
||||||
|
<div className="flight-details-page" data-testid="flight-details-page">
|
||||||
|
<PageTabs />
|
||||||
|
<PageEmptyList message={error || 'Flight not found'} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flight-details-page" data-testid="flight-details-page">
|
||||||
|
<PageTabs />
|
||||||
|
|
||||||
|
<PageLayout
|
||||||
|
headerLeft={
|
||||||
|
<Button
|
||||||
|
icon="pi pi-arrow-left"
|
||||||
|
label={t('SHARED.BACK')}
|
||||||
|
onClick={handleBack}
|
||||||
|
className="p-button-text"
|
||||||
|
data-testid="back-button"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
contentLeft={
|
||||||
|
<Card className="flight-details-page__mini-list" data-testid="flight-mini-list">
|
||||||
|
<h3>{flight.flightNumber}</h3>
|
||||||
|
<p>{flight.departure.airport} → {flight.arrival.airport}</p>
|
||||||
|
<p>{flight.status}</p>
|
||||||
|
</Card>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flight-details-page__content">
|
||||||
|
{/* Flight Header Info */}
|
||||||
|
<Card className="flight-details-page__header" data-testid="flight-header-card">
|
||||||
|
<div className="flight-details-page__header-grid">
|
||||||
|
<div className="flight-details-page__header-item">
|
||||||
|
<span className="flight-details-page__header-label">{t('SHARED.FLIGHT')}</span>
|
||||||
|
<span className="flight-details-page__header-value">{flight.flightNumber}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flight-details-page__header-item">
|
||||||
|
<span className="flight-details-page__header-label">{t('SHARED.STATUS')}</span>
|
||||||
|
<span className="flight-details-page__header-value">{flight.status}</span>
|
||||||
|
</div>
|
||||||
|
{flight.aircraft && (
|
||||||
|
<div className="flight-details-page__header-item">
|
||||||
|
<span className="flight-details-page__header-label">{t('SHARED.AIRCRAFT')}</span>
|
||||||
|
<span className="flight-details-page__header-value">{flight.aircraft}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{flight.operator && (
|
||||||
|
<div className="flight-details-page__header-item">
|
||||||
|
<span className="flight-details-page__header-label">{t('SHARED.OPERATOR')}</span>
|
||||||
|
<span className="flight-details-page__header-value">{flight.operator}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Flight Details */}
|
||||||
|
<Card className="flight-details-page__details" data-testid="flight-details-card">
|
||||||
|
<BoardFlightBody flight={flight} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Crew Information */}
|
||||||
|
{flight.crew && (
|
||||||
|
<Card className="flight-details-page__crew" data-testid="crew-info-card">
|
||||||
|
<h3>{t('BOARD.CREW_INFORMATION')}</h3>
|
||||||
|
<div className="flight-details-page__crew-grid">
|
||||||
|
{flight.crew.captain && (
|
||||||
|
<div>
|
||||||
|
<span className="flight-details-page__crew-label">{t('BOARD.CAPTAIN')}</span>
|
||||||
|
<span className="flight-details-page__crew-value">{flight.crew.captain}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{flight.crew.firstOfficer && (
|
||||||
|
<div>
|
||||||
|
<span className="flight-details-page__crew-label">{t('BOARD.FIRST_OFFICER')}</span>
|
||||||
|
<span className="flight-details-page__crew-value">{flight.crew.firstOfficer}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{flight.crew.flightAttendants && (
|
||||||
|
<div>
|
||||||
|
<span className="flight-details-page__crew-label">{t('BOARD.FLIGHT_ATTENDANTS')}</span>
|
||||||
|
<span className="flight-details-page__crew-value">{flight.crew.flightAttendants}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Seat Information */}
|
||||||
|
{flight.seats && (
|
||||||
|
<Card className="flight-details-page__seats" data-testid="seats-info-card">
|
||||||
|
<h3>{t('BOARD.SEATS')}</h3>
|
||||||
|
<div className="flight-details-page__seats-info">
|
||||||
|
<div className="flight-details-page__seats-item">
|
||||||
|
<span className="flight-details-page__seats-label">{t('BOARD.TOTAL_SEATS')}</span>
|
||||||
|
<span className="flight-details-page__seats-value">{flight.seats.total}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flight-details-page__seats-item">
|
||||||
|
<span className="flight-details-page__seats-label">{t('BOARD.AVAILABLE_SEATS')}</span>
|
||||||
|
<span className="flight-details-page__seats-value">{flight.seats.available}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Amenities */}
|
||||||
|
{flight.amenities && flight.amenities.length > 0 && (
|
||||||
|
<Card className="flight-details-page__amenities" data-testid="amenities-card">
|
||||||
|
<h3>{t('BOARD.AMENITIES')}</h3>
|
||||||
|
<div className="flight-details-page__amenities-list">
|
||||||
|
{flight.amenities.map((amenity, idx) => (
|
||||||
|
<span key={idx} className="flight-details-page__amenity-tag">
|
||||||
|
{amenity}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export { OnlineBoardStartPage } from './online-board-start-page'
|
export { OnlineBoardStartPage } from './online-board-start-page'
|
||||||
export { OnlineBoardSearchPage } from './online-board-search-page'
|
export { OnlineBoardSearchPage } from './online-board-search-page'
|
||||||
|
export { FlightDetailsPage } from './flight-details-page'
|
||||||
|
|||||||
Reference in New Issue
Block a user