Cleanup old code
This commit is contained in:
@@ -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>
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const theme = createTheme({
|
||||
});
|
||||
|
||||
export const metadata = {
|
||||
title: 'Aviation Weather',
|
||||
title: 'Aviation',
|
||||
description: ''
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user