Update frequencies to communications, fixed control icons

This commit is contained in:
2025-05-14 08:28:43 -04:00
parent 019fb77373
commit 1e3c75624a
18 changed files with 20457 additions and 20413 deletions

View File

@@ -0,0 +1,27 @@
import { Table } from '@mantine/core';
import { Communication } from '@lib/airport.types.ts';
export function CommunicationTable({ communications }: { communications: Communication[] }) {
const rows = communications.map((communication) => (
<Table.Tr key={communication.id}>
<Table.Td>{communication.id}</Table.Td>
<Table.Td>{communication.name}</Table.Td>
<Table.Td>{communication.frequencies_mhz}</Table.Td>
<Table.Td>{communication.phone}</Table.Td>
</Table.Tr>
));
return (
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>ID</Table.Th>
<Table.Th>Name</Table.Th>
<Table.Th>Frequencies (MHz)</Table.Th>
<Table.Th>Phone</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
);
}

View File

@@ -1,25 +0,0 @@
import { Table } from '@mantine/core';
import { Frequency } from '@lib/airport.types.ts';
export default function FrequencyTable({ frequencies }: { frequencies: Frequency[] }) {
const rows = frequencies.map((frequency) => (
<Table.Tr key={frequency.id}>
<Table.Td>{frequency.id}</Table.Td>
<Table.Td>{frequency.name}</Table.Td>
<Table.Td>{frequency.frequency_mhz}</Table.Td>
</Table.Tr>
));
return (
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>ID</Table.Th>
<Table.Th>Name</Table.Th>
<Table.Th>MHz</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
);
}

View File

