Cleanup old code
This commit is contained in:
72
ui/src/components/AirportDrawer.tsx
Normal file
72
ui/src/components/AirportDrawer.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Divider, Drawer, Group } from '@mantine/core';
|
||||
import { Airport, AirportCategory } from '@lib/airport.types.ts';
|
||||
|
||||
export default function AirportDrawer({
|
||||
airport,
|
||||
setAirport
|
||||
}: {
|
||||
airport: Airport | null;
|
||||
setAirport: (airport: Airport | null) => void;
|
||||
}) {
|
||||
if (!airport) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Drawer
|
||||
opened={true}
|
||||
onClose={() => setAirport(null)}
|
||||
title={airport.name}
|
||||
withinPortal
|
||||
zIndex={10000}
|
||||
styles={{ root: { width: 0, height: 0 } }}
|
||||
padding='md'
|
||||
size='md'
|
||||
position='left'
|
||||
withOverlay={false}
|
||||
closeOnClickOutside={false}
|
||||
>
|
||||
<Group>
|
||||
<div>ICAO: {airport.icao}</div>
|
||||
<div>Category: {airportCategoryToText(airport.category)}</div>
|
||||
<div>
|
||||
Country / Region: {airport.iso_country}, {airport.iso_region}
|
||||
</div>
|
||||
<div>Municipality: {airport.municipality || 'N/A'}</div>
|
||||
<div>Local Code: {airport.local || 'N/A'}</div>
|
||||
<div>Elevation: {airport.elevation_ft}</div>
|
||||
<div>
|
||||
Coordinates: {airport.latitude.toFixed(4)}, {airport.longitude.toFixed(4)}
|
||||
</div>
|
||||
<div>Control Tower: {airport.has_tower ? 'Yes' : 'No'}</div>
|
||||
<div>Beacon: {airport.has_beacon ? 'Yes' : 'No'}</div>
|
||||
{airport.latest_metar && airport.latest_metar.flight_category && (
|
||||
<>
|
||||
<Divider my='sm' />
|
||||
<div>Flight Category: {airport.latest_metar.flight_category}</div>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
@@ -1,104 +1,64 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Airport, AirportCategory } from '@lib/airport.types.ts';
|
||||
import { Marker, Popup, useMapEvents } from 'react-leaflet';
|
||||
import { useMapEvents } from 'react-leaflet';
|
||||
import { getAirports } from '@lib/airport.ts';
|
||||
import L from 'leaflet';
|
||||
import AirportMarker from '@components/AirportMarker.tsx';
|
||||
import { LeafletEvent } from 'leaflet';
|
||||
|
||||
interface Bounds {
|
||||
northEast: { lat: number; lon: number };
|
||||
southWest: { lat: number; lon: number };
|
||||
}
|
||||
|
||||
export default function AirportLayer() {
|
||||
export default function AirportLayer({ setAirport }: { setAirport: (airport: Airport) => void }) {
|
||||
const [airports, setAirports] = useState<Airport[]>([]);
|
||||
|
||||
useMapEvents({
|
||||
moveend: (event) => {
|
||||
const map = event.target;
|
||||
const bounds = map.getBounds();
|
||||
function loadAirports(event: LeafletEvent) {
|
||||
const map = event.target;
|
||||
const bounds = map.getBounds();
|
||||
|
||||
const boundsParam: Bounds = {
|
||||
northEast: {
|
||||
lat: bounds.getNorth(),
|
||||
lon: bounds.getEast()
|
||||
},
|
||||
southWest: {
|
||||
lat: bounds.getSouth(),
|
||||
lon: bounds.getWest()
|
||||
}
|
||||
};
|
||||
const boundsParam: Bounds = {
|
||||
northEast: {
|
||||
lat: bounds.getNorth(),
|
||||
lon: bounds.getEast()
|
||||
},
|
||||
southWest: {
|
||||
lat: bounds.getSouth(),
|
||||
lon: bounds.getWest()
|
||||
}
|
||||
};
|
||||
|
||||
// Call getAirports with the current map bounds and desired parameters.
|
||||
getAirports({
|
||||
bounds: boundsParam,
|
||||
metars: true,
|
||||
categories: [AirportCategory.SMALL, AirportCategory.MEDIUM, AirportCategory.LARGE],
|
||||
limit: 200
|
||||
getAirports({
|
||||
bounds: boundsParam,
|
||||
metars: true,
|
||||
categories: [AirportCategory.SMALL, AirportCategory.MEDIUM, AirportCategory.LARGE],
|
||||
limit: 200
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
setAirports(response.data);
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
setAirports(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching airports:', error);
|
||||
setAirports([]);
|
||||
});
|
||||
}
|
||||
.catch((error) => {
|
||||
console.error('Error fetching airports:', error);
|
||||
setAirports([]);
|
||||
});
|
||||
}
|
||||
|
||||
const map = useMapEvents({
|
||||
moveend: loadAirports
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (map) {
|
||||
loadAirports({ target: map } as LeafletEvent);
|
||||
}
|
||||
}, [map]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{airports.map((airport, index) => {
|
||||
const markerColor = getMarkerColor(airport);
|
||||
const icon = createCustomIcon(markerColor);
|
||||
return (
|
||||
<Marker key={index} position={[airport.latitude, airport.longitude]} icon={icon}>
|
||||
<Popup>
|
||||
<div>
|
||||
<h3>{airport.name || 'Unnamed Airport'}</h3>
|
||||
<p>ICAO: {airport.icao || 'N/A'}</p>
|
||||
<p>Flight Category: {airport.latest_metar ? airport.latest_metar.flight_category : 'No METAR Data'}</p>
|
||||
</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
);
|
||||
return <AirportMarker airport={airport} index={index} setAirport={setAirport} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getMarkerColor(airport: Airport): string {
|
||||
if (airport.latest_metar) {
|
||||
switch (airport.latest_metar.flight_category.toUpperCase()) {
|
||||
case 'IFR':
|
||||
return '#ff0100';
|
||||
case 'LIFR':
|
||||
return '#7f007f';
|
||||
case 'MVFR':
|
||||
return '#00f';
|
||||
case 'VFR':
|
||||
return '#018000';
|
||||
case 'UNKNOWN':
|
||||
return '#3e3e3e';
|
||||
default:
|
||||
return '#3e3e3e';
|
||||
}
|
||||
} else {
|
||||
return '#696969';
|
||||
}
|
||||
}
|
||||
|
||||
function createCustomIcon(color: string): L.DivIcon {
|
||||
return L.divIcon({
|
||||
html: `<div style="
|
||||
background-color: ${color};
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
"></div>`,
|
||||
className: '',
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
}
|
||||
|
||||
62
ui/src/components/AirportMarker.tsx
Normal file
62
ui/src/components/AirportMarker.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Airport } from '@lib/airport.types.ts';
|
||||
import { Marker } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
|
||||
export default function AirportMarker({
|
||||
index,
|
||||
airport,
|
||||
setAirport
|
||||
}: {
|
||||
index: number;
|
||||
airport: Airport;
|
||||
setAirport: (airport: Airport) => void;
|
||||
}) {
|
||||
const markerColor = getMarkerColor(airport);
|
||||
const icon = createCustomIcon(markerColor);
|
||||
return (
|
||||
<Marker
|
||||
key={index}
|
||||
position={[airport.latitude, airport.longitude]}
|
||||
icon={icon}
|
||||
eventHandlers={{
|
||||
click: () => setAirport(airport)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getMarkerColor(airport: Airport): string {
|
||||
if (airport.latest_metar) {
|
||||
switch (airport.latest_metar.flight_category.toUpperCase()) {
|
||||
case 'IFR':
|
||||
return '#ff0100';
|
||||
case 'LIFR':
|
||||
return '#7f007f';
|
||||
case 'MVFR':
|
||||
return '#00f';
|
||||
case 'VFR':
|
||||
return '#018000';
|
||||
case 'UNKNOWN':
|
||||
return '#3e3e3e';
|
||||
default:
|
||||
return '#3e3e3e';
|
||||
}
|
||||
} else {
|
||||
return '#696969';
|
||||
}
|
||||
}
|
||||
|
||||
function createCustomIcon(color: string): L.DivIcon {
|
||||
return L.divIcon({
|
||||
html: `<div style="
|
||||
background-color: ${color};
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
"></div>`,
|
||||
className: '',
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
.header {
|
||||
height: 56px;
|
||||
padding: 0 16px 0 16px;
|
||||
background-color: var(--mantine-color-body);
|
||||
border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
@@ -9,6 +10,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 10px; /* Optional horizontal padding */
|
||||
}
|
||||
|
||||
.link {
|
||||
@@ -20,13 +22,20 @@
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 500;
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme] &[data-active] {
|
||||
background-color: var(--mantine-color-blue-filled);
|
||||
color: var(--mantine-color-white);
|
||||
}
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme] .link[data-active] {
|
||||
background-color: var(--mantine-color-blue-filled);
|
||||
color: var(--mantine-color-white);
|
||||
}
|
||||
|
||||
/* Center the navigation items */
|
||||
.navGroup {
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Avatar, Burger, Container, Group, Text } from '@mantine/core';
|
||||
import { Avatar, Box, Burger, Button, Container, Group, Text } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
// import { ReactComponent as Logo } from '../../../public/logo.svg';
|
||||
import classes from './Header.module.css';
|
||||
|
||||
const links = [
|
||||
@@ -13,8 +12,9 @@ const links = [
|
||||
export function Header() {
|
||||
const [opened, { toggle }] = useDisclosure(false);
|
||||
const [active, setActive] = useState(links[0].link);
|
||||
const isSignedIn = false;
|
||||
|
||||
const items = links.map((link) => (
|
||||
const navItems = links.map((link) => (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.link}
|
||||
@@ -30,19 +30,35 @@ export function Header() {
|
||||
));
|
||||
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Container size='md' className={classes.inner}>
|
||||
<span style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Text>Aviation Weather</Text>
|
||||
<Avatar src='../../../public/logo.svg' alt="it's me" />
|
||||
</span>
|
||||
{/*<Logo />*/}
|
||||
<Group gap={5} visibleFrom='xs'>
|
||||
{items}
|
||||
<Box>
|
||||
<header className={classes.header}>
|
||||
<Group justify='space-between' h='100%'>
|
||||
<Group align='center' gap='xs'>
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom='xs' size='sm' />
|
||||
<Avatar src='/logo.svg' alt='logo' />
|
||||
<Text>Aviation</Text>
|
||||
</Group>
|
||||
<Group gap={5} visibleFrom='xs' className={classes.navGroup}>
|
||||
{navItems}
|
||||
</Group>
|
||||
<Group align='center' gap='xs'>
|
||||
{isSignedIn ? (
|
||||
// Clickable avatar if signed in
|
||||
<Avatar
|
||||
src='/user-avatar.jpg' // replace with dynamic source when available
|
||||
alt='User avatar'
|
||||
style={{ cursor: 'pointer' }}
|
||||
// Add click handler for user dropdown if needed
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Button variant='default'>Login</Button>
|
||||
<Button>Signup</Button>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom='xs' size='sm' />
|
||||
</Container>
|
||||
</header>
|
||||
</header>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user