Refactor, fixed search

This commit is contained in:
2023-09-15 09:08:33 -04:00
parent 4114d533b2
commit 128181bed6
28 changed files with 343 additions and 342 deletions

View File

@@ -1,9 +1,8 @@
import { getAirport } from '@/js/api/airport';
import { Airport } from '@/js/api/airport.types';
import { getAirport } from '@/app/_api/airport';
import Link from 'next/link';
export default async function Page({ params }: { params: { icao: string } }) {
const airport: Airport = await getAirport({ icao: params.icao });
const { data: airport } = await getAirport({ icao: params.icao });
return (
<>
<div className='border-b border-gray-200 bg-gray-400 px-4 py-5 sm:px-6 flex justify-between'>

View File

@@ -1,10 +1,10 @@
import React from 'react';
import RecoilRootWrapper from '@app/recoil-root-wrapper';
import Sidebar from '@/components/Sidebar';
import Topbar from '@/components/Topbar';
import Sidebar from '@/app/_components/Sidebar';
import Topbar from '@/app/_components/Topbar';
import 'styles/globals.css';
import 'styles/leaflet.css';
import StyledComponentsRegistry from '@/lib/AntdRegistry';
import StyledComponentsRegistry from '@/app/_lib/AntdRegistry';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });

View File

@@ -1,5 +1,5 @@
import React from 'react';
import Metar from '@/components/Metars';
import Metar from '@/app/_components/Metars';
export default function Page() {
return <Metar />;

View File

@@ -1,3 +1,3 @@
export default function Profile() {
}
return <></>;
}

View File

