Cleanup old code

This commit is contained in:
2025-04-10 22:51:33 -04:00
parent 05c49dee4c
commit 8b68653b6f
73 changed files with 249 additions and 8973 deletions

View File

@@ -1,15 +1,16 @@
import { MapContainer, TileLayer } from 'react-leaflet';
import { MapContainer, TileLayer, ZoomControl } from 'react-leaflet';
import '@mantine/core/styles.css';
import 'leaflet/dist/leaflet.css';
import './App.css';
import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
import markerIcon from 'leaflet/dist/images/marker-icon.png';
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
// Fix for default marker icon issues in React-Leaflet
import L from 'leaflet';
import { Header } from '@components/Header';
import AirportLayer from '@components/AirportLayer.tsx';
import { useState } from 'react';
import { Airport } from '@lib/airport.types.ts';
import AirportDrawer from '@components/AirportDrawer.tsx';
// Fix Leaflet's default icon path issues with Webpack
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -23,16 +24,21 @@ L.Icon.Default.mergeOptions({
});
const tileLayerUrl = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
const defaultZoom = 6;
const defaultCenter: L.LatLngExpression = [38.944444, -77.455833];
function App() {
const [airport, setAirport] = useState<Airport | null>(null);
return (
<div className='App'>
<Header />
<div className='map-wrapper'>
<AirportDrawer airport={airport} setAirport={setAirport} />
<MapContainer
className='leaflet-container'
center={[38.944444, -77.455833]}
zoom={6}
attributionControl={false}
center={defaultCenter}
zoom={defaultZoom}
minZoom={3}
maxZoom={19}
maxBounds={[
@@ -40,9 +46,11 @@ function App() {
[85.06, 180]
]}
scrollWheelZoom={true}
zoomControl={false}
>
<ZoomControl position={'bottomright'} />
<TileLayer url={tileLayerUrl} />
<AirportLayer />
<AirportLayer setAirport={setAirport} />
</MapContainer>
</div>
</div>

View 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';
}
}

View File

@@ -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]
});
}

View 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]
});
}

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -11,37 +11,6 @@ export enum AirportCategory {
UNKNOWN = 'unknown'
}
export 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';
}
}
export enum AirportOrderField {
ICAO = 'icao',
NAME = 'name',
CATEGORY = 'category',
CONTINENT = 'continent',
ISO_COUNTRY = 'iso_country',
ISO_REGION = 'iso_region',
MUNICIPALITY = 'municipality'
}
export interface Bounds {
northEast: Coordinate;
southWest: Coordinate;
@@ -66,10 +35,9 @@ export interface Airport {
longitude: number;
has_tower: boolean;
has_beacon: boolean;
has_metar: boolean;
public: boolean;
runways: Runway[];
frequencies: Frequency[];
public: boolean;
latest_metar?: Metar;
}

View File

@@ -11,7 +11,7 @@ const theme = createTheme({
});
export const metadata = {
title: 'Aviation Weather',
title: 'Aviation',
description: ''
};