@@ -1,7 +1,7 @@
import { Table } from '@mantine/core';
import { Runway } from '@lib/airport.types.ts';
export default function RunwayTable({ runways }: { runways: Runway[] }) {
export function RunwayTable({ runways }: { runways: Runway[] }) {
const rows = runways.map((runway) => (
<Table.Tr key={runway.id}>
<Table.Td>{runway.id}</Table.Td>

View File

@@ -17,10 +17,12 @@ 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';
import RunwayTable from '@components/AirportDrawer/RunwayTable.tsx';
import FrequencyTable from '@components/AirportDrawer/FrequencyTable.tsx';
import { RunwayTable } from '@components/AirportDrawer/RunwayTable.tsx';
import { CommunicationTable } from '@components/AirportDrawer/CommunicationTable.tsx';
import { useMap } from 'react-leaflet';
import type { Map as LeafletMap } from 'leaflet';
export default function Index({
export function AirportDrawer({
airport,
setAirport
}: {
@@ -29,12 +31,12 @@ export default function Index({
}) {
const [metar, setMetar] = useState<Metar | undefined>(undefined);
const isMobile = useMediaQuery('(max-width: 768px)');
const map = useMap();
useEffect(() => {
if (!airport) return;
function updateMetar() {
if (!airport) return;
console.log(airport.icao);
getMetars({ icaos: [airport.icao] }).then((m) => {
if (m.length > 0) {
setMetar(m[0]);
@@ -104,7 +106,7 @@ export default function Index({
<Tabs.Tab value={'weather'}>Weather</Tabs.Tab>
</TabsList>
<Tabs.Panel value={'info'}>
<AirportInfo airport={airport} />
<AirportInfo map={map} airport={airport} />
</Tabs.Panel>
<Tabs.Panel value={'weather'}>
<WeatherInfo metar={airport.latest_metar} />
@@ -149,7 +151,12 @@ function AirportInfoRow({ style, children }: { style?: CSSProperties; children:
);
}
function AirportInfo({ airport }: { airport: Airport }) {
function AirportInfo({ map, airport }: { map: LeafletMap, airport: Airport }) {
function goToLocation(map: LeafletMap, latitude: number, longitude: number) {
if (!map) return
map.setView([latitude, longitude], map.getZoom())
}
return (
<div>
<AirportInfoRow>
@@ -164,7 +171,9 @@ function AirportInfo({ airport }: { airport: Airport }) {
</AirportInfoSlot>
<AirportInfoSlot title={'Elevation'} style={{ paddingLeft: '1rem' }} children={`${airport.elevation_ft} ft`} />
<AirportInfoSlot style={{ marginLeft: 'auto', paddingLeft: '1rem', paddingTop: '0.5rem' }}>
<UnstyledButton>
<UnstyledButton onClick={() => {
goToLocation(map, airport.latitude, airport.longitude)
}}>
<IconViewfinder />
</UnstyledButton>
</AirportInfoSlot>
@@ -180,13 +189,13 @@ function AirportInfo({ airport }: { airport: Airport }) {
</Accordion.Panel>
</Accordion.Item>
)}
{airport.frequencies != null && airport.frequencies.length > 0 && (
<Accordion.Item value={'frequencies'}>
{airport.communications != null && airport.communications.length > 0 && (
<Accordion.Item value={'communication'}>
<Accordion.Control>
Frequencies
Communication
</Accordion.Control>
<Accordion.Panel>
<FrequencyTable frequencies={airport.frequencies} />
<CommunicationTable communications={airport.communications} />
</Accordion.Panel>
</Accordion.Item>
)}

View File

@@ -13,63 +13,55 @@ interface Props {
export function CustomControl({ position = 'bottomright', onClick, active = false, title = '', children }: Props) {
const map = useMap();
// Create references
const buttonRef = useRef<HTMLAnchorElement | null>(null);
const reactRootRef = useRef<Root | null>(null);
const controlRef = useRef<L.Control>(null);
const rootRef = useRef<Root>(null);
useEffect(() => {
const ctrl = new L.Control({ position });
ctrl.onAdd = () => {
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control custom-control');
const button = L.DomUtil.create('a', '', container) as HTMLAnchorElement;
button.href = '#';
button.title = title;
return L.DomUtil.create('div', 'leaflet-bar leaflet-control custom-control');
}
// Prevent clicks/scrolls on the control from hitting the map
L.DomEvent.disableClickPropagation(container);
L.DomEvent.disableScrollPropagation(container);
// Wire up the handler
L.DomEvent.on(button, 'click', L.DomEvent.stop);
L.DomEvent.on(button, 'click', L.DomEvent.preventDefault);
L.DomEvent.on(button, 'click', () => onClick());
buttonRef.current = button;
// Initial active status
if (active) {
button.classList.add('active');
}
// Render children
if (children) {
reactRootRef.current = createRoot(button);
reactRootRef.current.render(children);
}
return container;
};
// Add component to the map
ctrl.addTo(map);
controlRef.current = ctrl;
// @ts-expect-error ctrl is a L.Control
const container = (ctrl as unknown)._container as HTMLElement;
rootRef.current = createRoot(container);
// On unmount, remove component
return () => {
ctrl.remove();
if (reactRootRef.current) {
reactRootRef.current.unmount();
reactRootRef.current = null;
if (rootRef.current) {
rootRef.current!.unmount();
rootRef.current = null;
}
ctrl.remove();
};
}, [map, position, onClick, children, active, title]);
}, [map, position]);
useEffect(() => {
const btn = buttonRef.current;
if (!btn) return;
if (active) btn.classList.add('active');
else btn.classList.remove('active');
}, [active]);
if (rootRef.current) {
rootRef.current.render(
<a
href={'#'}
title={title}
className={active ? 'active' : ''}
onClick={e => {
e.preventDefault();
e.stopPropagation();
onClick();
}}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px'
}}
>
{children}
</a>
)
}
}, [onClick, active, title, children]);
return null;
}

View File

@@ -1,5 +1,4 @@
import { useEffect, useRef } from 'react';
import { ReactNode } from 'react';
import { ReactNode, useEffect, useRef } from 'react';
import * as L from 'leaflet';
import { useMap } from 'react-leaflet';
import { createRoot, Root } from 'react-dom/client';
@@ -19,56 +18,57 @@ interface GroupControlProps {
export function GroupControl({ position = 'bottomright', buttons }: GroupControlProps) {
const map = useMap();
// References
const buttonRefs = useRef<HTMLAnchorElement[]>([]);
const reactRootRefs = useRef<Root[]>([]);
const controlRef = useRef<L.Control>(null);
const rootRef = useRef<Root>(null);
useEffect(() => {
const ctrl = new L.Control({ position });
const current = reactRootRefs.current;
controlRef.current = ctrl;
ctrl.onAdd = () => {
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control custom-control');
buttons.forEach((btnDef, i) => {
const btn = L.DomUtil.create('a', '', container) as HTMLAnchorElement;
btn.href = '#';
btn.title = btnDef.title;
// standard leaflet clickblocking magic
L.DomEvent.disableClickPropagation(btn);
L.DomEvent.disableScrollPropagation(btn);
L.DomEvent.on(btn, 'click', L.DomEvent.stop)
.on(btn, 'click', L.DomEvent.preventDefault)
.on(btn, 'click', btnDef.onClick);
// Initial active status
if (btnDef.active) btn.classList.add('active');
// Render root
const rootRef = createRoot(btn);
rootRef.render(btnDef.icon);
reactRootRefs.current[i] = rootRef;
buttonRefs.current[i] = btn;
});
return container;
return L.DomUtil.create('div', 'leaflet-bar leaflet-control custom-control');
};
ctrl.addTo(map);
// @ts-expect-error ctrl is a L.Control
const container = (ctrl as unknown)._container as HTMLElement;
rootRef.current = createRoot(container);
return () => {
ctrl.remove();
// unmount React roots
current.forEach((r) => r.unmount());
rootRef.current!.unmount();
};
}, [map, buttons, position]);
}, [map, position]);
// if you want to toggle “.active” live when props change
useEffect(() => {
buttons.forEach((b, i) => {
const btn = buttonRefs.current[i];
if (!btn) return;
if (b.active) btn.classList.add('active');
else btn.classList.remove('active');
});
if (rootRef.current) {
rootRef.current.render(
<div style={{ display: 'flex', flexDirection: 'column' }}>
{buttons.map((b, i) => (
<a
key={i}
href="#"
title={b.title}
className={b.active ? 'active' : ''}
onClick={e => {
e.preventDefault();
e.stopPropagation();
b.onClick();
}}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px'
}}
>
{b.icon}
</a>
))}
</div>
);
}
}, [buttons]);
return null;

View File

@@ -0,0 +1,31 @@
import { useMap } from 'react-leaflet';
import { CustomControl } from '@components/CustomControl.tsx';
import { IconCurrentLocation } from '@tabler/icons-react';
export function LocateControl() {
const map = useMap();
function handleClick() {
if (!navigator.geolocation) {
alert('Geolocation is not supported by your browser');
return;
}
navigator.geolocation.getCurrentPosition(
(pos) => {
const { latitude, longitude } = pos.coords;
// you can use setView or flyTo
map.setView([latitude, longitude], map.getZoom());
},
(err) => {
console.error(err);
alert('Unable to retrieve your location');
}
);
}
return (
<CustomControl onClick={handleClick} title="Go to my location">
<IconCurrentLocation />
</CustomControl>
);
}