Fixed some styling issues, fixed icons
This commit is contained in:
@@ -1,14 +1,31 @@
|
|||||||
import { getAirport } from '@/api/airport';
|
'use client';
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export default async function Page({ params }: { params: { icao: string } }) {
|
import { getAirport } from '@/api/airport';
|
||||||
const { data: airport } = await getAirport({ icao: params.icao });
|
import { Airport } from '@/api/airport.types';
|
||||||
return (
|
import Link from 'next/link';
|
||||||
<>
|
import { useEffect, useState } from 'react';
|
||||||
<div className=''>
|
|
||||||
<h3 className=''>{airport.full_name}</h3>
|
export default function Page({ params }: { params: { icao: string } }) {
|
||||||
<Link href={'/'}>Back</Link>
|
const [airport, setAirport] = useState<Airport | undefined>(undefined);
|
||||||
</div>
|
|
||||||
</>
|
useEffect(() => {
|
||||||
);
|
async function loadAirport() {
|
||||||
|
const { data: airport } = await getAirport({ icao: params.icao });
|
||||||
|
setAirport(airport);
|
||||||
|
}
|
||||||
|
loadAirport();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (airport) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className=''>
|
||||||
|
<h3 className=''>{airport.full_name}</h3>
|
||||||
|
<Link href={'/'}>Back</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ import Metar from '@/components/Metars';
|
|||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <Metar />;
|
return <Metar />;
|
||||||
|
// return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import ReactDOMServer from 'react-dom/server';
|
import ReactDOMServer from 'react-dom/server';
|
||||||
import { Marker, TileLayer, Tooltip, useMap, useMapEvents } from 'react-leaflet';
|
import { Marker, TileLayer, Tooltip, useMap, useMapEvents } from 'react-leaflet';
|
||||||
import MetarModal from './MetarModal';
|
import MetarModal from './MetarModal';
|
||||||
import { BsCircle, BsCircleFill } from 'react-icons/bs';
|
import { Avatar, MantineProvider } from '@mantine/core';
|
||||||
|
|
||||||
export default function MapTiles() {
|
export default function MapTiles() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -60,20 +60,10 @@ export default function MapTiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function iconSize() {
|
function iconSize() {
|
||||||
if (zoomLevel <= 4) {
|
if (zoomLevel <= 5) {
|
||||||
return 'text-xs';
|
return 'xs';
|
||||||
} else if (zoomLevel <= 5) {
|
} else {
|
||||||
return 'text-sm';
|
return 'sm';
|
||||||
} else if (zoomLevel <= 6) {
|
|
||||||
return 'text-base';
|
|
||||||
} else if (zoomLevel <= 7) {
|
|
||||||
return 'text-lg';
|
|
||||||
} else if (zoomLevel <= 9) {
|
|
||||||
return 'text-2xl';
|
|
||||||
} else if (zoomLevel <= 11) {
|
|
||||||
return 'text-3xl';
|
|
||||||
} else if (zoomLevel >= 12) {
|
|
||||||
return 'text-4xl';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,46 +71,56 @@ export default function MapTiles() {
|
|||||||
if (airport.metar?.flight_category == 'VFR') {
|
if (airport.metar?.flight_category == 'VFR') {
|
||||||
return new DivIcon({
|
return new DivIcon({
|
||||||
html: ReactDOMServer.renderToString(
|
html: ReactDOMServer.renderToString(
|
||||||
<div>
|
<MantineProvider>
|
||||||
<BsCircle className={`${iconSize()} rounded-full bg-emerald-700`} />
|
<Avatar variant='filled' color='green' radius='xl' size={iconSize()}>
|
||||||
<span className={`${iconSize()} text-white`}>V</span>
|
V
|
||||||
</div>
|
</Avatar>
|
||||||
|
</MantineProvider>
|
||||||
),
|
),
|
||||||
className: 'metar-marker-icon'
|
className: 'metar-marker-icon'
|
||||||
});
|
});
|
||||||
} else if (airport.metar?.flight_category == 'MVFR') {
|
} else if (airport.metar?.flight_category == 'MVFR') {
|
||||||
return new DivIcon({
|
return new DivIcon({
|
||||||
html: ReactDOMServer.renderToString(
|
html: ReactDOMServer.renderToString(
|
||||||
<div>
|
<MantineProvider>
|
||||||
<BsCircle className={`${iconSize()} rounded-full bg-blue-700`} />
|
<Avatar variant='filled' color='blue' radius='xl' size={iconSize()}>
|
||||||
<span className={`${iconSize()} text-white`}>M</span>
|
M
|
||||||
</div>
|
</Avatar>
|
||||||
|
</MantineProvider>
|
||||||
),
|
),
|
||||||
className: 'metar-marker-icon'
|
className: 'metar-marker-icon'
|
||||||
});
|
});
|
||||||
} else if (airport.metar?.flight_category == 'IFR') {
|
} else if (airport.metar?.flight_category == 'IFR') {
|
||||||
return new DivIcon({
|
return new DivIcon({
|
||||||
html: ReactDOMServer.renderToString(
|
html: ReactDOMServer.renderToString(
|
||||||
<div>
|
<MantineProvider>
|
||||||
<BsCircle className={`${iconSize()} rounded-full bg-red-700`} />
|
<Avatar variant='filled' color='red' radius='xl' size={iconSize()}>
|
||||||
<span className={`${iconSize()} text-white`}>I</span>
|
I
|
||||||
</div>
|
</Avatar>
|
||||||
|
</MantineProvider>
|
||||||
),
|
),
|
||||||
className: 'metar-marker-icon'
|
className: 'metar-marker-icon'
|
||||||
});
|
});
|
||||||
} else if (airport.metar?.flight_category == 'LIFR') {
|
} else if (airport.metar?.flight_category == 'LIFR') {
|
||||||
return new DivIcon({
|
return new DivIcon({
|
||||||
html: ReactDOMServer.renderToString(
|
html: ReactDOMServer.renderToString(
|
||||||
<div>
|
<MantineProvider>
|
||||||
<BsCircle className={`${iconSize()} rounded-full bg-purple-700`} />
|
<Avatar variant='filled' color='purple' radius='xl' size={iconSize()}>
|
||||||
<span className={`${iconSize()} text-white`}>L</span>
|
L
|
||||||
</div>
|
</Avatar>
|
||||||
|
</MantineProvider>
|
||||||
),
|
),
|
||||||
className: 'metar-marker-icon'
|
className: 'metar-marker-icon'
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return new DivIcon({
|
return new DivIcon({
|
||||||
html: ReactDOMServer.renderToString(<BsCircleFill className={`text-black`} />),
|
html: ReactDOMServer.renderToString(
|
||||||
|
<MantineProvider>
|
||||||
|
<Avatar variant='filled' color='black' radius='xl' size={iconSize()}>
|
||||||
|
U
|
||||||
|
</Avatar>
|
||||||
|
</MantineProvider>
|
||||||
|
),
|
||||||
className: 'metar-marker-icon'
|
className: 'metar-marker-icon'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import { MapContainer } from 'react-leaflet';
|
import { MapContainer } from 'react-leaflet';
|
||||||
import MapTiles from './MapTiles';
|
import MapTiles from './MapTiles';
|
||||||
|
import './metars.css';
|
||||||
|
|
||||||
export default function Map({ className = '' }: { className?: string }) {
|
export default function Map() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MapContainer
|
<MapContainer
|
||||||
@@ -13,7 +14,7 @@ export default function Map({ className = '' }: { className?: string }) {
|
|||||||
minZoom={3} // Zoomed out
|
minZoom={3} // Zoomed out
|
||||||
id='map-container'
|
id='map-container'
|
||||||
style={{ height: '94.5vh' }}
|
style={{ height: '94.5vh' }}
|
||||||
className={`${className} overflow-y-hidden overflow-x-hidden`}
|
className={`overflow-y-hidden overflow-x-hidden`}
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
>
|
>
|
||||||
<MapTiles />
|
<MapTiles />
|
||||||
|
|||||||
@@ -5,9 +5,16 @@ import { Metar } from '@/api/metar.types';
|
|||||||
import { FaArrowsSpin, FaLocationArrow } from 'react-icons/fa6';
|
import { FaArrowsSpin, FaLocationArrow } from 'react-icons/fa6';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
|
import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
|
||||||
import { BsFillCloudRainFill, BsFillCloudRainHeavyFill, BsFillCloudSleetFill, BsFillCloudSnowFill, BsQuestionLg } from 'react-icons/bs';
|
import {
|
||||||
|
BsFillCloudRainFill,
|
||||||
|
BsFillCloudRainHeavyFill,
|
||||||
|
BsFillCloudSleetFill,
|
||||||
|
BsFillCloudSnowFill,
|
||||||
|
BsQuestionLg
|
||||||
|
} from 'react-icons/bs';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Grid, Modal, Tooltip } from '@mantine/core';
|
import { Grid, Modal, Tooltip } from '@mantine/core';
|
||||||
|
import './metars.css';
|
||||||
|
|
||||||
interface MetarModalProps {
|
interface MetarModalProps {
|
||||||
airport: Airport;
|
airport: Airport;
|
||||||
@@ -23,31 +30,17 @@ export default function MetarModal({ airport, isOpen, onClose }: MetarModalProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal opened={isOpen} onClose={onClose} withCloseButton={false} size={'55rem'} className='modal'>
|
||||||
title={
|
<span className='title'>
|
||||||
<span className='flex justify-between'>
|
<Link href={`/airport/${airport.icao}`}>
|
||||||
<Link href={`/airport/${airport.icao}`}>
|
{airport.icao} {airport.full_name}
|
||||||
{airport.icao} {airport.full_name}
|
</Link>
|
||||||
</Link>
|
{isFavorite ? (
|
||||||
{isFavorite ? (
|
<AiFillStar size={24} className='star' onClick={() => handleFavorite(false)} />
|
||||||
<AiFillStar
|
) : (
|
||||||
size={24}
|
<AiOutlineStar size={24} className='star' onClick={() => handleFavorite(true)} />
|
||||||
className='cursor-pointer text-blue-500 hover:text-blue-400'
|
)}
|
||||||
onClick={() => handleFavorite(false)}
|
</span>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<AiOutlineStar
|
|
||||||
size={24}
|
|
||||||
className='cursor-pointer text-blue-500 hover:text-blue-400'
|
|
||||||
onClick={() => handleFavorite(true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
opened={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
className='select-none'
|
|
||||||
>
|
|
||||||
<div className='min-w-0 flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
<hr />
|
<hr />
|
||||||
{airport.metar && <MetarInfo metar={airport.metar} />}
|
{airport.metar && <MetarInfo metar={airport.metar} />}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Metar } from '@/api/metar.types';
|
import { Metar } from '@/api/metar.types';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
export default async function Metar({ className = '' }: { className?: string }) {
|
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'>
|
<div className='grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8'>
|
||||||
@@ -12,5 +12,5 @@ export default async function Metar({ className = '' }: { className?: string })
|
|||||||
),
|
),
|
||||||
ssr: false
|
ssr: false
|
||||||
});
|
});
|
||||||
return <Map className={className} />;
|
return <Map />;
|
||||||
}
|
}
|
||||||
|
|||||||
18
ui/src/components/Metars/metars.css
Normal file
18
ui/src/components/Metars/metars.css
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* https://stackoverflow.com/questions/55291179/how-to-overlay-content-on-react-leaflet-z-index-problem */
|
||||||
|
.leaflet-control { z-index: 0 !important}
|
||||||
|
.leaflet-pane { z-index: 0 !important}
|
||||||
|
.leaflet-top, .leaflet-bottom {z-index: 0 !important}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .title {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .star {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -4,13 +4,14 @@ import Link from 'next/link';
|
|||||||
import { AiOutlineUser } from 'react-icons/ai';
|
import { AiOutlineUser } from 'react-icons/ai';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { getAirports } from '@/api/airport';
|
import { getAirports } from '@/api/airport';
|
||||||
import { useRouter } from 'next/navigation';
|
// import { useRouter } from 'next/navigation';
|
||||||
import { Autocomplete, Avatar } from '@mantine/core';
|
import { Autocomplete, Avatar } from '@mantine/core';
|
||||||
|
import './topbar.css';
|
||||||
|
|
||||||
export default function Topbar() {
|
export default function Topbar() {
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [airports, setAirports] = useState<{ key: string; value: string; label: string }[]>([]);
|
const [airports, setAirports] = useState<{ key: string; value: string; label: string }[]>([]);
|
||||||
const router = useRouter();
|
// const router = useRouter();
|
||||||
|
|
||||||
async function onChange(value: string) {
|
async function onChange(value: string) {
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
@@ -24,28 +25,30 @@ export default function Topbar() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick(value: string) {
|
// function onClick(value: string) {
|
||||||
router.push(`/airport/${value}`);
|
// router.push(`/airport/${value}`);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<nav className='navbar'>
|
||||||
<div style={{ display: 'flex' }}>
|
<div className='left'>
|
||||||
<Link href={'/'} style={{ paddingLeft: '2em', paddingRight: '2em', margin: 'auto' }}>
|
<Link href={'/'} className='title'>
|
||||||
<span>Aviation Weather</span>
|
<span>Aviation Weather</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Autocomplete
|
<div className='search'>
|
||||||
autoFocus
|
<Autocomplete
|
||||||
radius='xl'
|
autoFocus
|
||||||
placeholder='Search Airports...'
|
radius='xl'
|
||||||
limit={10}
|
placeholder='Search Airports...'
|
||||||
data={airports}
|
limit={10}
|
||||||
value={searchValue}
|
data={airports}
|
||||||
onChange={onChange}
|
value={searchValue}
|
||||||
onBlur={() => setSearchValue('')}
|
onChange={onChange}
|
||||||
/>
|
onBlur={() => setSearchValue('')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link className='' href={'/profile'}>
|
<Link className='avatar' href={'/profile'}>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AiOutlineUser />
|
<AiOutlineUser />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|||||||
19
ui/src/components/Topbar/topbar.css
Normal file
19
ui/src/components/Topbar/topbar.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .left {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .title {
|
||||||
|
padding-left: 2em;
|
||||||
|
padding-right: 2em;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .left .search {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user