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