Updated airport data format, dataset, and queries
This commit is contained in:
@@ -12,7 +12,7 @@ export async function getAirport({ icao }: GetAirportProps): Promise<GetAirportR
|
||||
|
||||
interface GetAirportsProps {
|
||||
bounds?: Bounds;
|
||||
category?: string;
|
||||
categories?: string[];
|
||||
search?: string;
|
||||
order_field?: AirportOrderField;
|
||||
order_by?: 'asc' | 'desc';
|
||||
@@ -22,7 +22,7 @@ interface GetAirportsProps {
|
||||
|
||||
export async function getAirports({
|
||||
bounds,
|
||||
category,
|
||||
categories,
|
||||
search,
|
||||
order_field,
|
||||
order_by,
|
||||
@@ -33,7 +33,7 @@ export async function getAirports({
|
||||
bounds: bounds
|
||||
? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}`
|
||||
: undefined,
|
||||
category: category ?? undefined,
|
||||
categories: categories ?? undefined,
|
||||
search: search ?? undefined,
|
||||
order_field: order_field ?? undefined,
|
||||
order_by: order_by ?? undefined,
|
||||
@@ -63,11 +63,11 @@ export async function updateAirport({ airport }: { airport: Airport }): Promise<
|
||||
return response?.json() || { data: undefined };
|
||||
}
|
||||
|
||||
export async function importAirports(payload: File): Promise<any> {
|
||||
export async function importAirports(payload: File): Promise<boolean> {
|
||||
const data = new FormData();
|
||||
data.append('data', payload);
|
||||
const response = await postRequest('airports/import', data, {
|
||||
type: 'form'
|
||||
});
|
||||
return response?.status == 200;
|
||||
return response ? response.status === 200 : false;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ export enum AirportOrderField {
|
||||
ISO_COUNTRY = 'iso_country',
|
||||
ISO_REGION = 'iso_region',
|
||||
MUNICIPALITY = 'municipality',
|
||||
GPS_CODE = 'gps_code',
|
||||
IATA_CODE = 'iata_code',
|
||||
LOCAL_CODE = 'local_code',
|
||||
}
|
||||
@@ -44,19 +43,15 @@ export interface Coordinate {
|
||||
export interface Airport {
|
||||
icao: string;
|
||||
category: AirportCategory;
|
||||
full_name: string;
|
||||
name: string;
|
||||
elevation_ft: number;
|
||||
iso_country: string;
|
||||
iso_region: string;
|
||||
municipality: string;
|
||||
gps_code: string;
|
||||
iata_code: string;
|
||||
local_code: string;
|
||||
point: {
|
||||
x: number;
|
||||
y: number;
|
||||
srid: number;
|
||||
};
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
latest_metar?: Metar;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Page({ params }: { params: { icao: string } }) {
|
||||
return (
|
||||
<>
|
||||
<div className=''>
|
||||
<h3 className=''>{airport.full_name}</h3>
|
||||
<h3 className=''>{airport.name}</h3>
|
||||
{metar && <SkyConditions metar={metar} />}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import RecoilRootWrapper from '@app/recoil-root-wrapper';
|
||||
import Sidebar from '@/components/Sidebar';
|
||||
import Header from '@/components/Header';
|
||||
import { Inter } from 'next/font/google';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
@@ -8,6 +7,7 @@ import { ModalsProvider } from '@mantine/modals';
|
||||
import 'styles/globals.css';
|
||||
import 'styles/leaflet.css';
|
||||
import '@mantine/core/styles.css';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Aviation Weather',
|
||||
@@ -23,15 +23,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<title>Aviation Weather</title>
|
||||
</head>
|
||||
<body className={`${inter.className} wrapper h-full`}>
|
||||
<RecoilRootWrapper>
|
||||
<MantineProvider>
|
||||
<ModalsProvider>
|
||||
<MantineProvider>
|
||||
<Notifications />
|
||||
<ModalsProvider>
|
||||
<RecoilRootWrapper>
|
||||
<Header />
|
||||
<Sidebar />
|
||||
{children}
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</RecoilRootWrapper>
|
||||
</RecoilRootWrapper>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Text, Button, Card, Group, Pagination, Table, TextInput, rem, UnstyledB
|
||||
import { HiChevronUp, HiChevronDown, HiSelector } from "react-icons/hi";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CiSearch } from "react-icons/ci";
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
|
||||
export default function AirportTablePanel({ setAirport }: { setAirport: (airport: Airport) => void }) {
|
||||
@@ -37,12 +38,11 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Table.Td>{airport.icao}</Table.Td>
|
||||
<Table.Td>{airport.full_name}</Table.Td>
|
||||
<Table.Td>{airport.name}</Table.Td>
|
||||
<Table.Td>{airportCategoryToText(airport.category)}</Table.Td>
|
||||
<Table.Td>{airport.iso_country}</Table.Td>
|
||||
<Table.Td>{airport.iso_region}</Table.Td>
|
||||
<Table.Td>{airport.municipality}</Table.Td>
|
||||
<Table.Td>{airport.gps_code}</Table.Td>
|
||||
<Table.Td>{airport.iata_code}</Table.Td>
|
||||
<Table.Td>{airport.local_code}</Table.Td>
|
||||
</Table.Tr>
|
||||
@@ -61,12 +61,11 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>ICAO</Table.Th>
|
||||
<Table.Th>Full Name</Table.Th>
|
||||
<Table.Th>Name</Table.Th>
|
||||
<Table.Th>Category</Table.Th>
|
||||
<Table.Th>ISO Country</Table.Th>
|
||||
<Table.Th>ISO Region</Table.Th>
|
||||
<Table.Th>Municipality</Table.Th>
|
||||
<Table.Th>GPS Code</Table.Th>
|
||||
<Table.Th>IATA Code</Table.Th>
|
||||
<Table.Th>Local Code</Table.Th>
|
||||
</Table.Tr>
|
||||
@@ -83,8 +82,17 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport
|
||||
<Space mr={'sm'}>
|
||||
<PanelFileButton accept={'.json'} onChange={async (payload) => {
|
||||
if (payload instanceof File) {
|
||||
await importAirports(payload);
|
||||
await getAirportData();
|
||||
const response = await importAirports(payload);
|
||||
if (response) {
|
||||
await getAirportData();
|
||||
} else {
|
||||
notifications.show({
|
||||
title: `Failed to import airports`,
|
||||
message: `Please try again.`,
|
||||
color: 'red',
|
||||
autoClose: 2000
|
||||
});
|
||||
}
|
||||
}
|
||||
}}>
|
||||
Import
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getAirport, getAirports } from '@/api/airport';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Autocomplete, Avatar, Button, Card, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core';
|
||||
import './header.css';
|
||||
import { refresh, refreshLoggedIn, logout } from '@/api/auth';
|
||||
@@ -17,6 +16,7 @@ import { favoritesState } from '@/state/user';
|
||||
import { coordinatesState, zoomState } from '@/state/map';
|
||||
|
||||
export default function Header() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [airports, setAirports] = useState<{ key: string; value: string; label: string }[]>([]);
|
||||
const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']);
|
||||
@@ -24,7 +24,6 @@ export default function Header() {
|
||||
const [favorites, setFavorites] = useRecoilState(favoritesState);
|
||||
const [refreshId, setRefreshId] = useState<NodeJS.Timeout | undefined>(undefined);
|
||||
const [profilePicture, setProfilePicture] = useState<File | null>(null);
|
||||
const router = useRouter();
|
||||
const [coordinates, setCoordinates] = useRecoilState(coordinatesState);
|
||||
const [zoom, setZoom] = useRecoilState(zoomState);
|
||||
|
||||
@@ -49,6 +48,7 @@ export default function Header() {
|
||||
}
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}, [user]);
|
||||
|
||||
async function onChange(value: string) {
|
||||
@@ -58,7 +58,7 @@ export default function Header() {
|
||||
airportData.data.map((airport) => ({
|
||||
key: airport.icao,
|
||||
value: airport.icao,
|
||||
label: `${airport.icao} - ${airport.full_name}`
|
||||
label: `${airport.icao} - ${airport.name}`
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export default function Header() {
|
||||
async function onClick(value: string) {
|
||||
const airport = await getAirport({ icao: value });
|
||||
if (airport) {
|
||||
setCoordinates({ lat: airport.data.point.y, lon: airport.data.point.x });
|
||||
setCoordinates({ lat: airport.data.latitude, lon: airport.data.longitude });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ export default function Header() {
|
||||
toggle={toggle}
|
||||
setFavorites={setFavorites}
|
||||
refreshId={refreshId}
|
||||
loading={loading}
|
||||
/>
|
||||
</nav>
|
||||
<HeaderModal
|
||||
@@ -128,114 +129,121 @@ interface UserSectionProps {
|
||||
toggle: (type: string) => void;
|
||||
setFavorites: (favorites: string[]) => void;
|
||||
refreshId: NodeJS.Timeout | undefined;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
function UserSection({ profilePicture, setProfilePicture, setFavorites, refreshId, toggle }: UserSectionProps) {
|
||||
function UserSection({ profilePicture, setProfilePicture, setFavorites, refreshId, toggle, loading }: UserSectionProps) {
|
||||
const [user, setUser] = useRecoilState(userState);
|
||||
|
||||
return (
|
||||
<div className='user-section'>
|
||||
{user ? (
|
||||
<Menu shadow='md' width={200} openDelay={100} closeDelay={400}>
|
||||
<Menu.Target>
|
||||
<UnstyledButton className='user user-button'>
|
||||
<Group>
|
||||
<Avatar src={profilePicture ? URL.createObjectURL(profilePicture) : undefined} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size='sm' fw={500}>
|
||||
{user.first_name} {user.last_name}
|
||||
</Text>
|
||||
<Text c='dimmed' size='xs' style={{ textTransform: 'uppercase' }}>
|
||||
{user.role}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown p={0}>
|
||||
<Card>
|
||||
<Card.Section h={140} style={{ backgroundColor: '#4481e3' }} />
|
||||
<FileButton
|
||||
onChange={(payload) => {
|
||||
if (payload) {
|
||||
setPicture(payload).then((response) => {
|
||||
if (response) {
|
||||
setProfilePicture(payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
accept='image/png,image/jpeg,image/jpg'
|
||||
multiple={false}
|
||||
>
|
||||
{(props) => (
|
||||
<Avatar
|
||||
{...props}
|
||||
component='button'
|
||||
size={80}
|
||||
radius={80}
|
||||
mx={'auto'}
|
||||
mt={-30}
|
||||
style={{ cursor: 'pointer' }}
|
||||
bg={profilePicture ? 'transparent' : 'white'}
|
||||
src={profilePicture ? URL.createObjectURL(profilePicture) : undefined}
|
||||
/>
|
||||
)}
|
||||
</FileButton>
|
||||
<Text ta='center' fz='lg' fw={500} mt='sm'>
|
||||
{user.first_name} {user.last_name}
|
||||
</Text>
|
||||
<Text ta='center' fz='sm' c='dimmed' style={{ textTransform: 'uppercase' }}>
|
||||
{user.role}
|
||||
</Text>
|
||||
<Grid mt='xl'>
|
||||
<Grid.Col span={6}>
|
||||
<Link href='/profile'>
|
||||
<Button fullWidth radius='md' size='xs' variant='default'>
|
||||
Profile
|
||||
</Button>
|
||||
</Link>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Button
|
||||
fullWidth
|
||||
radius='md'
|
||||
size='xs'
|
||||
variant='default'
|
||||
onClick={async () => {
|
||||
await logout();
|
||||
Cookies.remove('logged_in');
|
||||
setUser(undefined);
|
||||
setFavorites([]);
|
||||
setProfilePicture(null);
|
||||
if (refreshId) {
|
||||
clearInterval(refreshId);
|
||||
{loading ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
{user ? (
|
||||
<Menu shadow='md' width={200} openDelay={100} closeDelay={400}>
|
||||
<Menu.Target>
|
||||
<UnstyledButton className='user user-button'>
|
||||
<Group>
|
||||
<Avatar src={profilePicture ? URL.createObjectURL(profilePicture) : undefined} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size='sm' fw={500}>
|
||||
{user.first_name} {user.last_name}
|
||||
</Text>
|
||||
<Text c='dimmed' size='xs' style={{ textTransform: 'uppercase' }}>
|
||||
{user.role}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown p={0}>
|
||||
<Card>
|
||||
<Card.Section h={140} style={{ backgroundColor: '#4481e3' }} />
|
||||
<FileButton
|
||||
onChange={(payload) => {
|
||||
if (payload) {
|
||||
setPicture(payload).then((response) => {
|
||||
if (response) {
|
||||
setProfilePicture(payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
accept='image/png,image/jpeg,image/jpg'
|
||||
multiple={false}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
{user.role == 'admin' && (
|
||||
<Grid.Col span={12}>
|
||||
<Link href='/admin'>
|
||||
<Button fullWidth radius='md' size='xs' variant='default'>
|
||||
Administration
|
||||
{(props) => (
|
||||
<Avatar
|
||||
{...props}
|
||||
component='button'
|
||||
size={80}
|
||||
radius={80}
|
||||
mx={'auto'}
|
||||
mt={-30}
|
||||
style={{ cursor: 'pointer' }}
|
||||
bg={profilePicture ? 'transparent' : 'white'}
|
||||
src={profilePicture ? URL.createObjectURL(profilePicture) : undefined}
|
||||
/>
|
||||
)}
|
||||
</FileButton>
|
||||
<Text ta='center' fz='lg' fw={500} mt='sm'>
|
||||
{user.first_name} {user.last_name}
|
||||
</Text>
|
||||
<Text ta='center' fz='sm' c='dimmed' style={{ textTransform: 'uppercase' }}>
|
||||
{user.role}
|
||||
</Text>
|
||||
<Grid mt='xl'>
|
||||
<Grid.Col span={6}>
|
||||
<Link href='/profile'>
|
||||
<Button fullWidth radius='md' size='xs' variant='default'>
|
||||
Profile
|
||||
</Button>
|
||||
</Link>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Button
|
||||
fullWidth
|
||||
radius='md'
|
||||
size='xs'
|
||||
variant='default'
|
||||
onClick={async () => {
|
||||
await logout();
|
||||
Cookies.remove('logged_in');
|
||||
setUser(undefined);
|
||||
setFavorites([]);
|
||||
setProfilePicture(null);
|
||||
if (refreshId) {
|
||||
clearInterval(refreshId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</Link>
|
||||
</Grid.Col>
|
||||
)}
|
||||
</Grid>
|
||||
</Card>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
) : (
|
||||
<Group className='user'>
|
||||
<Button onClick={() => toggle('login')}>Login</Button>
|
||||
<Button variant='outline' onClick={() => toggle('register')}>
|
||||
Sign up
|
||||
</Button>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
{user.role == 'admin' && (
|
||||
<Grid.Col span={12}>
|
||||
<Link href='/admin'>
|
||||
<Button fullWidth radius='md' size='xs' variant='default'>
|
||||
Administration
|
||||
</Button>
|
||||
</Link>
|
||||
</Grid.Col>
|
||||
)}
|
||||
</Grid>
|
||||
</Card>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
) : (
|
||||
<Group className='user'>
|
||||
<Button onClick={() => toggle('login')}>Login</Button>
|
||||
<Button variant='outline' onClick={() => toggle('register')}>
|
||||
Sign up
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -53,6 +53,7 @@ export default function MapTiles() {
|
||||
northEast: { lat: ne.lat, lon: ne.lng },
|
||||
southWest: { lat: sw.lat, lon: sw.lng }
|
||||
},
|
||||
categories: ['small_airport', 'medium_airport', 'large_airport'],
|
||||
order_field: AirportOrderField.CATEGORY,
|
||||
order_by: 'asc',
|
||||
limit: 250,
|
||||
@@ -111,7 +112,7 @@ export default function MapTiles() {
|
||||
{airports.map((airport) => (
|
||||
<Marker
|
||||
key={airport.icao}
|
||||
position={[airport.point.y, airport.point.x]}
|
||||
position={[airport.latitude, airport.longitude]}
|
||||
icon={metarIcon(airport)}
|
||||
eventHandlers={{
|
||||
click: () => handleOpen(airport)
|
||||
@@ -119,7 +120,7 @@ export default function MapTiles() {
|
||||
>
|
||||
{!isOpen && (
|
||||
<Tooltip className='metar-tooltip' direction='top' offset={[5, -5]} opacity={1}>
|
||||
<b>{airport.icao}</b> - {airport.full_name}
|
||||
<b>{airport.icao}</b> - {airport.name}
|
||||
</Tooltip>
|
||||
)}
|
||||
</Marker>
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function MetarModal({ airport, isOpen, onClose }: MetarModalProps
|
||||
>
|
||||
<span className='title'>
|
||||
<Link href={`/airport/${airport.icao}`}>
|
||||
{airport.icao} {airport.full_name}
|
||||
{airport.icao} {airport.name}
|
||||
</Link>
|
||||
{isFavorite ? (
|
||||
<AiFillStar size={24} className='star' onClick={() => handleFavorite(false)} />
|
||||
|
||||
Reference in New Issue
Block a user