Migrate front end to Mantine

This commit is contained in:
2023-09-22 16:44:30 -04:00
parent ab4073f280
commit 02a4d840e0
17 changed files with 102 additions and 113 deletions

View File

@@ -11,22 +11,25 @@ help: ## This info
@cat Makefile | grep -E '^[a-zA-Z\/_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @cat Makefile | grep -E '^[a-zA-Z\/_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
@echo @echo
up: build: ## Build Docker service container
docker compose build
up: ## Start Docker service containers
docker compose up -d docker compose up -d
down: down: ## Stop Docker service containers
docker compose down docker compose down
connect: connect: ## Connect to the Weather DB
docker exec -it aviation_weather_db psql -U postgres docker exec -it aviation_weather_db psql -U postgres
lint: ## Run the linter lint: ## Run the linter
npm run lint npm run lint
clean: clean: ## Clean up the service
rm -rf target rm -rf target
clean-db: ## Remove database and Cargo packages clean-db: ## Remove database
docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "DROP DATABASE IF EXISTS \"${DATABASE_NAME}\";"' docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "DROP DATABASE IF EXISTS \"${DATABASE_NAME}\";"'
docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "CREATE DATABASE \"${DATABASE_NAME}\";"' || true docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "CREATE DATABASE \"${DATABASE_NAME}\";"' || true

View File

@@ -1,9 +1,10 @@
version: '3' version: '3'
name: weather
services: services:
weather-db: db:
image: postgis/postgis:latest image: postgis/postgis:latest
container_name: weather.db container_name: weather-db
env_file: env_file:
- .env - .env
environment: environment:
@@ -17,6 +18,22 @@ services:
- "${DATABASE_PORT}:5432" - "${DATABASE_PORT}:5432"
restart: unless-stopped restart: unless-stopped
service:
container_name: weather-service
env_file:
- .env
ports:
- "${SERVICE_PORT}:${SERVICE_PORT}"
build:
context: ./
depends_on:
- db
restart: unless-stopped
volumes: volumes:
db: db:
db_logs: db_logs:
networks:
default:
name: weather-backend

View File

@@ -44,8 +44,8 @@ async fn main() -> std::io::Result<()> {
server = match listenfd.take_tcp_listener(0)? { server = match listenfd.take_tcp_listener(0)? {
Some(listener) => server.listen(listener)?, Some(listener) => server.listen(listener)?,
None => { None => {
let host = std::env::var("HOST").expect("Please set host in .env"); let host = std::env::var("SERVICE_HOST").expect("Please set host in .env");
let port = std::env::var("PORT").expect("Please set port in .env"); let port = std::env::var("SERVICE_PORT").expect("Please set port in .env");
debug!("Binding server to {}:{}", host, port); debug!("Binding server to {}:{}", host, port);
server.bind(format!("{}:{}", host, port))? server.bind(format!("{}:{}", host, port))?
} }

View File

@@ -9,9 +9,9 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@ant-design/cssinjs": "^1.17.0", "@mantine/core": "^7.0.0",
"@blueprintjs/core": "^5.3.0", "@mantine/hooks": "^7.0.0",
"antd": "^5.9.0", "@mantine/modals": "^7.0.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"next": "^13.4.19", "next": "^13.4.19",
@@ -34,8 +34,9 @@
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"postcss": "^8.4.28", "postcss": "^8.4.28",
"postcss-import": "^15.1.0",
"postcss-preset-mantine": "^1.7.0",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"tailwindcss": "^3.3.3",
"typescript": "5.1.6" "typescript": "5.1.6"
} }
} }

View File

@@ -1,8 +1,7 @@
module.exports = { module.exports = {
plugins: { plugins: {
'postcss-preset-mantine': {},
'postcss-import': {}, 'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {} autoprefixer: {}
} }
}; };

View File

