Working on drawer and header

This commit is contained in:
2025-04-17 18:20:54 -04:00
parent 3aa8954626
commit 20d5bf26de
7 changed files with 197 additions and 72 deletions

View File

@@ -13,7 +13,9 @@ import { Airport } from '@lib/airport.types.ts';
import AirportDrawer from '@components/AirportDrawer.tsx';
import { getWeatherMapUrl } from '@lib/rainViewer.ts';
import Cookies from 'js-cookie';
// import { createRoot } from 'react-dom/client';
import { UnstyledButton } from '@mantine/core';
import { IconBuildingAirport, IconRadar } from '@tabler/icons-react';
// Fix Leaflet's default icon path issues with Webpack
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
@@ -41,6 +43,63 @@ const layerMap: LayerInfo[] = [
const defaultZoom = 6;
const defaultCenter: L.LatLngExpression = [38.944444, -77.455833];
// function CustomControl({ toggleRadar, showRadar, toggleShowNoMetar, showNoMetar }) {
// const map = useMap();
//
// useEffect(() => {
// const CustomLeafletControl = L.Control.extend({
// options: { position: 'bottomright' },
// onAdd: function () {
// // Create a container for the control
// const container = L.DomUtil.create('div', 'leaflet-bar custom-control');
//
// // Radar button
// const radarButton = L.DomUtil.create('button', 'control-button radar-button', container);
// // radarButton.innerHTML = 'Radar';
// // if (showRadar) {
// // radarButton.classList.add('active');
// // }
// const radarRoot = createRoot(radarButton);
// radarRoot.render(
// <IconRadar
// style={{ width: '24px', height: '24px', color: showRadar ? 'blue' : 'black' }}
// />
// );
// // Prevent click events from propagating to the map
// L.DomEvent.disableClickPropagation(radarButton);
// L.DomEvent.on(radarButton, 'click', (e) => {
// L.DomEvent.stopPropagation(e);
// toggleRadar();
// });
//
// // Airports (no METARs) button
// const airportButton = L.DomUtil.create('button', 'control-button', container);
// airportButton.innerHTML = 'Airports';
// if (showNoMetar) {
// airportButton.classList.add('active');
// }
// L.DomEvent.disableClickPropagation(airportButton);
// L.DomEvent.on(airportButton, 'click', (e) => {
// L.DomEvent.stopPropagation(e);
// toggleShowNoMetar();
// });
//
// return container;
// }
// });
//
// const customControl = new CustomLeafletControl();
// customControl.addTo(map);
//
// // Remove control on cleanup
// return () => {
// customControl.remove();
// };
// }, [map, toggleRadar, toggleShowNoMetar, showRadar, showNoMetar]);
//
// return null;
// }
function App() {
const [airport, setAirport] = useState<Airport | null>(null);
const [rainViewerUrl, setRainViewerUrl] = useState<string | null>(null);
@@ -117,20 +176,26 @@ function App() {
<ZoomControl position={'bottomright'} />
<AirportLayer setAirport={setAirport} selectedLayer={selectedLayer} showNoMetar={showNoMetar} />
<BaseLayerChangeHandler />
{/*<CustomControl*/}
{/* toggleRadar={toggleRadar}*/}
{/* showRadar={showRadar}*/}
{/* toggleShowNoMetar={toggleShowNoMetar}*/}
{/* showNoMetar={showNoMetar}*/}
{/*/>*/}
</MapContainer>
<UnstyledButton
onClick={toggleShowNoMetar}
style={{ bottom: '120px' }}
className={`map-button ${showNoMetar ? 'active' : ''}`}
>
U
<IconBuildingAirport />
</UnstyledButton>
<UnstyledButton
onClick={toggleRadar}
style={{ bottom: '80px' }}
className={`map-button ${showRadar ? 'active' : ''}`}
>
R
<IconRadar />
</UnstyledButton>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { Box, Drawer, Group, Tabs, TabsList, Text, Tooltip } from '@mantine/core';
import { Badge, Box, Divider, Drawer, Group, Tabs, TabsList, Text, Tooltip } from '@mantine/core';
import { Airport, AirportCategory } from '@lib/airport.types.ts';
import { getMarkerColor, Metar } from '@lib/metar.types.ts';
import { useEffect, useState } from 'react';
import { forwardRef, useEffect, useState } from 'react';
import { getMetars } from '@lib/metar.ts';
import { useMediaQuery } from '@mantine/hooks';
@@ -43,19 +43,23 @@ export default function AirportDrawer({
const metarColor = getMarkerColor(metar?.flight_category || 'UNKN');
return (
<Drawer
<Drawer.Root
opened={true}
onClose={() => setAirport(null)}
title={airport.name}
withinPortal
zIndex={1000}
styles={{ root: { padding: 0, margin: 0, width: 0, height: 0 } }}
padding='md'
size={isMobile ? '100%' : 'md'}
position='left'
withOverlay={false}
closeOnClickOutside={false}
>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title><Text size={'xl'}>{airport.name}</Text></Drawer.Title>
<Drawer.CloseButton />
</Drawer.Header>
<Drawer.Body>
<Box mb='lg'>
{metar && metar.flight_category && (
<Group
@@ -68,7 +72,10 @@ export default function AirportDrawer({
padding: '10px'
}}
>
<Text style={{ color: metarColor }}>{metar.flight_category}</Text>
<Badge size="lg" color={metarColor}>
{metar.flight_category}
</Badge>
{/*<Text style={{ color: metarColor }}>{metar.flight_category}</Text>*/}
<Tooltip zIndex={1001} label={new Date(metar.observation_time).toLocaleString()}>
<TimeSince date={metar.observation_time} />
</Tooltip>
@@ -85,14 +92,54 @@ export default function AirportDrawer({
)}
</Tabs>
</Box>
</Drawer>
</Drawer.Body>
</Drawer.Content>
</Drawer.Root>
);
}
function AirportInfo({ airport }: { airport: Airport }) {
return (<div>
<Text>ICAO: {airport.icao}</Text>
<Text>Category: {airportCategoryToText(airport.category)}</Text>
<div style={{
display: 'flex',
justifyContent: 'space-between',
padding: 'var(--mantine-spacing-sm) var(--mantine-spacing-lg)',
borderTop: '1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5))'
}}>
<div>
<Text size="xs" color="dimmed">
ICAO
</Text>
<Text fw={500} size="sm">
{airport.icao}
</Text>
</div>
<div>
<Text size="xs" color="dimmed">
IATA
</Text>
<Text fw={500} size="sm">
{airport.iata}
</Text>
</div>
<div>
<Text size="xs" color="dimmed">
Local
</Text>
<Text fw={500} size="sm">
{airport.local}
</Text>
</div>
<div>
<Text size="xs" color="dimmed">
Category
</Text>
<Text fw={500} size="sm">
{airportCategoryToText(airport.category)}
</Text>
</div>
</div>
<Divider />
</div>);
}
@@ -121,18 +168,20 @@ function airportCategoryToText(category: AirportCategory): string {
}
}
function TimeSince({ date }: { date: string }) {
const TimeSince = forwardRef<HTMLParagraphElement, { date: string }>(
({ date }, ref) => {
const inputDate = new Date(date);
// @ts-expect-error doing arithmetic with dates
const seconds = Math.floor((new Date() - inputDate) / 1000);
if (seconds < 60) {
const content = seconds + (seconds === 1 ? " second ago" : " seconds ago");
return <Text>{content}</Text>;
return <Text ref={ref} style={{ userSelect: 'none' }} >{content}</Text>;
} else {
const minutes = Math.floor(seconds / 60);
const content = minutes + (minutes === 1 ? " minute ago" : " minutes ago");
// If more than 60 minutes have passed, set the text color to yellow
return <Text style={{ color: minutes >= 60 ? '#fca903' : undefined }}>{content}</Text>;
return <Text ref={ref} style={{ color: minutes >= 60 ? '#fca903' : undefined, userSelect: 'none' }}>{content}</Text>;
}
}
);

View File

@@ -28,10 +28,10 @@ export default function AirportMarker({
eventHandlers={{
click: () => setAirport(airport),
mouseover: () => markerRef.current?.openPopup(),
mouseout: () => markerRef.current?.closePopup()
mouseout: () => markerRef.current?.closePopup(),
}}
>
<Popup closeButton={false} autoPan={false}>
<Popup closeButton={false} autoPan={false} interactive={false}>
{airport.icao} - {airport.name}
</Popup>
</Marker>
@@ -47,7 +47,7 @@ function createCustomIcon(airport: Airport, selectedLayer: LayerInfo): L.DivIcon
height: 14px;
border-radius: 50%;
border: 2px solid black;
background-color: ${selectedLayer.markerOutline};
background-color: white;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -5,6 +5,10 @@
border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.user {
user-select: none;
}
.inner {
height: 56px;
display: flex;

View File

@@ -19,7 +19,7 @@ export default function HeaderUser({ user, profilePicture, logout }: HeaderUserP
<UnstyledButton>
<Group>
<Avatar src={profilePicture ? URL.createObjectURL(profilePicture) : undefined} />
<div style={{ flex: 1 }}>
<div tabIndex={-1} style={{ flex: 1, userSelect: 'none' }}>
<Text size='sm' fw={500}>
{user.first_name} {user.last_name}
</Text>

View File

@@ -1,5 +1,5 @@
import { Avatar, Box, Burger, Button, Group, Text } from '@mantine/core';
import { useDisclosure, useToggle } from '@mantine/hooks';
import { Autocomplete, Avatar, Box, Burger, Button, Group, Text } from '@mantine/core';
import { useDisclosure, useMediaQuery, useToggle } from '@mantine/hooks';
import classes from './Header.module.css';
import { HeaderModal } from '@components/Header/HeaderModal.tsx';
import { notifications } from '@mantine/notifications';
@@ -18,6 +18,7 @@ export function Header() {
const { user, setUser } = useUserContext();
const [opened, { toggle }] = useDisclosure(false);
const [modalType, modalToggle] = useToggle([undefined, 'login', 'register', 'reset']);
const isMobile = useMediaQuery('(max-width: 768px)');
// const [active, setActive] = useState(links[0].link);
// const navItems = links.map((link) => (
@@ -130,17 +131,18 @@ export function Header() {
<Box>
<header className={classes.header}>
<Group justify='space-between' h='100%'>
<Burger opened={opened} onClick={toggle} hiddenFrom='sm' size='sm' />
<Group align='center' gap='xs'>
<Link to='/'>
<Avatar src='/logo.svg' alt='logo' onClick={toggle} />
</Link>
<Text>Aviation Data</Text>
<Text size={'xl'}>Aviation Data</Text>
</Group>
{/*<Group gap={5} visibleFrom='sm' className={classes.navGroup}>*/}
{/* {navItems}*/}
{/*</Group>*/}
{!isMobile && (
<Group align='center' gap='xs'>
<Autocomplete placeholder={'Enter airport name or ICAO'} limit={5} />
{user ? (
<HeaderUser user={user} profilePicture={undefined} logout={logoutUser} />
) : (
@@ -152,6 +154,10 @@ export function Header() {
</Group>
)}
</Group>
)}
{isMobile && (
<Burger opened={opened} onClick={toggle} size='sm' />
)}
</Group>
</header>
</Box>

View File

@@ -11,6 +11,7 @@ export default defineConfig({
'@assets': path.resolve(__dirname, './src/assets'),
'@components': path.resolve(__dirname, './src/components'),
'@lib': path.resolve(__dirname, './src/lib'),
'@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs',
}
},
server: {