Working on drawer
This commit is contained in:
25
ui/package-lock.json
generated
25
ui/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
3
ui/src/components/AirportDrawer/AirportDrawer.module.css
Normal file
3
ui/src/components/AirportDrawer/AirportDrawer.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.drawer {
|
||||
background: #32495f;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user