@@ -0,0 +1,42 @@
import axios from 'axios';
import { Bounds, GetAirportResponse, GetAirportsResponse } from './airport.types';
interface GetAirportProps {
icao: string;
}
export async function getAirport({ icao }: GetAirportProps): Promise<GetAirportResponse> {
const response = await axios.get(`http://localhost:5000/airports/${icao}`).catch((error) => console.error(error));
return response?.data || { data: undefined };
}
interface GetAirportsProps {
bounds?: Bounds;
category?: string;
filter?: string;
page?: number;
limit?: number;
}
export async function getAirports({
bounds,
category,
filter,
limit = 10,
page = 1
}: GetAirportsProps): Promise<GetAirportsResponse> {
const response = await axios
.get(`http://localhost:5000/airports`, {
params: {
bounds: bounds
? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}`
: undefined,
category: category ?? undefined,
filter: filter ?? undefined,
limit,
page
}
})
.catch((error) => console.error(error));
return response?.data || { data: [] };
}

View File

@@ -6,6 +6,16 @@ export enum AirportCategory {
LARGE = 'large_airport'
}
export interface Bounds {
northEast: Coordinate;
southWest: Coordinate;
}
export interface Coordinate {
lat: number;
lon: number;
}
export interface Airport {
icao: string;
category: AirportCategory;
@@ -25,3 +35,11 @@ export interface Airport {
};
metar?: Metar;
}
export interface GetAirportResponse {
data: Airport;
}
export interface GetAirportsResponse {
data: Airport[];
}

View File

@@ -2,12 +2,16 @@ import axios from 'axios';
import { Airport } from './airport.types';
import { Metar } from './metar.types';
export async function getMetars(airports: Airport[]): Promise<Metar[]> {
interface GetMetarsResponse {
data: Metar[];
}
export async function getMetars(airports: Airport[]): Promise<GetMetarsResponse> {
if (airports.length == 0) {
return [];
return { data: [] };
}
const stationICAOs: string = airports.map((airport) => airport.icao).join(',');
const url = `http://localhost:5000/metars/${stationICAOs}`;
const response = await axios.get(url).catch((error) => console.error(error));
return response?.data || [];
return response?.data || { data: [] };
}

View File

@@ -1,9 +1,9 @@
'use client';
import { getAirports } from '@/js/api/airport';
import { Airport } from '@/js/api/airport.types';
import { getMetars } from '@/js/api/metar';
import { Metar } from '@/js/api/metar.types';
import { getAirports } from '@/app/_api/airport';
import { Airport } from '@/app/_api/airport.types';
import { getMetars } from '@/app/_api/metar';
import { Metar } from '@/app/_api/metar.types';
import { FaLocationPin } from 'react-icons/fa6';
import { DivIcon, LatLngBounds } from 'leaflet';
import { useEffect, useState } from 'react';
@@ -41,7 +41,7 @@ export default function MapTiles() {
async function updateAirports(bounds: LatLngBounds) {
const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest();
const _airports = await getAirports({
const { data: _airports } = await getAirports({
bounds: {
northEast: { lat: ne.lat, lon: ne.lng },
southWest: { lat: sw.lat, lon: sw.lng }
@@ -49,7 +49,7 @@ export default function MapTiles() {
limit: 100,
page: 1
});
const metars = await getMetars(_airports);
const { data: metars } = await getMetars(_airports);
metars.forEach((metar) => {
_airports.forEach((airport) => {
if (metar.station_id == airport.icao) {

View File

@@ -1,5 +1,5 @@
import { Airport } from '@/js/api/airport.types';
import { Metar } from '@/js/api/metar.types';
import { Airport } from '@/app/_api/airport.types';
import { Metar } from '@/app/_api/metar.types';
import { FaArrowsSpin, FaLocationArrow } from 'react-icons/fa6';
import { Modal } from 'antd';
@@ -34,7 +34,7 @@ export default function MetarDialog({ airport, isOpen, onClose }: MetarDialogPro
}
}
return (
<Modal title={`${airport.icao} ${airport.full_name}`} open={isOpen} onCancel={onClose} footer={[]}>
<Modal title={`${airport.icao} ${airport.full_name}`} open={isOpen} onCancel={onClose} closable={false} footer={[]}>
<div className='min-w-0 flex-1 select-none'>
<hr />
<p className='text-sm font-medium text-gray-500'>{airport.metar?.raw_text}</p>
@@ -45,23 +45,25 @@ export default function MetarDialog({ airport, isOpen, onClose }: MetarDialogPro
{airport.metar?.flight_category ? airport.metar?.flight_category : 'UNKN'}
</span>
<div className='flex inline-block px-2'>
<span className={`text-sm text-black ${windColor(airport.metar)} py-2 px-2 rounded-full`}>
<span className={`text-sm text-black ${windColor(airport.metar)} py-2 px-3 rounded-full`}>
{airport.metar && airport.metar.wind_dir_degrees && Number(airport.metar.wind_dir_degrees) > 0 ? (
<FaLocationArrow
className='pr-1'
className='align-middle'
style={{ rotate: `${-45 + 180 + Number(airport.metar.wind_dir_degrees)}deg` }}
/>
) : (
<></>
)}
{airport.metar && airport.metar.wind_dir_degrees && airport.metar.wind_dir_degrees == 'VRB' ? (
<FaArrowsSpin className='pr-1' />
<FaArrowsSpin className='align-middle' />
) : (
<></>
)}
{airport.metar?.wind_speed_kt != undefined && airport.metar?.wind_speed_kt > 0
? `${airport.metar?.wind_speed_kt} KT`
: 'CALM'}
<span className='align-middle pl-1.5'>
{airport.metar?.wind_speed_kt != undefined && airport.metar?.wind_speed_kt > 0
? `${airport.metar?.wind_speed_kt} KT`
: 'CALM'}
</span>
</span>
</div>
</div>

View File

@@ -1,8 +1,8 @@
import { Metar } from '@/js/api/metar.types';
import { Metar } from '@/app/_api/metar.types';
import dynamic from 'next/dynamic';
export default async function Metar({ className = '' }: { className?: string }) {
const Map = dynamic(() => import('@/components/Metars/MetarMap'), {
const Map = dynamic(() => import('@/app/_components/Metars/MetarMap'), {
loading: () => (
<div className='grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8'>
<div className='text-center'>

View File

@@ -0,0 +1,82 @@
'use client';
import { AutoComplete, Avatar, Modal } from 'antd';
import Link from 'next/link';
import { AiOutlineSearch, AiOutlineUser } from 'react-icons/ai';
import { Button } from '@blueprintjs/core';
import { useState } from 'react';
import { getAirports } from '@/app/_api/airport';
import { useRouter } from 'next/navigation';
const DEFAULT_ICON_SIZE = 40;
export default function Topbar() {
const [modalOpen, setModalOpen] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [airports, setAirports] = useState<{ key: string; value: string; label: string }[]>([]);
const router = useRouter();
async function onSearch(value: string) {
setSearchValue(value);
const airportData = await getAirports({ filter: value });
setAirports(
airportData.data.map((airport) => ({
key: airport.icao,
value: airport.icao,
label: `${airport.icao} - ${airport.full_name}`
}))
);
}
function onSelect(value: string) {
setModalOpen(false);
setSearchValue('');
router.push(`/airport/${value}`);
}
function onClose() {
setModalOpen(false);
setSearchValue('');
}
return (
<>
<Modal
open={modalOpen}
closable={false}
onCancel={onClose}
footer={[]}
className='p-0'
title={'Search for Airports'}
>
<AutoComplete
className='w-full'
allowClear
autoFocus
value={searchValue}
options={airports}
onSelect={onSelect}
onSearch={onSearch}
placeholder='Search Airports...'
/>
</Modal>
<nav className='w-screen flex bg-gray-700 text-gray-200 justify-between'>
<div className='flex'>
<Link href={'/'} className='align-middle pt-2.5 pl-6 text-lg'>
<span>Aviation Weather</span>
</Link>
<Button
icon={<AiOutlineSearch size={24} className='float-left mr-2 hover:text-white' />}
className='my-1 ml-6 pl-10 pr-12 border-none rounded-lg bg-gray-800 text-base text-gray-200/75 hover:bg-gray-600 hover:text-white cursor-pointer'
onClick={() => setModalOpen(true)}
>
Search Airports...
</Button>
</div>
<Link className='my-1 mr-2' href={'/profile'}>
<Avatar shape='circle' size={DEFAULT_ICON_SIZE} icon={<AiOutlineUser />} />
</Link>
</nav>
</>
);
}

View File

@@ -1,7 +0,0 @@
export const airportsState = atom({
key: 'airportsState',
default: [] as Airport[]
});
import { Airport } from "@/js/airport";
import { atom } from "recoil";

View File

@@ -1,27 +0,0 @@
'use client';
import { Avatar } from 'antd';
import Search from 'antd/es/input/Search';
import { useRouter } from 'next/navigation';
import { AiOutlineUser } from 'react-icons/ai';
export default function Topbar() {
const router = useRouter();
function onSearch(value: string) {
router.push(`/airports/${value}`);
}
return (
<nav className='w-screen flex bg-gray-700 text-gray-200'>
<Search
placeholder='Search Airports...'
onSearch={onSearch}
enterButton
className='p-2'
style={{ width: '20em' }}
/>
<Avatar shape='square' size={48} icon={<AiOutlineUser />} />
</nav>
);
}

View File

@@ -1,45 +0,0 @@
import axios from 'axios';
import { Airport } from './airport.types';
interface GetAirportsProps {
bounds?: Bounds;
category?: string;
page?: number;
limit?: number;
}
export interface Bounds {
northEast: Coordinate;
southWest: Coordinate;
}
export interface Coordinate {
lat: number;
lon: number;
}
interface GetAirportProps {
icao: string;
}
export async function getAirport({ icao }: GetAirportProps) {
const response = await axios.get(`http://localhost:5000/airports/${icao}`).catch((error) => console.error(error));
return response?.data;
}
export async function getAirports({ bounds, category, limit = 10, page = 1 }: GetAirportsProps): Promise<Airport[]> {
const response = await axios
.get(`http://localhost:5000/airports`, {
params: {
ne_lat: bounds?.northEast.lat,
ne_lon: bounds?.northEast.lon,
sw_lat: bounds?.southWest.lat,
sw_lon: bounds?.southWest.lon,
category,
limit,
page
}
})
.catch((error) => console.error(error));
return response?.data || [];
}