feat: create FlightDetailsPage with full flight information

This commit is contained in:
gnezim
2026-04-05 21:14:54 +03:00
parent 49c696610c
commit 50a5442d79
3 changed files with 383 additions and 0 deletions
@@ -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 { OnlineBoardSearchPage } from './online-board-search-page'
export { FlightDetailsPage } from './flight-details-page'