Working on drawer

This commit is contained in:
2025-04-20 20:51:10 -04:00
parent 19ed8ef2ca
commit 06f9a96498
11 changed files with 109 additions and 427 deletions

25
ui/package-lock.json generated
View File

@@ -17,6 +17,7 @@
"d3": "^7.9.0",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"lodash.debounce": "^4.0.8",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0",
@@ -27,6 +28,7 @@
"@types/d3": "^7.4.3",
"@types/js-cookie": "^3.0.6",
"@types/leaflet": "^1.9.16",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^22.13.10",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
@@ -1881,6 +1883,23 @@
"@types/geojson": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.17.16",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
"integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash.debounce": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
"integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/node": {
"version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
@@ -3741,6 +3760,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",

View File

@@ -20,6 +20,7 @@
"d3": "^7.9.0",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"lodash.debounce": "^4.0.8",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0",
@@ -30,6 +31,7 @@
"@types/d3": "^7.4.3",
"@types/js-cookie": "^3.0.6",
"@types/leaflet": "^1.9.16",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^22.13.10",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",

View File

@@ -10,7 +10,7 @@ import { Header } from '@components/Header';
import AirportLayer from '@components/AirportLayer.tsx';
import { useEffect, useState } from 'react';
import { Airport } from '@lib/airport.types.ts';
import AirportDrawer from '@components/AirportDrawer.tsx';
import Index from '@components/AirportDrawer';
import { getWeatherMapUrl } from '@lib/rainViewer.ts';
import Cookies from 'js-cookie';
// import { createRoot } from 'react-dom/client';
@@ -150,7 +150,7 @@ function App() {
<div className='App'>
<Header />
<div className='map-wrapper'>
<AirportDrawer airport={airport} setAirport={setAirport} />
<Index airport={airport} setAirport={setAirport} />
<MapContainer
className='leaflet-container'
attributionControl={false}

View File

@@ -0,0 +1,3 @@
.drawer {
background: #32495f;
}

View File

@@ -1,11 +1,12 @@
import { Badge, Box, Divider, Drawer, Group, Tabs, TabsList, Text, Tooltip } from '@mantine/core';
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 { forwardRef, ReactNode, useEffect, useState } from 'react';
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 AirportDrawer({
export default function Index({
airport,
setAirport
}: {
@@ -48,7 +49,7 @@ export default function AirportDrawer({
onClose={() => setAirport(null)}
withinPortal
zIndex={1000}
styles={{ root: { padding: 0, margin: 0, width: 0, height: 0 } }}
styles={{ root: { padding: 0, margin: 0, width: 0, height: 0, backgroundColor: 'red' } }}
padding='md'
size={isMobile ? '100%' : 'md'}
position='left'
@@ -68,7 +69,7 @@ export default function AirportDrawer({
justify='space-between'
mb='md'
style={{
backgroundColor: '#272f38',
backgroundColor: '#32495f',
borderTop: '1px solid #1a242f',
borderBottom: '1px solid #1a242f',
padding: '10px'
@@ -102,21 +103,22 @@ export default function AirportDrawer({
);
}
function AirportInfoSlot({ title, value, units }: { title: string; value: string | number; units?: string }) {
function AirportInfoSlot({ title, style, children }: { title?: string; style?: CSSProperties; children?: ReactNode }) {
return (
<div>
<Text size='xs' color='dimmed'>
{title}
</Text>
<Text fw={500} size='sm'>
{value}
{units}
</Text>
<div style={{ ...style }}>
{title && (
<Text size='xs' color='dimmed'>
{title}
</Text>
)}
<Box fw={500} size='sm'>
{children}
</Box>
</div>
);
}
function AirportInfoRow({ children }: { children: ReactNode }) {
function AirportInfoRow({ style, children }: { style?: CSSProperties; children: ReactNode }) {
return (
<div
style={{
@@ -124,7 +126,8 @@ function AirportInfoRow({ children }: { children: ReactNode }) {
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))'
borderTop: '1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5))',
...style
}}
>
{children}
@@ -136,16 +139,21 @@ function AirportInfo({ airport }: { airport: Airport }) {
return (
<div>
<AirportInfoRow>
<AirportInfoSlot title={'ICAO'} value={airport.icao} />
<AirportInfoSlot title={'IATA'} value={airport.iata} />
<AirportInfoSlot title={'LOCAL'} value={airport.local} />
<AirportInfoSlot title={'Category'} value={airportCategoryToText(airport.category)} />
<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>
<AirportInfoSlot title={'Latitude'} value={airport.latitude} units={'} />
<AirportInfoSlot title={'Longitude'} value={airport.longitude} units={'°'} />
<AirportInfoSlot title={'Elevation'} value={airport.elevation_ft} units={' ft'} />
Zoom To
<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>

View File

@@ -1,15 +1,12 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Airport, AirportCategory } from '@lib/airport.types.ts';
import { useMapEvents } from 'react-leaflet';
import debounce from 'lodash.debounce';
import { getAirports } from '@lib/airport.ts';
import AirportMarker from '@components/AirportMarker.tsx';
import { LeafletEvent } from 'leaflet';
import { LayerInfo } from '@/App.tsx';
interface Bounds {
northEast: { lat: number; lon: number };
southWest: { lat: number; lon: number };
}
const EXPANSION_FACTOR = 0.5;
export default function AirportLayer({
setAirport,
@@ -21,76 +18,48 @@ export default function AirportLayer({
selectedLayer: LayerInfo;
}) {
const [airports, setAirports] = useState<Airport[]>([]);
const lastBoundsRef = useRef<{ ne: any; sw: any } | null>(null);
function loadAirports(event: LeafletEvent) {
const map = event.target;
const bounds = map.getBounds();
const debouncedLoad = useRef(
debounce(async (map: any) => {
const b = map.getBounds();
const north = b.getNorth(),
south = b.getSouth();
const east = b.getEast(),
west = b.getWest();
const latDelta = (north - south) * EXPANSION_FACTOR;
const lonDelta = (east - west) * EXPANSION_FACTOR;
const boundsParam: Bounds = {
northEast: {
lat: bounds.getNorth(),
lon: bounds.getEast()
},
southWest: {
lat: bounds.getSouth(),
lon: bounds.getWest()
}
};
// expanded bbox
const ne = { lat: north + latDelta, lon: east + lonDelta };
const sw = { lat: south - latDelta, lon: west - lonDelta };
lastBoundsRef.current = { ne, sw };
getAirports({
bounds: boundsParam,
metars: true,
categories: [AirportCategory.HELIPORT, AirportCategory.SMALL, AirportCategory.MEDIUM, AirportCategory.LARGE]
})
.then((response) => {
setAirports(response.data);
})
.catch((error) => {
console.error('Error fetching airports:', error);
try {
const resp = await getAirports({
bounds: { northEast: ne, southWest: sw },
metars: true,
categories: [AirportCategory.HELIPORT, AirportCategory.SMALL, AirportCategory.MEDIUM, AirportCategory.LARGE]
});
setAirports(resp.data);
} catch (err) {
console.error('fetch error', err);
setAirports([]);
});
}
}
}, 300)
).current;
const map = useMapEvents({
moveend: loadAirports
move: () => debouncedLoad(map)
});
useEffect(() => {
if (map) {
loadAirports({ target: map } as LeafletEvent);
}
if (map) debouncedLoad(map);
return () => {
debouncedLoad.cancel();
};
}, [map]);
// const categoryOrder: { [key in AirportCategory]?: number } = {
// [AirportCategory.LARGE]: 3,
// [AirportCategory.MEDIUM]: 2,
// [AirportCategory.SMALL]: 1,
// [AirportCategory.HELIPORT]: 0
// };
// const sortedAirports = airports.slice().sort((a, b) => {
// // Compare by airport category first.
// const categoryA = categoryOrder[a.category] ?? 4;
// const categoryB = categoryOrder[b.category] ?? 4;
// if (categoryA !== categoryB) {
// return categoryA - categoryB;
// }
//
// // Then compare by flight category if available.
// // Assuming that latest_metar.flight_category is a string and "UNKN" needs to come last.
// const fcA = a.latest_metar?.flight_category ?? 'UNKN';
// const fcB = b.latest_metar?.flight_category ?? 'UNKN';
//
// if (fcA === 'UNKN' && fcB !== 'UNKN') return 1;
// if (fcB === 'UNKN' && fcA !== 'UNKN') return -1;
//
// // If both flight categories are not "UNKN", do a simple alphabetical comparison.
// // (You may wish to customize this logic based on the actual flight category values.)
// if (fcA < fcB) return -1;
// if (fcA > fcB) return 1;
// return 0;
// });
return (
<>
{airports.map((airport, index) => (

View File

@@ -1,7 +1,8 @@
.header {
height: 56px;
padding: 0 16px 0 16px;
background-color: var(--mantine-color-body);
/*background-color: var(--mantine-color-body);*/
background: #32495f;
border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}