Working on drawer and header
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,56 +43,103 @@ 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}
|
||||
>
|
||||
<Box mb='lg'>
|
||||
{metar && metar.flight_category && (
|
||||
<Group
|
||||
justify='space-between'
|
||||
mb='md'
|
||||
style={{
|
||||
backgroundColor: '#272f38',
|
||||
borderTop: '1px solid #1a242f',
|
||||
borderBottom: '1px solid #1a242f',
|
||||
padding: '10px'
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: metarColor }}>{metar.flight_category}</Text>
|
||||
<Tooltip zIndex={1001} label={new Date(metar.observation_time).toLocaleString()}>
|
||||
<TimeSince date={metar.observation_time} />
|
||||
</Tooltip>
|
||||
</Group>
|
||||
)}
|
||||
<Tabs variant={'outline'} defaultValue={'info'}>
|
||||
<TabsList grow>
|
||||
<Tabs.Tab value={'info'}>Info</Tabs.Tab>
|
||||
<Tabs.Tab value={'weather'}>Weather</Tabs.Tab>
|
||||
</TabsList>
|
||||
<Tabs.Panel value={'info'}><AirportInfo airport={airport}/></Tabs.Panel>
|
||||
{airport.latest_metar && (
|
||||
<Tabs.Panel value={'weather'}><WeatherInfo metar={airport.latest_metar} /></Tabs.Panel>
|
||||
)}
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Drawer>
|
||||
<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
|
||||
justify='space-between'
|
||||
mb='md'
|
||||
style={{
|
||||
backgroundColor: '#272f38',
|
||||
borderTop: '1px solid #1a242f',
|
||||
borderBottom: '1px solid #1a242f',
|
||||
padding: '10px'
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</Group>
|
||||
)}
|
||||
<Tabs variant={'outline'} defaultValue={'info'}>
|
||||
<TabsList grow>
|
||||
<Tabs.Tab value={'info'}>Info</Tabs.Tab>
|
||||
<Tabs.Tab value={'weather'}>Weather</Tabs.Tab>
|
||||
</TabsList>
|
||||
<Tabs.Panel value={'info'}><AirportInfo airport={airport}/></Tabs.Panel>
|
||||
{airport.latest_metar && (
|
||||
<Tabs.Panel value={'weather'}><WeatherInfo metar={airport.latest_metar} /></Tabs.Panel>
|
||||
)}
|
||||
</Tabs>
|
||||
</Box>
|
||||
</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 inputDate = new Date(date);
|
||||
// @ts-expect-error doing arithmetic with dates
|
||||
const seconds = Math.floor((new Date() - inputDate) / 1000);
|
||||
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>;
|
||||
} 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>;
|
||||
if (seconds < 60) {
|
||||
const content = seconds + (seconds === 1 ? " second ago" : " seconds ago");
|
||||
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 ref={ref} style={{ color: minutes >= 60 ? '#fca903' : undefined, userSelect: 'none' }}>{content}</Text>;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,28 +131,33 @@ 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>*/}
|
||||
<Group align='center' gap='xs'>
|
||||
{user ? (
|
||||
<HeaderUser user={user} profilePicture={undefined} logout={logoutUser} />
|
||||
) : (
|
||||
<Group className={'user'}>
|
||||
<Button variant='default' onClick={() => modalToggle('login')}>
|
||||
Login
|
||||
</Button>
|
||||
<Button onClick={() => modalToggle('register')}>Signup</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
{!isMobile && (
|
||||
<Group align='center' gap='xs'>
|
||||
<Autocomplete placeholder={'Enter airport name or ICAO'} limit={5} />
|
||||
{user ? (
|
||||
<HeaderUser user={user} profilePicture={undefined} logout={logoutUser} />
|
||||
) : (
|
||||
<Group className={'user'}>
|
||||
<Button variant='default' onClick={() => modalToggle('login')}>
|
||||
Login
|
||||
</Button>
|
||||
<Button onClick={() => modalToggle('register')}>Signup</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
{isMobile && (
|
||||
<Burger opened={opened} onClick={toggle} size='sm' />
|
||||
)}
|
||||
</Group>
|
||||
</header>
|
||||
</Box>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user