@@ -5,8 +5,8 @@ export default async function Page({ params }: { params: { icao: string } }) {
const { data: airport } = await getAirport({ icao: params.icao }); const { data: airport } = await getAirport({ icao: params.icao });
return ( return (
<> <>
<div className='border-b border-gray-200 bg-gray-400 px-4 py-5 sm:px-6 flex justify-between'> <div className=''>
<h3 className='text-base font-semibold leading-6 text-gray-900'>{airport.full_name}</h3> <h3 className=''>{airport.full_name}</h3>
<Link href={'/'}>Back</Link> <Link href={'/'}>Back</Link>
</div> </div>
</> </>

View File

@@ -2,18 +2,20 @@ import React from 'react';
import RecoilRootWrapper from '@app/recoil-root-wrapper'; import RecoilRootWrapper from '@app/recoil-root-wrapper';
import Sidebar from '@/components/Sidebar'; import Sidebar from '@/components/Sidebar';
import Topbar from '@/components/Topbar'; import Topbar from '@/components/Topbar';
import { Inter } from 'next/font/google';
import { MantineProvider } from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import 'styles/globals.css'; import 'styles/globals.css';
import 'styles/leaflet.css'; import 'styles/leaflet.css';
import StyledComponentsRegistry from '@/lib/AntdRegistry'; import '@mantine/core/styles.css';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata = { export const metadata = {
title: 'Aviation Weather', title: 'Aviation Weather',
description: '' description: ''
}; };
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang='en' className='h-full bg-white'> <html lang='en' className='h-full bg-white'>
@@ -22,11 +24,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</head> </head>
<body className={`${inter.className} wrapper h-full`}> <body className={`${inter.className} wrapper h-full`}>
<RecoilRootWrapper> <RecoilRootWrapper>
<StyledComponentsRegistry> <MantineProvider>
<ModalsProvider>
<Topbar /> <Topbar />
<Sidebar /> <Sidebar />
{children} {children}
</StyledComponentsRegistry> </ModalsProvider>
</MantineProvider>
</RecoilRootWrapper> </RecoilRootWrapper>
</body> </body>
</html> </html>

View File

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

View File

@@ -7,7 +7,7 @@ import { DivIcon, LatLngBounds } from 'leaflet';
import { useEffect, useState } from 'react'; 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 MetarDialog from './MetarDialog'; import MetarModal from './MetarModal';
import { BsCircle, BsCircleFill } from 'react-icons/bs'; import { BsCircle, BsCircleFill } from 'react-icons/bs';
export default function MapTiles() { export default function MapTiles() {
@@ -133,7 +133,7 @@ export default function MapTiles() {
return ( return (
<> <>
{selectedAirport && <MetarDialog isOpen={isOpen} onClose={() => setIsOpen(false)} airport={selectedAirport} />} {selectedAirport && <MetarModal isOpen={isOpen} onClose={() => setIsOpen(false)} airport={selectedAirport} />}
<TileLayer <TileLayer
attribution='&copy; <a href="https://www.osm.org/copyright">OpenStreetMap</a> contributors' attribution='&copy; <a href="https://www.osm.org/copyright">OpenStreetMap</a> contributors'
url='http://{s}.tile.osm.org/{z}/{x}/{y}.png' url='http://{s}.tile.osm.org/{z}/{x}/{y}.png'

View File

@@ -3,19 +3,19 @@
import { Airport } from '@/api/airport.types'; import { Airport } from '@/api/airport.types';
import { Metar } from '@/api/metar.types'; import { Metar } from '@/api/metar.types';
import { FaArrowsSpin, FaLocationArrow } from 'react-icons/fa6'; import { FaArrowsSpin, FaLocationArrow } from 'react-icons/fa6';
import { Col, Grid, Modal, Row, Tooltip } from 'antd';
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';
interface MetarDialogProps { interface MetarModalProps {
airport: Airport; airport: Airport;
isOpen: boolean; isOpen: boolean;
onClose(): void; onClose(): void;
} }
export default function MetarDialog({ airport, isOpen, onClose }: MetarDialogProps) { export default function MetarModal({ airport, isOpen, onClose }: MetarModalProps) {
const [isFavorite, setIsFavorite] = useState(false); const [isFavorite, setIsFavorite] = useState(false);
function handleFavorite(value: boolean) { function handleFavorite(value: boolean) {
@@ -44,10 +44,8 @@ export default function MetarDialog({ airport, isOpen, onClose }: MetarDialogPro
)} )}
</span> </span>
} }
open={isOpen} opened={isOpen}
onCancel={onClose} onClose={onClose}
closable={false}
footer={[]}
className='select-none' className='select-none'
> >
<div className='min-w-0 flex-1'> <div className='min-w-0 flex-1'>
@@ -131,8 +129,8 @@ function MetarInfo({ metar }: { metar: Metar }) {
return ( return (
<div> <div>
<p className='text-xs font-small text-gray-500'>{metar.raw_text}</p> <p className='text-xs font-small text-gray-500'>{metar.raw_text}</p>
<Row gutter={18}> <Grid gutter={18}>
<Col className='gutter-row' span={6}> <Grid.Col className='gutter-row' span={6}>
<span <span
className={`text-sm text-white py-2 px-4 rounded-full className={`text-sm text-white py-2 px-4 rounded-full
${metarBGColor(metar)} ${metarBGColor(metar)}
@@ -140,15 +138,11 @@ function MetarInfo({ metar }: { metar: Metar }) {
> >
{metar.flight_category ? metar.flight_category : 'UNKN'} {metar.flight_category ? metar.flight_category : 'UNKN'}
</span> </span>
</Col> </Grid.Col>
<Col className='gutter-row' span={12}> <Grid.Col className='gutter-row' span={12}>
{metar.wx_string && metar.wx_string.split(' ').map((wx) => <MetarIcon wx={wx} />)} {metar.wx_string && metar.wx_string.split(' ').map((wx) => <MetarIcon wx={wx} />)}
</Col> </Grid.Col>
</Row> </Grid>
<Row gutter={2}>Compass TBD Compass TBD Compass TBD Compass TBD Compass TB</Row>
<Row gutter={2}>
<Col></Col>
</Row>
</div> </div>
); );
} }
@@ -215,7 +209,7 @@ function MetarIcon({ wx }: { wx: string }) {
// color = ''; // color = '';
// } // }
return ( return (
<Tooltip title={title}> <Tooltip label={title}>
<span className={`rounded-full`}>{icon}</span> <span className={`rounded-full`}>{icon}</span>
</Tooltip> </Tooltip>
); );

View File

@@ -1,3 +1,5 @@
'use client';
import './Sidebar.css'; import './Sidebar.css';
export default function Sidebar() { export default function Sidebar() {

View File

@@ -1,20 +1,18 @@
'use client'; 'use client';
import { AutoComplete, Avatar } from 'antd';
import Link from 'next/link'; 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';
const DEFAULT_ICON_SIZE = 40;
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 onSearch(value: string) { async function onChange(value: string) {
setSearchValue(value); setSearchValue(value);
const airportData = await getAirports({ filter: value }); const airportData = await getAirports({ filter: value });
setAirports( setAirports(
@@ -26,34 +24,32 @@ export default function Topbar() {
); );
} }
function onSelect(value: string) { function onClick(value: string) {
setSearchValue('');
router.push(`/airport/${value}`); router.push(`/airport/${value}`);
} }
return ( return (
<> <nav style={{ display: 'flex', justifyContent: 'space-between' }}>
<nav className='w-screen flex bg-gray-700 text-gray-200 justify-between'> <div style={{ display: 'flex' }}>
<div className='flex'> <Link href={'/'} style={{ paddingLeft: '2em', paddingRight: '2em', margin: 'auto' }}>
<Link href={'/'} className='align-middle pt-2.5 px-6 text-lg'>
<span>Aviation Weather</span> <span>Aviation Weather</span>
</Link> </Link>
<AutoComplete <Autocomplete
className='w-72 relative top-2'
autoFocus autoFocus
defaultActiveFirstOption radius='xl'
value={searchValue}
options={airports}
onSelect={onSelect}
onSearch={onSearch}
onBlur={() => setSearchValue('')}
placeholder='Search Airports...' placeholder='Search Airports...'
limit={10}
data={airports}
value={searchValue}
onChange={onChange}
onBlur={() => setSearchValue('')}
/> />
</div> </div>
<Link className='my-1 mr-2' href={'/profile'}> <Link className='' href={'/profile'}>
<Avatar shape='circle' size={DEFAULT_ICON_SIZE} icon={<AiOutlineUser />} /> <Avatar>
<AiOutlineUser />
</Avatar>
</Link> </Link>
</nav> </nav>
</>
); );
} }

View File

@@ -0,0 +1,5 @@
'use client';
import { createTheme } from '@mantine/core';
export const theme = createTheme({});

View File

@@ -1,14 +0,0 @@
'use client';
import React from 'react';
import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
import type Entity from '@ant-design/cssinjs/es/Cache';
import { useServerInsertedHTML } from 'next/navigation';
const StyledComponentsRegistry = ({ children }: { children: React.ReactNode }) => {
const cache = React.useMemo<Entity>(() => createCache(), [createCache]);
useServerInsertedHTML(() => <style id='antd' dangerouslySetInnerHTML={{ __html: extractStyle(cache, true) }} />);
return <StyleProvider cache={cache}>{children}</StyleProvider>;
};
export default StyledComponentsRegistry;

View File

@@ -1,7 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html, html,
body { body {
padding: 0; padding: 0;

View File

@@ -1,11 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {}
},
plugins: [],
corePlugins: {
preflight: false
}
};