Updated queries/endpoints, made admin page
This commit is contained in:
@@ -1,38 +1,67 @@
|
||||
import { Bounds, GetAirportResponse, GetAirportsResponse } from './airport.types';
|
||||
import { getRequest } from '.';
|
||||
import { AirportOrderField, Bounds, GetAirportResponse, GetAirportsResponse } from './airport.types';
|
||||
import { getRequest, deleteRequest } from '.';
|
||||
|
||||
interface GetAirportProps {
|
||||
icao: string;
|
||||
}
|
||||
|
||||
export async function getAirport({ icao }: GetAirportProps): Promise<GetAirportResponse> {
|
||||
const response = await getRequest(`airports/${icao}`, {});
|
||||
const response = await getRequest(`airports/search/${icao}`);
|
||||
return response?.json() || { data: undefined };
|
||||
}
|
||||
|
||||
interface GetAirportsProps {
|
||||
bounds?: Bounds;
|
||||
category?: string;
|
||||
filter?: string;
|
||||
name?: string;
|
||||
order_field?: AirportOrderField;
|
||||
order_by?: 'asc' | 'desc';
|
||||
icao?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export async function getAirportsCount() {
|
||||
const response = await getRequest('airports/count');
|
||||
return response?.json() || { data: 0 };
|
||||
}
|
||||
|
||||
export async function getAirports({
|
||||
bounds,
|
||||
category,
|
||||
filter,
|
||||
name,
|
||||
icao,
|
||||
order_field,
|
||||
order_by,
|
||||
limit = 10,
|
||||
page = 1
|
||||
}: GetAirportsProps): Promise<GetAirportsResponse> {
|
||||
const response = await getRequest('airports', {
|
||||
const response = await getRequest('airports/search', {
|
||||
bounds: bounds
|
||||
? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}`
|
||||
: undefined,
|
||||
category: category ?? undefined,
|
||||
filter: filter ?? undefined,
|
||||
name: name ?? undefined,
|
||||
icao: icao ?? undefined,
|
||||
order_field: order_field ?? undefined,
|
||||
order_by: order_by ?? undefined,
|
||||
limit,
|
||||
page
|
||||
});
|
||||
return response?.json() || { data: [] };
|
||||
}
|
||||
|
||||
export async function removeAirport({ icao }: { icao?: string }): Promise<any> {
|
||||
let response
|
||||
if (icao) {
|
||||
response = await deleteRequest(`airports/remove/${icao}`);
|
||||
} else {
|
||||
response = await deleteRequest('airports/remove');
|
||||
}
|
||||
return response.status == 204;
|
||||
}
|
||||
|
||||
export async function importAirports(): Promise<any> {
|
||||
const response = await getRequest('airports/import');
|
||||
return response?.json() || { data: undefined };
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Metadata } from '.';
|
||||
import { Metar } from './metar.types';
|
||||
|
||||
export enum AirportCategory {
|
||||
@@ -6,6 +7,19 @@ export enum AirportCategory {
|
||||
LARGE = 'large_airport'
|
||||
}
|
||||
|
||||
export enum AirportOrderField {
|
||||
ICAO = 'icao',
|
||||
NAME = 'name',
|
||||
CATEGORY = 'category',
|
||||
CONTINENT = 'continent',
|
||||
ISO_COUNTRY = 'iso_country',
|
||||
ISO_REGION = 'iso_region',
|
||||
MUNICIPALITY = 'municipality',
|
||||
GPS_CODE = 'gps_code',
|
||||
IATA_CODE = 'iata_code',
|
||||
LOCAL_CODE = 'local_code',
|
||||
}
|
||||
|
||||
export interface Bounds {
|
||||
northEast: Coordinate;
|
||||
southWest: Coordinate;
|
||||
@@ -20,7 +34,7 @@ export interface Airport {
|
||||
icao: string;
|
||||
category: AirportCategory;
|
||||
full_name: string;
|
||||
elevation_ft: string;
|
||||
elevation_ft: number;
|
||||
continent: string;
|
||||
iso_country: string;
|
||||
iso_region: string;
|
||||
@@ -38,8 +52,10 @@ export interface Airport {
|
||||
|
||||
export interface GetAirportResponse {
|
||||
data: Airport;
|
||||
meta: Metadata;
|
||||
}
|
||||
|
||||
export interface GetAirportsResponse {
|
||||
data: Airport[];
|
||||
meta: Metadata;
|
||||
}
|
||||
|
||||
28
ui/src/app/admin/page.tsx
Normal file
28
ui/src/app/admin/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import { Airport } from "@/api/airport.types";
|
||||
import AirportTablePanel from "@/components/Admin/AirportTablePanel";
|
||||
import CreateAirportPanel from "@/components/Admin/CreateAirportPanel";
|
||||
import { Container, Grid } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
const [airport, setAirport] = useState<Airport | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(airport);
|
||||
}, [airport]);
|
||||
|
||||
return <Container fluid>
|
||||
<Grid p={'lg'}>
|
||||
<Grid.Col span={12}>
|
||||
<AirportTablePanel setAirport={setAirport} />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<CreateAirportPanel airport={airport} setAirport={setAirport} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>;
|
||||
}
|
||||
|
||||
|
||||
117
ui/src/components/Admin/AirportTablePanel.tsx
Normal file
117
ui/src/components/Admin/AirportTablePanel.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { getAirports, importAirports, removeAirport } from "@/api/airport";
|
||||
import { Airport } from "@/api/airport.types";
|
||||
import { Text, Button, Card, Group, Pagination, ScrollArea, Table, TextInput, rem } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CiSearch } from "react-icons/ci";
|
||||
|
||||
|
||||
export default function AirportTablePanel({ setAirport }: { setAirport: (airport: Airport) => void }) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [airports, setAirports] = useState<Airport[]>([]);
|
||||
|
||||
async function getAirportData() {
|
||||
const response = await getAirports({
|
||||
page,
|
||||
limit: 100
|
||||
});
|
||||
setAirports(response.data);
|
||||
setTotalPages(response.meta.pages);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAirportData();
|
||||
}, [page, search]);
|
||||
|
||||
function handleSearchChange(event: any) {
|
||||
setSearch(event.currentTarget.value);
|
||||
}
|
||||
|
||||
const rows = airports.map((airport) => (
|
||||
<Table.Tr
|
||||
key={airport.icao}
|
||||
onClick={() => {
|
||||
console.log('here');
|
||||
setAirport(airport);
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Table.Td>{airport.icao}</Table.Td>
|
||||
<Table.Td>{airport.full_name}</Table.Td>
|
||||
<Table.Td>{airport.category}</Table.Td>
|
||||
<Table.Td>{airport.continent}</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.Td>{airport.point.x}</Table.Td>
|
||||
<Table.Td>{airport.point.y}</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
|
||||
return <Card shadow={'sm'} padding={'lg'} radius={'md'} withBorder>
|
||||
<TextInput
|
||||
placeholder="Search by ICAO"
|
||||
mb="md"
|
||||
leftSection={<CiSearch style={{ width: rem(16), height: rem(16) }} />}
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
<Table.ScrollContainer minWidth={500} h={500}>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>ICAO</Table.Th>
|
||||
<Table.Th>Full Name</Table.Th>
|
||||
<Table.Th>Category</Table.Th>
|
||||
<Table.Th>Continent</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.Th>Latitude</Table.Th>
|
||||
<Table.Th>Longitude</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
<Group>
|
||||
<Pagination value={page} total={totalPages} onChange={setPage} />
|
||||
<PanelButton onClick={async () => {
|
||||
await importAirports();
|
||||
await getAirportData();
|
||||
}}>
|
||||
Import
|
||||
</PanelButton>
|
||||
<PanelButton color={'red'} onClick={async () => {
|
||||
await removeAirport({});
|
||||
await getAirportData();
|
||||
}}>
|
||||
Remove All
|
||||
</PanelButton>
|
||||
</Group>
|
||||
</Card>
|
||||
}
|
||||
|
||||
function PanelButton({ children, color = 'blue', onClick }: {children: any, color?: string, onClick: () => Promise<void> }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
return <Button
|
||||
loading={loading}
|
||||
variant='light'
|
||||
color={color}
|
||||
mt={'md'}
|
||||
radius={'md'}
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
onClick().then(() => setLoading(false));
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
}
|
||||
159
ui/src/components/Admin/CreateAirportPanel.tsx
Normal file
159
ui/src/components/Admin/CreateAirportPanel.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { Airport, AirportCategory } from "@/api/airport.types";
|
||||
import { Card, TextInput, Select, Group, Flex, Space, Button } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function CreateAirportPanel({ airport, setAirport } : { airport?: Airport, setAirport: (airport: Airport | undefined) => void }) {
|
||||
const form = useForm<Airport>({
|
||||
initialValues: {
|
||||
icao: '',
|
||||
category: AirportCategory.SMALL,
|
||||
full_name: '',
|
||||
elevation_ft: 0,
|
||||
continent: '',
|
||||
iso_country: '',
|
||||
iso_region: '',
|
||||
municipality: '',
|
||||
gps_code: '',
|
||||
iata_code: '',
|
||||
local_code: '',
|
||||
point: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
srid: 4326
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log(airport);
|
||||
if (airport) {
|
||||
form.setValues(airport);
|
||||
}
|
||||
}, [airport]);
|
||||
|
||||
return <Card shadow={'sm'} padding={'lg'} radius={'md'} withBorder>
|
||||
Create Airport
|
||||
<form onSubmit={form.onSubmit((values) => {
|
||||
if (airport) {
|
||||
console.log('update');
|
||||
} else {
|
||||
console.log('create');
|
||||
}
|
||||
})}>
|
||||
<TextInput
|
||||
required
|
||||
label='ICAO'
|
||||
placeholder='KHEF'
|
||||
{...form.getInputProps('icao')}
|
||||
/>
|
||||
<Select
|
||||
required
|
||||
label='Category'
|
||||
placeholder='Select category'
|
||||
data={[
|
||||
{ value: AirportCategory.SMALL, label: 'Small' },
|
||||
{ value: AirportCategory.MEDIUM, label: 'Medium' },
|
||||
{ value: AirportCategory.LARGE, label: 'Large' },
|
||||
]}
|
||||
{...form.getInputProps('category')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label='Full Name'
|
||||
placeholder='Manassas Regional Airport/Harry P. Davis Field'
|
||||
{...form.getInputProps('full_name')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label='Elevation (ft)'
|
||||
placeholder='192'
|
||||
{...form.getInputProps('elevation_ft')}
|
||||
/>
|
||||
<Group>
|
||||
<TextInput
|
||||
required
|
||||
label='Continent'
|
||||
placeholder='NA'
|
||||
{...form.getInputProps('continent')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label='ISO Country'
|
||||
placeholder='US'
|
||||
{...form.getInputProps('iso_country')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label='ISO Region'
|
||||
placeholder='US-VA'
|
||||
{...form.getInputProps('iso_region')}
|
||||
/>
|
||||
</Group>
|
||||
<TextInput
|
||||
required
|
||||
label='Municipality'
|
||||
placeholder='Manassas'
|
||||
{...form.getInputProps('municipality')}
|
||||
/>
|
||||
<Group>
|
||||
<TextInput
|
||||
required
|
||||
label='GPS Code'
|
||||
placeholder='KHEF'
|
||||
{...form.getInputProps('gps_code')}
|
||||
/>
|
||||
<TextInput
|
||||
label='IATA Code'
|
||||
placeholder='MNZ'
|
||||
{...form.getInputProps('iata_code')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Local Code'
|
||||
placeholder='HEF'
|
||||
{...form.getInputProps('local_code')}
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<TextInput
|
||||
required
|
||||
label='Latitude'
|
||||
placeholder='38.72140121'
|
||||
{...form.getInputProps('point.x')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label='Longitude'
|
||||
placeholder='-77.51540375'
|
||||
{...form.getInputProps('point.y')}
|
||||
/>
|
||||
</Group>
|
||||
<Flex justify={'end'} mt={'sm'}>
|
||||
<Space mr={'sm'}>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='light'
|
||||
color='blue'
|
||||
radius={'md'}
|
||||
>
|
||||
{airport ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button
|
||||
type='button'
|
||||
variant='light'
|
||||
color='red'
|
||||
radius={'md'}
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
setAirport(undefined);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
</form>
|
||||
</Card>
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getAirports } from '@/api/airport';
|
||||
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';
|
||||
@@ -14,6 +14,7 @@ import { getFavorites, getPicture, setPicture } from '@/api/users';
|
||||
import { useToggle } from '@mantine/hooks';
|
||||
import { HeaderModal } from './HeaderModal';
|
||||
import { favoritesState } from '@/state/user';
|
||||
import { coordinatesState, zoomState } from '@/state/map';
|
||||
|
||||
export default function Header() {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
@@ -24,6 +25,8 @@ export default function Header() {
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !Cookies.get('logged_in')) {
|
||||
@@ -50,7 +53,7 @@ export default function Header() {
|
||||
|
||||
async function onChange(value: string) {
|
||||
setSearchValue(value);
|
||||
const airportData = await getAirports({ filter: value });
|
||||
const airportData = await getAirports({ name: value, icao: value });
|
||||
setAirports(
|
||||
airportData.data.map((airport) => ({
|
||||
key: airport.icao,
|
||||
@@ -60,9 +63,11 @@ export default function Header() {
|
||||
);
|
||||
}
|
||||
|
||||
function onClick(value: string) {
|
||||
router.push(`/airport/${value}`);
|
||||
setSearchValue('');
|
||||
async function onClick(value: string) {
|
||||
const airport = await getAirport({ icao: value });
|
||||
if (airport) {
|
||||
setCoordinates({ lat: airport.data.point.y, lon: airport.data.point.x });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { getAirports } from '@/api/airport';
|
||||
import { Airport } from '@/api/airport.types';
|
||||
import { Airport, AirportOrderField } from '@/api/airport.types';
|
||||
import { getMetars } from '@/api/metar';
|
||||
import { DivIcon, LatLngBounds } from 'leaflet';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -9,19 +9,22 @@ import ReactDOMServer from 'react-dom/server';
|
||||
import { Marker, TileLayer, Tooltip, useMap, useMapEvents } from 'react-leaflet';
|
||||
import MetarModal from './MetarModal';
|
||||
import { Avatar, MantineProvider } from '@mantine/core';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { coordinatesState, zoomState } from '@/state/map';
|
||||
|
||||
export default function MapTiles() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [airports, setAirports] = useState<Airport[]>([]);
|
||||
const [selectedAirport, setSelectedAirport] = useState<Airport | undefined>();
|
||||
const coordinates = useRecoilValue(coordinatesState);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [zoomLevel, setZoomLevel] = useState(8);
|
||||
const [zoom, setZoom] = useRecoilState(zoomState);
|
||||
// const [dragging, setDragging] = useState(false);
|
||||
const map = useMap();
|
||||
|
||||
const mapEvents = useMapEvents({
|
||||
zoomend: async () => {
|
||||
setZoomLevel(mapEvents.getZoom());
|
||||
setZoom(mapEvents.getZoom());
|
||||
await updateAirports(mapEvents.getBounds());
|
||||
},
|
||||
movestart: () => {
|
||||
@@ -33,6 +36,10 @@ export default function MapTiles() {
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
map.setView([coordinates.lat, coordinates.lon]);
|
||||
}, [coordinates]);
|
||||
|
||||
function handleOpen(airport: Airport) {
|
||||
setSelectedAirport(airport);
|
||||
setIsOpen(true);
|
||||
@@ -46,6 +53,8 @@ export default function MapTiles() {
|
||||
northEast: { lat: ne.lat, lon: ne.lng },
|
||||
southWest: { lat: sw.lat, lon: sw.lng }
|
||||
},
|
||||
order_field: AirportOrderField.CATEGORY,
|
||||
order_by: 'asc',
|
||||
limit: 100,
|
||||
page: 1
|
||||
});
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { MapContainer } from 'react-leaflet';
|
||||
import { MapContainer, useMap } from 'react-leaflet';
|
||||
import MapTiles from './MapTiles';
|
||||
import './metars.css';
|
||||
import { coordinatesState, zoomState } from '@/state/map';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export default function Map() {
|
||||
const coordinates = useRecoilValue(coordinatesState);
|
||||
const zoom = useRecoilValue(zoomState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MapContainer
|
||||
center={[38.7209, -77.5133]}
|
||||
zoom={8}
|
||||
center={[coordinates.lat, coordinates.lon]}
|
||||
zoom={zoom}
|
||||
maxZoom={14} // Zoomed in
|
||||
minZoom={3} // Zoomed out
|
||||
id='map-container'
|
||||
|
||||
12
ui/src/state/map.ts
Normal file
12
ui/src/state/map.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Coordinate } from '@/api/airport.types';
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const coordinatesState = atom({
|
||||
key: 'coordinatesState',
|
||||
default: { lat: 38.7209, lon: -77.5133 } as Coordinate
|
||||
});
|
||||
|
||||
export const zoomState = atom({
|
||||
key: 'zoomState',
|
||||
default: 8
|
||||
});
|
||||
Reference in New Issue
Block a user