Working on drawer
This commit is contained in:
214
ui/src/components/AirportDrawer/index.tsx
Normal file
214
ui/src/components/AirportDrawer/index.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import { Badge, Box, Divider, Drawer, Group, Tabs, TabsList, Text, Tooltip, UnstyledButton } from '@mantine/core';
|
||||
import { Airport, AirportCategory } from '@lib/airport.types.ts';
|
||||
import { getMarkerColor, Metar } from '@lib/metar.types.ts';
|
||||
import { CSSProperties, forwardRef, ReactNode, useEffect, useState } from 'react';
|
||||
import { getMetars } from '@lib/metar.ts';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { IconViewfinder } from '@tabler/icons-react';
|
||||
|
||||
export default function Index({
|
||||
airport,
|
||||
setAirport
|
||||
}: {
|
||||
airport: Airport | null;
|
||||
setAirport: (airport: Airport | null) => void;
|
||||
}) {
|
||||
const [metar, setMetar] = useState<Metar | undefined>(undefined);
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
|
||||
useEffect(() => {
|
||||
if (!airport) return;
|
||||
function updateMetar() {
|
||||
if (!airport) return;
|
||||
console.log(airport.icao);
|
||||
getMetars({ icaos: [airport.icao] }).then((m) => {
|
||||
if (m.length > 0) {
|
||||
setMetar(m[0]);
|
||||
} else {
|
||||
setMetar(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateMetar();
|
||||
|
||||
const interval = setInterval(updateMetar, 60000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [airport]);
|
||||
|
||||
if (!airport) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const metarColor = getMarkerColor(metar?.flight_category || 'UNKN');
|
||||
|
||||
return (
|
||||
<Drawer.Root
|
||||
opened={true}
|
||||
onClose={() => setAirport(null)}
|
||||
withinPortal
|
||||
zIndex={1000}
|
||||
styles={{ root: { padding: 0, margin: 0, width: 0, height: 0, backgroundColor: 'red' } }}
|
||||
padding='md'
|
||||
size={isMobile ? '100%' : 'md'}
|
||||
position='left'
|
||||
closeOnClickOutside={false}
|
||||
>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>
|
||||
<Text size={'xl'}>{airport.name}</Text>
|
||||
</Drawer.Title>
|
||||
<Drawer.CloseButton />
|
||||
</Drawer.Header>
|
||||
<Drawer.Body>
|
||||
<Box mb='lg'>
|
||||
{metar && metar.flight_category && (
|
||||
<Group
|
||||
justify='space-between'
|
||||
mb='md'
|
||||
style={{
|
||||
backgroundColor: '#32495f',
|
||||
borderTop: '1px solid #1a242f',
|
||||
borderBottom: '1px solid #1a242f',
|
||||
padding: '10px'
|
||||
}}
|
||||
>
|
||||
<Badge size='lg' color={metarColor}>
|
||||
{metar.flight_category}
|
||||
</Badge>
|
||||
{/*<Text style={{ color: metarColor }}>{metar.flight_category}</Text>*/}
|
||||
<Tooltip zIndex={1001} label={new Date(metar.observation_time).toLocaleString()}>
|
||||
<TimeSince date={metar.observation_time} />
|
||||
</Tooltip>
|
||||
</Group>
|
||||
)}
|
||||
<Tabs variant={'outline'} defaultValue={'info'}>
|
||||
<TabsList grow>
|
||||
<Tabs.Tab value={'info'}>Info</Tabs.Tab>
|
||||
<Tabs.Tab value={'weather'}>Weather</Tabs.Tab>
|
||||
</TabsList>
|
||||
<Tabs.Panel value={'info'}>
|
||||
<AirportInfo airport={airport} />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={'weather'}>
|
||||
<WeatherInfo metar={airport.latest_metar} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Drawer.Body>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function AirportInfoSlot({ title, style, children }: { title?: string; style?: CSSProperties; children?: ReactNode }) {
|
||||
return (
|
||||
<div style={{ ...style }}>
|
||||
{title && (
|
||||
<Text size='xs' color='dimmed'>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
<Box fw={500} size='sm'>
|
||||
{children}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AirportInfoRow({ style, children }: { style?: CSSProperties; children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignContent: 'center',
|
||||
padding: 'var(--mantine-spacing-sm) var(--mantine-spacing-lg)',
|
||||
borderTop: '1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5))',
|
||||
...style
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AirportInfo({ airport }: { airport: Airport }) {
|
||||
return (
|
||||
<div>
|
||||
<AirportInfoRow>
|
||||
<AirportInfoSlot title={'ICAO'} children={airport.icao} />
|
||||
<AirportInfoSlot title={'IATA'} children={airport.iata} />
|
||||
<AirportInfoSlot title={'LOCAL'} children={airport.local} />
|
||||
<AirportInfoSlot title={'Category'} children={airportCategoryToText(airport.category)} />
|
||||
</AirportInfoRow>
|
||||
<AirportInfoRow style={{ justifyContent: 'flex-start' }}>
|
||||
<AirportInfoSlot title={'Location'}>
|
||||
{airport.latitude}°, {airport.longitude}°
|
||||
</AirportInfoSlot>
|
||||
<AirportInfoSlot title={'Elevation'} style={{ paddingLeft: '1rem' }} children={`${airport.elevation_ft} ft`} />
|
||||
<AirportInfoSlot style={{ marginLeft: 'auto', paddingLeft: '1rem', paddingTop: '0.5rem' }}>
|
||||
<UnstyledButton>
|
||||
<IconViewfinder />
|
||||
</UnstyledButton>
|
||||
</AirportInfoSlot>
|
||||
</AirportInfoRow>
|
||||
<Divider />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WeatherInfo({ metar }: { metar?: Metar }) {
|
||||
if (metar) {
|
||||
return <>{metar.raw_text}</>;
|
||||
} else {
|
||||
return <>No METAR observation available</>;
|
||||
}
|
||||
}
|
||||
|
||||
function airportCategoryToText(category: AirportCategory): string {
|
||||
switch (category) {
|
||||
case AirportCategory.SMALL:
|
||||
return 'Small';
|
||||
case AirportCategory.MEDIUM:
|
||||
return 'Medium';
|
||||
case AirportCategory.LARGE:
|
||||
return 'Large';
|
||||
case AirportCategory.HELIPORT:
|
||||
return 'Helipad';
|
||||
case AirportCategory.CLOSED:
|
||||
return 'Closed';
|
||||
case AirportCategory.SEAPLANE:
|
||||
return 'Seaplane Base';
|
||||
case AirportCategory.BALLOONPORT:
|
||||
return 'Balloon Port';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
const TimeSince = forwardRef<HTMLParagraphElement, { date: string }>(({ date }, ref) => {
|
||||
const inputDate = new Date(date);
|
||||
// @ts-expect-error doing arithmetic with dates
|
||||
const seconds = Math.floor((new Date() - inputDate) / 1000);
|
||||
|
||||
if (seconds < 60) {
|
||||
const content = seconds + (seconds === 1 ? ' second ago' : ' seconds ago');
|
||||
return (
|
||||
<Text ref={ref} style={{ userSelect: 'none' }}>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const content = minutes + (minutes === 1 ? ' minute ago' : ' minutes ago');
|
||||
// If more than 60 minutes have passed, set the text color to yellow
|
||||
return (
|
||||
<Text ref={ref} style={{ color: minutes >= 60 ? '#fca903' : undefined, userSelect: 'none' }}>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user