Fixed layouts
This commit is contained in:
@@ -144,7 +144,8 @@ pub struct QueryAirport {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct QueryFilters {
|
pub struct QueryFilters {
|
||||||
pub search: Option<String>,
|
pub icaos: Option<Vec<String>>,
|
||||||
|
pub name: Option<String>,
|
||||||
pub bounds: Option<Polygon<Point>>,
|
pub bounds: Option<Polygon<Point>>,
|
||||||
pub categories: Option<Vec<AirportCategory>>,
|
pub categories: Option<Vec<AirportCategory>>,
|
||||||
pub order_field: Option<QueryOrderField>,
|
pub order_field: Option<QueryOrderField>,
|
||||||
@@ -154,7 +155,8 @@ pub struct QueryFilters {
|
|||||||
impl Default for QueryFilters {
|
impl Default for QueryFilters {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
QueryFilters {
|
QueryFilters {
|
||||||
search: None,
|
icaos: None,
|
||||||
|
name: None,
|
||||||
bounds: None,
|
bounds: None,
|
||||||
categories: None,
|
categories: None,
|
||||||
order_field: None,
|
order_field: None,
|
||||||
@@ -305,11 +307,21 @@ impl QueryAirport {
|
|||||||
if let Some(categories) = &filters.categories {
|
if let Some(categories) = &filters.categories {
|
||||||
parts.push(format!("({})", categories.iter().map(|category| format!("category = '{}'", category.to_string())).collect::<Vec<String>>().join(" OR ")));
|
parts.push(format!("({})", categories.iter().map(|category| format!("category = '{}'", category.to_string())).collect::<Vec<String>>().join(" OR ")));
|
||||||
}
|
}
|
||||||
if let Some(search) = &filters.search {
|
fn sanitize_icao(icao: &str) -> String {
|
||||||
// Sanitize search to only allow [a-zA-Z0-9-\\s]
|
// Sanitize search to only allow [a-zA-Z0-9-\\s]
|
||||||
let search = search.chars().filter(|c| c.is_alphanumeric() || *c == '-' || *c == ' ').collect::<String>();
|
icao.chars().filter(|c| c.is_alphanumeric() || *c == '-' || *c == ' ').collect::<String>()
|
||||||
let search_strs = vec!["icao", "name", "iso_country", "iso_region", "municipality"];
|
}
|
||||||
parts.push(format!("({})", search_strs.iter().map(|s| format!("{} ILIKE '%{}%'", s, search)).collect::<Vec<String>>().join(" OR ")));
|
if &filters.icaos.is_some() == &true && &filters.name.is_some() == &true {
|
||||||
|
let icaos = filters.icaos.as_ref().unwrap();
|
||||||
|
let name = sanitize_icao(filters.name.as_ref().unwrap());
|
||||||
|
let icao_part = format!("({})", icaos.iter().map(|icao| format!("icao ILIKE '{}'", sanitize_icao(icao))).collect::<Vec<String>>().join(" OR "));
|
||||||
|
let name_part = format!("name ILIKE '%{}%'", name);
|
||||||
|
parts.push(format!("({} OR {})", icao_part, name_part));
|
||||||
|
} else if let Some(icaos) = &filters.icaos {
|
||||||
|
parts.push(format!("({})", icaos.iter().map(|icao| format!("icao ILIKE '{}'", sanitize_icao(icao))).collect::<Vec<String>>().join(" OR ")));
|
||||||
|
} else if let Some(name) = &filters.name {
|
||||||
|
let search = sanitize_icao(name);
|
||||||
|
parts.push(format!("name ILIKE '%{}%'", search));
|
||||||
}
|
}
|
||||||
|
|
||||||
if parts.len() > 0 {
|
if parts.len() > 0 {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ use serde::{Serialize, Deserialize};
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct GetAllParameters {
|
struct GetAllParameters {
|
||||||
search: Option<String>,
|
icaos: Option<String>,
|
||||||
|
name: Option<String>,
|
||||||
bounds: Option<String>,
|
bounds: Option<String>,
|
||||||
categories: Option<String>,
|
categories: Option<String>,
|
||||||
order_field: Option<String>,
|
order_field: Option<String>,
|
||||||
@@ -68,7 +69,11 @@ async fn import(mut payload: Multipart, auth: JwtAuth) -> HttpResponse {
|
|||||||
async fn get_all(req: HttpRequest) -> HttpResponse {
|
async fn get_all(req: HttpRequest) -> HttpResponse {
|
||||||
let params = web::Query::<GetAllParameters>::from_query(req.query_string()).unwrap();
|
let params = web::Query::<GetAllParameters>::from_query(req.query_string()).unwrap();
|
||||||
let mut filters = QueryFilters::default();
|
let mut filters = QueryFilters::default();
|
||||||
filters.search = params.search.clone();
|
filters.icaos = match ¶ms.icaos {
|
||||||
|
Some(i) => Some(i.split(",").map(|s| s.to_string()).collect()),
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
filters.name = params.name.clone();
|
||||||
filters.categories = match ¶ms.categories {
|
filters.categories = match ¶ms.categories {
|
||||||
Some(c) => Some(c.split(",").map(|s| AirportCategory::from_str(s).unwrap()).collect()),
|
Some(c) => Some(c.split(",").map(|s| AirportCategory::from_str(s).unwrap()).collect()),
|
||||||
None => None
|
None => None
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export async function getAirport({ icao }: GetAirportProps): Promise<GetAirportR
|
|||||||
interface GetAirportsProps {
|
interface GetAirportsProps {
|
||||||
bounds?: Bounds;
|
bounds?: Bounds;
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
search?: string;
|
icaos?: string[];
|
||||||
|
name?: string;
|
||||||
order_field?: AirportOrderField;
|
order_field?: AirportOrderField;
|
||||||
order_by?: 'asc' | 'desc';
|
order_by?: 'asc' | 'desc';
|
||||||
page?: number;
|
page?: number;
|
||||||
@@ -23,7 +24,8 @@ interface GetAirportsProps {
|
|||||||
export async function getAirports({
|
export async function getAirports({
|
||||||
bounds,
|
bounds,
|
||||||
categories,
|
categories,
|
||||||
search,
|
icaos,
|
||||||
|
name,
|
||||||
order_field,
|
order_field,
|
||||||
order_by,
|
order_by,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
@@ -34,7 +36,8 @@ export async function getAirports({
|
|||||||
? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}`
|
? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}`
|
||||||
: undefined,
|
: undefined,
|
||||||
categories: categories ?? undefined,
|
categories: categories ?? undefined,
|
||||||
search: search ?? undefined,
|
icaos: icaos ?? undefined,
|
||||||
|
name: name ?? undefined,
|
||||||
order_field: order_field ?? undefined,
|
order_field: order_field ?? undefined,
|
||||||
order_by: order_by ?? undefined,
|
order_by: order_by ?? undefined,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -57,19 +57,35 @@ export interface Coordinate {
|
|||||||
|
|
||||||
export interface Airport {
|
export interface Airport {
|
||||||
icao: string;
|
icao: string;
|
||||||
category: AirportCategory;
|
iata: string;
|
||||||
|
local: string;
|
||||||
name: string;
|
name: string;
|
||||||
elevation_ft: number;
|
category: AirportCategory;
|
||||||
iso_country: string;
|
iso_country: string;
|
||||||
iso_region: string;
|
iso_region: string;
|
||||||
municipality: string;
|
municipality: string;
|
||||||
iata_code: string;
|
elevation_ft: number;
|
||||||
local_code: string;
|
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
|
has_tower: boolean;
|
||||||
|
has_beacon: boolean;
|
||||||
|
runways: Runway[];
|
||||||
|
frequencies: Frequency[];
|
||||||
latest_metar?: Metar;
|
latest_metar?: Metar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Runway {
|
||||||
|
id: string;
|
||||||
|
length_ft: number;
|
||||||
|
width_ft: number;
|
||||||
|
surface: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Frequency {
|
||||||
|
id: string;
|
||||||
|
frequency_mhz: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GetAirportResponse {
|
export interface GetAirportResponse {
|
||||||
data: Airport;
|
data: Airport;
|
||||||
meta: Metadata;
|
meta: Metadata;
|
||||||
|
|||||||
@@ -4,29 +4,41 @@ import { Airport } from "@/api/airport.types";
|
|||||||
import AirportTablePanel from "@/components/Admin/AirportTablePanel";
|
import AirportTablePanel from "@/components/Admin/AirportTablePanel";
|
||||||
import CreateAirportPanel from "@/components/Admin/CreateAirportPanel";
|
import CreateAirportPanel from "@/components/Admin/CreateAirportPanel";
|
||||||
import UpdateAirportModal from "@/components/Admin/UpdateAirportModal";
|
import UpdateAirportModal from "@/components/Admin/UpdateAirportModal";
|
||||||
|
import { isAdminState } from "@/state/auth";
|
||||||
import { Container, Grid, SimpleGrid } from "@mantine/core";
|
import { Container, Grid, SimpleGrid } from "@mantine/core";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRecoilValue } from "recoil";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [airport, setAirport] = useState<Airport | undefined>(undefined);
|
const [airport, setAirport] = useState<Airport | undefined>(undefined);
|
||||||
|
const isAdmin = useRecoilValue(isAdminState);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isAdmin) {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
}, [airport]);
|
}, [airport]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container fluid>
|
<>
|
||||||
<SimpleGrid cols={{ base: 1, xs: 1 }} spacing={'md'}>
|
{isAdmin && (
|
||||||
<Grid p={'lg'}>
|
<Container fluid>
|
||||||
<Grid.Col span={12}>
|
<SimpleGrid cols={{ base: 1, xs: 1 }} spacing={'md'}>
|
||||||
<AirportTablePanel setAirport={setAirport} />
|
<Grid p={'lg'}>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<AirportTablePanel setAirport={setAirport} />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<CreateAirportPanel />
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={12}>
|
</Grid>
|
||||||
<CreateAirportPanel />
|
</SimpleGrid>
|
||||||
</Grid.Col>
|
<UpdateAirportModal airport={airport} setAirport={setAirport} />
|
||||||
</Grid>
|
</Container>
|
||||||
</SimpleGrid>
|
)}
|
||||||
<UpdateAirportModal airport={airport} setAirport={setAirport} />
|
</>
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getAirport } from '@/api/airport';
|
|||||||
import { Airport } from '@/api/airport.types';
|
import { Airport } from '@/api/airport.types';
|
||||||
import { getMetars } from '@/api/metar';
|
import { getMetars } from '@/api/metar';
|
||||||
import { Metar } from '@/api/metar.types';
|
import { Metar } from '@/api/metar.types';
|
||||||
import SkyConditions from '@/components/Metars/SkyConditions';
|
import { Grid, Title, Text } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function Page({ params }: { params: { icao: string } }) {
|
export default function Page({ params }: { params: { icao: string } }) {
|
||||||
@@ -25,12 +25,14 @@ export default function Page({ params }: { params: { icao: string } }) {
|
|||||||
|
|
||||||
if (airport) {
|
if (airport) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Grid gutter={80} style={{ margin: '1em auto 0'}}>
|
||||||
<div className=''>
|
<Grid.Col span={12}>
|
||||||
<h3 className=''>{airport.name}</h3>
|
<Title className='title' order={1}>{airport.icao} - {airport.name}</Title>
|
||||||
{metar && <SkyConditions metar={metar} />}
|
<Text c="dimmed">
|
||||||
</div>
|
{airport.municipality} | {airport.iso_region} | {airport.iso_country}
|
||||||
</>
|
</Text>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@@ -1,3 +1,159 @@
|
|||||||
export default async function Page() {
|
'use client';
|
||||||
return <></>;
|
|
||||||
|
import { getAirports } from "@/api/airport";
|
||||||
|
import { Airport } from "@/api/airport.types";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRecoilState, useRecoilValue } from "recoil";
|
||||||
|
import { Badge, Button, Card, Grid, Group, SimpleGrid, Text, Title } from "@mantine/core";
|
||||||
|
import classes from './profile.module.css';
|
||||||
|
import { getFavorites, removeFavorite } from "@/api/users";
|
||||||
|
import { getMetars } from "@/api/metar";
|
||||||
|
import { Metar } from "@/api/metar.types";
|
||||||
|
import { MdLocationSearching } from 'react-icons/md';
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { coordinatesState } from "@/state/map";
|
||||||
|
import { userState } from "@/state/auth";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const user = useRecoilValue(userState);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid gutter={80}>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Card shadow="sm" m={"lg"} padding="lg" radius="md" withBorder>
|
||||||
|
<Title className={classes.title} order={2}>
|
||||||
|
{user?.first_name} {user?.last_name}
|
||||||
|
</Title>
|
||||||
|
<hr />
|
||||||
|
<Text c="dimmed">
|
||||||
|
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<TopSection />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopSection() {
|
||||||
|
const [airports, setAirports] = useState<Airport[]>([]);
|
||||||
|
const [metars, setMetars] = useState<Metar[]>([]);
|
||||||
|
const router = useRouter();
|
||||||
|
const [_, setCoordinates] = useRecoilState(coordinatesState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFavorites();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function metarColor(metar?: Metar): string {
|
||||||
|
switch (metar?.flight_category) {
|
||||||
|
case 'VFR':
|
||||||
|
return 'green';
|
||||||
|
case 'MVFR':
|
||||||
|
return 'blue';
|
||||||
|
case 'IFR':
|
||||||
|
return 'red';
|
||||||
|
case 'LIFR':
|
||||||
|
return 'purple';
|
||||||
|
default:
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AirportCard(airport: Airport) {
|
||||||
|
let metar = metars.find((m) => m.station_id === airport.icao);
|
||||||
|
let color = metarColor(metar);
|
||||||
|
let text = metar?.flight_category || 'UNKN';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={airport.icao} shadow="sm" padding="lg" radius="md" withBorder>
|
||||||
|
<Group justify="space-between" mt="md" mb="xs">
|
||||||
|
<Text fw={500} style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', width: '20em' }}>{airport.name}</Text>
|
||||||
|
<Badge color={color} variant="light">{text}</Badge>
|
||||||
|
</Group>
|
||||||
|
<Group style={{ cursor: 'pointer', userSelect: 'none' }} onClick={() => {
|
||||||
|
setCoordinates({
|
||||||
|
lat: airport.latitude,
|
||||||
|
lon: airport.longitude,
|
||||||
|
});
|
||||||
|
router.push('/');
|
||||||
|
}}>
|
||||||
|
<MdLocationSearching size={20} />
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{airport.latitude.toFixed(3)}, {airport.longitude.toFixed(3)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'end',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="blue"
|
||||||
|
size="sm"
|
||||||
|
radius="lg"
|
||||||
|
style={{ marginTop: '10px' }}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="red"
|
||||||
|
size="sm"
|
||||||
|
radius="lg"
|
||||||
|
style={{ marginTop: '10px' }}
|
||||||
|
onClick={async () => {
|
||||||
|
await removeFavorite(airport.icao);
|
||||||
|
await updateFavorites();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateFavorites() {
|
||||||
|
const favorites = await getFavorites();
|
||||||
|
const m = (await getMetars(favorites)).data;
|
||||||
|
setMetars(m);
|
||||||
|
const a = (await getAirports({ icaos: favorites })).data;
|
||||||
|
setAirports(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<Grid gutter={80}>
|
||||||
|
<Grid.Col span={{ base: 12, md: 5 }}>
|
||||||
|
<Title className={classes.title} order={2}>
|
||||||
|
Logbook
|
||||||
|
</Title>
|
||||||
|
<hr />
|
||||||
|
<Text c="dimmed">
|
||||||
|
Your logbook is a list of your flights. You can add flights to your logbook by clicking the "Add to logbook" button on the flight page.
|
||||||
|
</Text>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={{ base: 12, md: 7 }}>
|
||||||
|
<Title className={classes.title} order={2}>
|
||||||
|
Saved Airports
|
||||||
|
</Title>
|
||||||
|
<hr />
|
||||||
|
<SimpleGrid cols={{ base: 1, md: 2 }} spacing={30}>
|
||||||
|
{airports.map((airport) => AirportCard(airport))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
14
ui/src/app/profile/profile.module.css
Normal file
14
ui/src/app/profile/profile.module.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.wrapper {
|
||||||
|
padding: calc(var(--mantine-spacing-xl) * 2) var(--mantine-spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family:
|
||||||
|
Greycliff CF,
|
||||||
|
var(--mantine-font-family);
|
||||||
|
font-size: rem(36px);
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: var(--mantine-spacing-md);
|
||||||
|
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport
|
|||||||
|
|
||||||
async function getAirportData() {
|
async function getAirportData() {
|
||||||
const response = await getAirports({
|
const response = await getAirports({
|
||||||
search,
|
icaos: [search],
|
||||||
|
name: search,
|
||||||
page,
|
page,
|
||||||
limit: 100
|
limit: 100
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,4 +24,10 @@
|
|||||||
padding-right: 2em;
|
padding-right: 2em;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .user-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 2em;
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import { useToggle } from '@mantine/hooks';
|
|||||||
import { HeaderModal } from './HeaderModal';
|
import { HeaderModal } from './HeaderModal';
|
||||||
import { coordinatesState } from '@/state/map';
|
import { coordinatesState } from '@/state/map';
|
||||||
import { User } from '@/api/auth.types';
|
import { User } from '@/api/auth.types';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
user: User | undefined;
|
user: User | undefined;
|
||||||
@@ -26,10 +27,12 @@ export default function Header({ user, profilePicture, setProfilePicture, login,
|
|||||||
const [airports, setAirports] = useState<{ key: string; value: string; label: string }[]>([]);
|
const [airports, setAirports] = useState<{ key: string; value: string; label: string }[]>([]);
|
||||||
const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']);
|
const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']);
|
||||||
const [_, setCoordinates] = useRecoilState(coordinatesState);
|
const [_, setCoordinates] = useRecoilState(coordinatesState);
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
async function onChange(value: string) {
|
async function onChange(value: string) {
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
const airportData = await getAirports({ search: value });
|
const airportData = await getAirports({ icaos: [value], name: value });
|
||||||
setAirports(
|
setAirports(
|
||||||
airportData.data.map((airport) => ({
|
airportData.data.map((airport) => ({
|
||||||
key: airport.icao,
|
key: airport.icao,
|
||||||
@@ -40,9 +43,15 @@ export default function Header({ user, profilePicture, setProfilePicture, login,
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onClick(value: string) {
|
async function onClick(value: string) {
|
||||||
const airport = await getAirport({ icao: value });
|
setSearchValue('');
|
||||||
if (airport) {
|
// Get current path
|
||||||
setCoordinates({ lat: airport.data.latitude, lon: airport.data.longitude });
|
if (pathname == '/') {
|
||||||
|
const airport = await getAirport({ icao: value });
|
||||||
|
if (airport) {
|
||||||
|
setCoordinates({ lat: airport.data.latitude, lon: airport.data.longitude });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
router.push(`/airport/${value}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export default function MapTiles() {
|
|||||||
const [airports, setAirports] = useState<Airport[]>([]);
|
const [airports, setAirports] = useState<Airport[]>([]);
|
||||||
const [selectedAirport, setSelectedAirport] = useState<Airport | undefined>();
|
const [selectedAirport, setSelectedAirport] = useState<Airport | undefined>();
|
||||||
const coordinates = useRecoilValue(coordinatesState);
|
const coordinates = useRecoilValue(coordinatesState);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [zoom, setZoom] = useRecoilState(zoomState);
|
const [zoom, setZoom] = useRecoilState(zoomState);
|
||||||
// const [dragging, setDragging] = useState(false);
|
// const [dragging, setDragging] = useState(false);
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
@@ -46,7 +45,6 @@ export default function MapTiles() {
|
|||||||
async function updateAirports(bounds: LatLngBounds) {
|
async function updateAirports(bounds: LatLngBounds) {
|
||||||
const ne = bounds.getNorthEast();
|
const ne = bounds.getNorthEast();
|
||||||
const sw = bounds.getSouthWest();
|
const sw = bounds.getSouthWest();
|
||||||
console.log('zoom', zoom)
|
|
||||||
const { data: airportData } = await getAirports({
|
const { data: airportData } = await getAirports({
|
||||||
bounds: {
|
bounds: {
|
||||||
northEast: { lat: ne.lat, lon: ne.lng },
|
northEast: { lat: ne.lat, lon: ne.lng },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { MapContainer, useMap } from 'react-leaflet';
|
import { MapContainer } from 'react-leaflet';
|
||||||
import MapTiles from './MapTiles';
|
import MapTiles from './MapTiles';
|
||||||
import './metars.css';
|
import './metars.css';
|
||||||
import { coordinatesState, zoomState } from '@/state/map';
|
import { coordinatesState, zoomState } from '@/state/map';
|
||||||
@@ -18,8 +18,7 @@ export default function Map() {
|
|||||||
maxZoom={14} // Zoomed in
|
maxZoom={14} // Zoomed in
|
||||||
minZoom={3} // Zoomed out
|
minZoom={3} // Zoomed out
|
||||||
id='map-container'
|
id='map-container'
|
||||||
style={{ height: '94.5vh' }}
|
className={`map-container`}
|
||||||
className={`overflow-y-hidden overflow-x-hidden`}
|
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
>
|
>
|
||||||
<MapTiles />
|
<MapTiles />
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import './metars.css';
|
|||||||
import SkyConditions from './SkyConditions';
|
import SkyConditions from './SkyConditions';
|
||||||
import { addFavorite, getFavorites, removeFavorite } from '@/api/users';
|
import { addFavorite, getFavorites, removeFavorite } from '@/api/users';
|
||||||
import { favoritesState } from '@/state/user';
|
import { favoritesState } from '@/state/user';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
interface MetarModalProps {
|
interface MetarModalProps {
|
||||||
airport: Airport;
|
airport: Airport;
|
||||||
@@ -31,20 +31,21 @@ interface MetarModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MetarModal({ airport, isOpen, onClose }: MetarModalProps) {
|
export default function MetarModal({ airport, isOpen, onClose }: MetarModalProps) {
|
||||||
const favorites = useRecoilValue(favoritesState);
|
const [favorites, setFavorites] = useRecoilState(favoritesState);
|
||||||
const [isFavorite, setIsFavorite] = useState(false);
|
const [isFavorite, setIsFavorite] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsFavorite(favorites.includes(airport.icao));
|
setIsFavorite(favorites.includes(airport.icao));
|
||||||
}, [favorites, airport]);
|
}, [airport, isOpen]);
|
||||||
|
|
||||||
function handleFavorite(value: boolean) {
|
async function updateIsFavorite(value: boolean) {
|
||||||
setIsFavorite(value);
|
setIsFavorite(value);
|
||||||
if (value) {
|
if (value) {
|
||||||
addFavorite(airport.icao);
|
await addFavorite(airport.icao);
|
||||||
} else {
|
} else {
|
||||||
removeFavorite(airport.icao);
|
await removeFavorite(airport.icao);
|
||||||
}
|
}
|
||||||
|
setFavorites(await getFavorites());
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,9 +61,9 @@ export default function MetarModal({ airport, isOpen, onClose }: MetarModalProps
|
|||||||
{airport.icao} {airport.name}
|
{airport.icao} {airport.name}
|
||||||
</Link>
|
</Link>
|
||||||
{isFavorite ? (
|
{isFavorite ? (
|
||||||
<AiFillStar size={24} className='star' onClick={() => handleFavorite(false)} />
|
<AiFillStar size={24} className='star' onClick={async () => await updateIsFavorite(false)} />
|
||||||
) : (
|
) : (
|
||||||
<AiOutlineStar size={24} className='star' onClick={() => handleFavorite(true)} />
|
<AiOutlineStar size={24} className='star' onClick={async () => await updateIsFavorite(true)} />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div className='min-w-0 flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
@@ -171,11 +172,6 @@ function MetarInfo({ metar }: { metar: Metar }) {
|
|||||||
<Grid.Col className='gutter-row' span={12}>
|
<Grid.Col className='gutter-row' span={12}>
|
||||||
<Grid gutter={18}>
|
<Grid gutter={18}>
|
||||||
<Grid.Col className='gutter-row' span={12}>
|
<Grid.Col className='gutter-row' span={12}>
|
||||||
<Card shadow='sm' padding='sm' radius='md' style={{ textAlign: 'center' }}>
|
|
||||||
<Card.Section>
|
|
||||||
|
|
||||||
</Card.Section>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { Metar } from '@/api/metar.types';
|
import { Metar } from '@/api/metar.types';
|
||||||
|
import { Skeleton } from '@mantine/core';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
export default async function Metar() {
|
export default async function Metar() {
|
||||||
const Map = dynamic(() => import('@/components/Metars/MetarMap'), {
|
const Map = dynamic(() => import('@/components/Metars/MetarMap'), {
|
||||||
loading: () => (
|
loading: () => (
|
||||||
<div className='grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8'>
|
<Skeleton className='map-container' />
|
||||||
<div className='text-center'>
|
|
||||||
<p className='mt-4 text-3xl font-bold tracking-tight text-gray-300 sm:text-5xl'>Loading...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
ssr: false
|
ssr: false
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,3 +16,11 @@
|
|||||||
.modal .star {
|
.modal .star {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-container {
|
||||||
|
/* 100vh - (height of navbar) */
|
||||||
|
height: calc(100vh - 46px);
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { User } from '@/api/auth.types';
|
import { User } from '@/api/auth.types';
|
||||||
import { atom } from 'recoil';
|
import { atom, selector } from 'recoil';
|
||||||
|
|
||||||
export const userState = atom({
|
export const userState = atom({
|
||||||
key: 'userState',
|
key: 'userState',
|
||||||
default: undefined as User | undefined
|
default: undefined as User | undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const isAdminState = selector({
|
||||||
|
key: 'isAdminState',
|
||||||
|
get: ({ get }) => {
|
||||||
|
const user = get(userState);
|
||||||
|
return user?.role === 'admin';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const refreshIdState = atom({
|
export const refreshIdState = atom({
|
||||||
key: 'refreshIdState',
|
key: 'refreshIdState',
|
||||||
default: undefined as NodeJS.Timeout | undefined
|
default: undefined as NodeJS.Timeout | undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
export const isAuthenticatedState = atom({
|
|
||||||
key: 'isAuthenticatedState',
|
|
||||||
default: false
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user