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 AirportDrawer from '@components/AirportDrawer.tsx';
import { getWeatherMapUrl } from '@lib/rainViewer.ts'; import { getWeatherMapUrl } from '@lib/rainViewer.ts';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
// import { createRoot } from 'react-dom/client';
import { UnstyledButton } from '@mantine/core'; import { UnstyledButton } from '@mantine/core';
import { IconBuildingAirport, IconRadar } from '@tabler/icons-react';
// Fix Leaflet's default icon path issues with Webpack // Fix Leaflet's default icon path issues with Webpack
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-expect-error
@@ -41,6 +43,63 @@ const layerMap: LayerInfo[] = [
const defaultZoom = 6; const defaultZoom = 6;
const defaultCenter: L.LatLngExpression = [38.944444, -77.455833]; 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() { function App() {
const [airport, setAirport] = useState<Airport | null>(null); const [airport, setAirport] = useState<Airport | null>(null);
const [rainViewerUrl, setRainViewerUrl] = useState<string | null>(null); const [rainViewerUrl, setRainViewerUrl] = useState<string | null>(null);
@@ -117,20 +176,26 @@ function App() {
<ZoomControl position={'bottomright'} /> <ZoomControl position={'bottomright'} />
<AirportLayer setAirport={setAirport} selectedLayer={selectedLayer} showNoMetar={showNoMetar} /> <AirportLayer setAirport={setAirport} selectedLayer={selectedLayer} showNoMetar={showNoMetar} />
<BaseLayerChangeHandler /> <BaseLayerChangeHandler />
{/*<CustomControl*/}
{/* toggleRadar={toggleRadar}*/}
{/* showRadar={showRadar}*/}
{/* toggleShowNoMetar={toggleShowNoMetar}*/}
{/* showNoMetar={showNoMetar}*/}
{/*/>*/}
</MapContainer> </MapContainer>
<UnstyledButton <UnstyledButton
onClick={toggleShowNoMetar} onClick={toggleShowNoMetar}
style={{ bottom: '120px' }} style={{ bottom: '120px' }}
className={`map-button ${showNoMetar ? 'active' : ''}`} className={`map-button ${showNoMetar ? 'active' : ''}`}
> >
U <IconBuildingAirport />
</UnstyledButton> </UnstyledButton>
<UnstyledButton <UnstyledButton
onClick={toggleRadar} onClick={toggleRadar}
style={{ bottom: '80px' }} style={{ bottom: '80px' }}
className={`map-button ${showRadar ? 'active' : ''}`} className={`map-button ${showRadar ? 'active' : ''}`}
> >
R <IconRadar />
</UnstyledButton> </UnstyledButton>
</div> </div>
</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 { Airport, AirportCategory } from '@lib/airport.types.ts';
import { getMarkerColor, Metar } from '@lib/metar.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 { getMetars } from '@lib/metar.ts';
import { useMediaQuery } from '@mantine/hooks'; import { useMediaQuery } from '@mantine/hooks';
@@ -43,19 +43,23 @@ export default function AirportDrawer({
const metarColor = getMarkerColor(metar?.flight_category || 'UNKN'); const metarColor = getMarkerColor(metar?.flight_category || 'UNKN');
return ( return (
<Drawer <Drawer.Root
opened={true} opened={true}
onClose={() => setAirport(null)} onClose={() => setAirport(null)}
title={airport.name}
withinPortal withinPortal
zIndex={1000} zIndex={1000}
styles={{ root: { padding: 0, margin: 0, width: 0, height: 0 } }} styles={{ root: { padding: 0, margin: 0, width: 0, height: 0 } }}
padding='md' padding='md'
size={isMobile ? '100%' : 'md'} size={isMobile ? '100%' : 'md'}
position='left' position='left'
withOverlay={false}
closeOnClickOutside={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'> <Box mb='lg'>
{metar && metar.flight_category && ( {metar && metar.flight_category && (
<Group <Group
@@ -68,7 +72,10 @@ export default function AirportDrawer({
padding: '10px' 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()}> <Tooltip zIndex={1001} label={new Date(metar.observation_time).toLocaleString()}>
<TimeSince date={metar.observation_time} /> <TimeSince date={metar.observation_time} />
</Tooltip> </Tooltip>
@@ -85,14 +92,54 @@ export default function AirportDrawer({
)} )}
</Tabs> </Tabs>
</Box> </Box>
</Drawer> </Drawer.Body>
</Drawer.Content>
</Drawer.Root>
); );
} }
function AirportInfo({ airport }: { airport: Airport }) { function AirportInfo({ airport }: { airport: Airport }) {
return (<div> return (<div>
<Text>ICAO: {airport.icao}</Text> <div style={{
<Text>Category: {airportCategoryToText(airport.category)}</Text> 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>); </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); const inputDate = new Date(date);
// @ts-expect-error doing arithmetic with dates // @ts-expect-error doing arithmetic with dates
const seconds = Math.floor((new Date() - inputDate) / 1000); const seconds = Math.floor((new Date() - inputDate) / 1000);
if (seconds < 60) { if (seconds < 60) {
const content = seconds + (seconds === 1 ? " second ago" : " seconds ago"); const content = seconds + (seconds === 1 ? " second ago" : " seconds ago");
return <Text>{content}</Text>; return <Text ref={ref} style={{ userSelect: 'none' }} >{content}</Text>;
} else { } else {
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);
const content = minutes + (minutes === 1 ? " minute ago" : " minutes ago"); const content = minutes + (minutes === 1 ? " minute ago" : " minutes ago");
// If more than 60 minutes have passed, set the text color to yellow // 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={{ eventHandlers={{
click: () => setAirport(airport), click: () => setAirport(airport),
mouseover: () => markerRef.current?.openPopup(), 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} {airport.icao} - {airport.name}
</Popup> </Popup>
</Marker> </Marker>
@@ -47,7 +47,7 @@ function createCustomIcon(airport: Airport, selectedLayer: LayerInfo): L.DivIcon
height: 14px; height: 14px;
border-radius: 50%; border-radius: 50%;
border: 2px solid black; border: 2px solid black;
background-color: ${selectedLayer.markerOutline}; background-color: white;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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)); border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
} }
.user {
user-select: none;
}
.inner { .inner {
height: 56px; height: 56px;
display: flex; display: flex;

View File

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

View File

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

View File

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