Refactor, fixed search
This commit is contained in:
@@ -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'>
|
||||
@@ -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'] });
|
||||
@@ -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 />;
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function Profile() {
|
||||
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
42
weather-ui/src/app/_api/airport.ts
Normal file
42
weather-ui/src/app/_api/airport.ts
Normal 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: [] };
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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: [] };
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -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>
|
||||
@@ -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'>
|
||||
82
weather-ui/src/app/_components/Topbar/index.tsx
Normal file
82
weather-ui/src/app/_components/Topbar/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export const airportsState = atom({
|
||||
key: 'airportsState',
|
||||
default: [] as Airport[]
|
||||
});
|
||||
|
||||
import { Airport } from "@/js/airport";
|
||||
import { atom } from "recoil";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 || [];
|
||||
}
|
||||
Reference in New Issue
Block a user