mirror of
https://github.com/azaion/ui.git
synced 2026-04-23 10:06:35 +00:00
Refactor project structure and dependencies; rename package to azaion-ui, update version to 0.0.1, and remove unused files. Introduce new routing and authentication features in App component.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFlight } from '../../components/FlightContext'
|
||||
import { api } from '../../api/client'
|
||||
import { createSSE } from '../../api/sse'
|
||||
import { useResizablePanel } from '../../hooks/useResizablePanel'
|
||||
import ConfirmDialog from '../../components/ConfirmDialog'
|
||||
import type { Flight, Waypoint, Aircraft } from '../../types'
|
||||
import FlightMap from './FlightMap'
|
||||
|
||||
export default function FlightsPage() {
|
||||
const { t } = useTranslation()
|
||||
const { flights, selectedFlight, selectFlight, refreshFlights } = useFlight()
|
||||
const [mode, setMode] = useState<'params' | 'gps'>('params')
|
||||
const [waypoints, setWaypoints] = useState<Waypoint[]>([])
|
||||
const [aircrafts, setAircrafts] = useState<Aircraft[]>([])
|
||||
const [liveGps, setLiveGps] = useState<{ lat: number; lon: number; satellites: number; status: string } | null>(null)
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||
const [newName, setNewName] = useState('')
|
||||
const leftPanel = useResizablePanel(200, 150, 350)
|
||||
|
||||
useEffect(() => {
|
||||
api.get<Aircraft[]>('/api/flights/aircrafts').then(setAircrafts).catch(() => {})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedFlight) { setWaypoints([]); return }
|
||||
api.get<Waypoint[]>(`/api/flights/${selectedFlight.id}/waypoints`).then(setWaypoints).catch(() => {})
|
||||
}, [selectedFlight])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedFlight || mode !== 'gps') return
|
||||
return createSSE(`/api/flights/${selectedFlight.id}/live-gps`, (data: any) => setLiveGps(data))
|
||||
}, [selectedFlight, mode])
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newName.trim()) return
|
||||
await api.post('/api/flights', { name: newName.trim() })
|
||||
setNewName('')
|
||||
refreshFlights()
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteId) return
|
||||
await api.delete(`/api/flights/${deleteId}`)
|
||||
if (selectedFlight?.id === deleteId) selectFlight(null)
|
||||
setDeleteId(null)
|
||||
refreshFlights()
|
||||
}
|
||||
|
||||
const handleAddWaypoint = async () => {
|
||||
if (!selectedFlight) return
|
||||
await api.post(`/api/flights/${selectedFlight.id}/waypoints`, {
|
||||
name: `Point ${waypoints.length}`,
|
||||
latitude: 50.45, longitude: 30.52, order: waypoints.length,
|
||||
})
|
||||
const wps = await api.get<Waypoint[]>(`/api/flights/${selectedFlight.id}/waypoints`)
|
||||
setWaypoints(wps)
|
||||
}
|
||||
|
||||
const handleDeleteWaypoint = async (wpId: string) => {
|
||||
if (!selectedFlight) return
|
||||
await api.delete(`/api/flights/${selectedFlight.id}/waypoints/${wpId}`)
|
||||
setWaypoints(prev => prev.filter(w => w.id !== wpId))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
{/* Flight list sidebar */}
|
||||
<div style={{ width: leftPanel.width }} className="bg-az-panel border-r border-az-border flex flex-col shrink-0">
|
||||
<div className="p-2 border-b border-az-border">
|
||||
<div className="flex gap-1">
|
||||
<input
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleCreate()}
|
||||
placeholder={t('flights.create')}
|
||||
className="flex-1 bg-az-bg border border-az-border rounded px-2 py-1 text-xs text-az-text outline-none"
|
||||
/>
|
||||
<button onClick={handleCreate} className="bg-az-orange text-white text-xs px-2 py-1 rounded">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{flights.map(f => (
|
||||
<div
|
||||
key={f.id}
|
||||
onClick={() => selectFlight(f)}
|
||||
className={`px-2 py-1.5 cursor-pointer border-b border-az-border text-sm ${
|
||||
selectedFlight?.id === f.id ? 'bg-az-bg text-white' : 'text-az-text hover:bg-az-bg'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="truncate">{f.name}</span>
|
||||
<button onClick={e => { e.stopPropagation(); setDeleteId(f.id) }} className="text-az-muted hover:text-az-red text-xs">×</button>
|
||||
</div>
|
||||
<div className="text-xs text-az-muted">{new Date(f.createdDate).toLocaleDateString()}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resize handle */}
|
||||
<div onMouseDown={leftPanel.onMouseDown} className="w-1 cursor-col-resize bg-az-border hover:bg-az-orange shrink-0" />
|
||||
|
||||
{/* Left params panel */}
|
||||
{selectedFlight && (
|
||||
<div className="w-64 bg-az-panel border-r border-az-border flex flex-col shrink-0 overflow-y-auto">
|
||||
<div className="flex border-b border-az-border">
|
||||
<button
|
||||
onClick={() => setMode('params')}
|
||||
className={`flex-1 py-1.5 text-xs ${mode === 'params' ? 'bg-az-bg text-white' : 'text-az-muted'}`}
|
||||
>
|
||||
{t('flights.params')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('gps')}
|
||||
className={`flex-1 py-1.5 text-xs ${mode === 'gps' ? 'bg-az-bg text-white' : 'text-az-muted'}`}
|
||||
>
|
||||
{t('flights.gpsDenied')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{mode === 'params' && (
|
||||
<div className="p-2 space-y-2 text-xs">
|
||||
<div>
|
||||
<label className="text-az-muted block mb-0.5">{t('flights.aircraft')}</label>
|
||||
<select className="w-full bg-az-bg border border-az-border rounded px-2 py-1 text-az-text">
|
||||
{aircrafts.map(a => <option key={a.id} value={a.id}>{a.model}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-az-muted block mb-0.5">{t('flights.height')}</label>
|
||||
<input type="number" className="w-full bg-az-bg border border-az-border rounded px-2 py-1 text-az-text" defaultValue={100} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="text-az-muted">{t('flights.waypoints')}</label>
|
||||
<button onClick={handleAddWaypoint} className="text-az-orange text-xs">+ Add</button>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{waypoints.map(wp => (
|
||||
<div key={wp.id} className="flex items-center justify-between bg-az-bg rounded px-1.5 py-0.5">
|
||||
<span className="text-az-text">{wp.name}</span>
|
||||
<button onClick={() => handleDeleteWaypoint(wp.id)} className="text-az-muted hover:text-az-red">×</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'gps' && (
|
||||
<div className="p-2 space-y-2 text-xs">
|
||||
<div>
|
||||
<label className="text-az-muted block mb-1">{t('flights.liveGps')}</label>
|
||||
{liveGps ? (
|
||||
<div className="bg-az-bg rounded p-1.5 space-y-0.5">
|
||||
<div className="text-az-text">Status: <span className="text-az-green">{liveGps.status}</span></div>
|
||||
<div className="text-az-text">Lat: {liveGps.lat.toFixed(6)}</div>
|
||||
<div className="text-az-text">Lon: {liveGps.lon.toFixed(6)}</div>
|
||||
<div className="text-az-text">Sats: {liveGps.satellites}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-az-muted">Waiting for GPS signal...</div>
|
||||
)}
|
||||
</div>
|
||||
<button onClick={() => setMode('params')} className="text-az-orange text-xs">
|
||||
← {t('flights.back')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Map view */}
|
||||
<div className="flex-1 relative">
|
||||
<FlightMap waypoints={waypoints} />
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!deleteId}
|
||||
title={t('common.delete')}
|
||||
message="Delete this flight and all its data?"
|
||||
onConfirm={handleDelete}
|
||||
onCancel={() => setDeleteId(null)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user