Migrate front end to Mantine
This commit is contained in:
@@ -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}'
|
||||
@echo
|
||||
|
||||
up:
|
||||
build: ## Build Docker service container
|
||||
docker compose build
|
||||
|
||||
up: ## Start Docker service containers
|
||||
docker compose up -d
|
||||
|
||||
down:
|
||||
down: ## Stop Docker service containers
|
||||
docker compose down
|
||||
|
||||
connect:
|
||||
connect: ## Connect to the Weather DB
|
||||
docker exec -it aviation_weather_db psql -U postgres
|
||||
|
||||
lint: ## Run the linter
|
||||
npm run lint
|
||||
|
||||
clean:
|
||||
clean: ## Clean up the service
|
||||
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 "CREATE DATABASE \"${DATABASE_NAME}\";"' || true
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
version: '3'
|
||||
|
||||
name: weather
|
||||
services:
|
||||
weather-db:
|
||||
db:
|
||||
image: postgis/postgis:latest
|
||||
container_name: weather.db
|
||||
container_name: weather-db
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@@ -17,6 +18,22 @@ services:
|
||||
- "${DATABASE_PORT}:5432"
|
||||
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:
|
||||
db:
|
||||
db_logs:
|
||||
db_logs:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: weather-backend
|
||||
@@ -44,8 +44,8 @@ async fn main() -> std::io::Result<()> {
|
||||
server = match listenfd.take_tcp_listener(0)? {
|
||||
Some(listener) => server.listen(listener)?,
|
||||
None => {
|
||||
let host = std::env::var("HOST").expect("Please set host in .env");
|
||||
let port = std::env::var("PORT").expect("Please set port in .env");
|
||||
let host = std::env::var("SERVICE_HOST").expect("Please set host in .env");
|
||||
let port = std::env::var("SERVICE_PORT").expect("Please set port in .env");
|
||||
debug!("Binding server to {}:{}", host, port);
|
||||
server.bind(format!("{}:{}", host, port))?
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -9,9 +9,9 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/cssinjs": "^1.17.0",
|
||||
"@blueprintjs/core": "^5.3.0",
|
||||
"antd": "^5.9.0",
|
||||
"@mantine/core": "^7.0.0",
|
||||
"@mantine/hooks": "^7.0.0",
|
||||
"@mantine/modals": "^7.0.0",
|
||||
"axios": "^1.4.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"next": "^13.4.19",
|
||||
@@ -34,8 +34,9 @@
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"postcss": "^8.4.28",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-preset-mantine": "^1.7.0",
|
||||
"prettier": "^3.0.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "5.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-import': {},
|
||||
'tailwindcss/nesting': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@ export default async function Page({ params }: { params: { icao: string } }) {
|
||||
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'>
|
||||
<h3 className='text-base font-semibold leading-6 text-gray-900'>{airport.full_name}</h3>
|
||||
<div className=''>
|
||||
<h3 className=''>{airport.full_name}</h3>
|
||||
<Link href={'/'}>Back</Link>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -2,18 +2,20 @@ import React from 'react';
|
||||
import RecoilRootWrapper from '@app/recoil-root-wrapper';
|
||||
import Sidebar from '@/components/Sidebar';
|
||||
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/leaflet.css';
|
||||
import StyledComponentsRegistry from '@/lib/AntdRegistry';
|
||||
import { Inter } from 'next/font/google';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
import '@mantine/core/styles.css';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Aviation Weather',
|
||||
description: ''
|
||||
};
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang='en' className='h-full bg-white'>
|
||||
@@ -22,11 +24,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
</head>
|
||||
<body className={`${inter.className} wrapper h-full`}>
|
||||
<RecoilRootWrapper>
|
||||
<StyledComponentsRegistry>
|
||||
<Topbar />
|
||||
<Sidebar />
|
||||
{children}
|
||||
</StyledComponentsRegistry>
|
||||
<MantineProvider>
|
||||
<ModalsProvider>
|
||||
<Topbar />
|
||||
<Sidebar />
|
||||
{children}
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</RecoilRootWrapper>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function Profile() {
|
||||
return <></>;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { DivIcon, LatLngBounds } from 'leaflet';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import { Marker, TileLayer, Tooltip, useMap, useMapEvents } from 'react-leaflet';
|
||||
import MetarDialog from './MetarDialog';
|
||||
import MetarModal from './MetarModal';
|
||||
import { BsCircle, BsCircleFill } from 'react-icons/bs';
|
||||
|
||||
export default function MapTiles() {
|
||||
@@ -133,7 +133,7 @@ export default function MapTiles() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedAirport && <MetarDialog isOpen={isOpen} onClose={() => setIsOpen(false)} airport={selectedAirport} />}
|
||||
{selectedAirport && <MetarModal isOpen={isOpen} onClose={() => setIsOpen(false)} airport={selectedAirport} />}
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
url='http://{s}.tile.osm.org/{z}/{x}/{y}.png'
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
import { Airport } from '@/api/airport.types';
|
||||
import { Metar } from '@/api/metar.types';
|
||||
import { FaArrowsSpin, FaLocationArrow } from 'react-icons/fa6';
|
||||
import { Col, Grid, Modal, Row, Tooltip } from 'antd';
|
||||
import Link from 'next/link';
|
||||
import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
|
||||
import { BsFillCloudRainFill, BsFillCloudRainHeavyFill, BsFillCloudSleetFill, BsFillCloudSnowFill, BsQuestionLg } from 'react-icons/bs';
|
||||
import { useState } from 'react';
|
||||
import { Grid, Modal, Tooltip } from '@mantine/core';
|
||||
|
||||
interface MetarDialogProps {
|
||||
interface MetarModalProps {
|
||||
airport: Airport;
|
||||
isOpen: boolean;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
export default function MetarDialog({ airport, isOpen, onClose }: MetarDialogProps) {
|
||||
export default function MetarModal({ airport, isOpen, onClose }: MetarModalProps) {
|
||||
const [isFavorite, setIsFavorite] = useState(false);
|
||||
|
||||
function handleFavorite(value: boolean) {
|
||||
@@ -44,10 +44,8 @@ export default function MetarDialog({ airport, isOpen, onClose }: MetarDialogPro
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
open={isOpen}
|
||||
onCancel={onClose}
|
||||
closable={false}
|
||||
footer={[]}
|
||||
opened={isOpen}
|
||||
onClose={onClose}
|
||||
className='select-none'
|
||||
>
|
||||
<div className='min-w-0 flex-1'>
|
||||
@@ -131,8 +129,8 @@ function MetarInfo({ metar }: { metar: Metar }) {
|
||||
return (
|
||||
<div>
|
||||
<p className='text-xs font-small text-gray-500'>{metar.raw_text}</p>
|
||||
<Row gutter={18}>
|
||||
<Col className='gutter-row' span={6}>
|
||||
<Grid gutter={18}>
|
||||
<Grid.Col className='gutter-row' span={6}>
|
||||
<span
|
||||
className={`text-sm text-white py-2 px-4 rounded-full
|
||||
${metarBGColor(metar)}
|
||||
@@ -140,15 +138,11 @@ function MetarInfo({ metar }: { metar: Metar }) {
|
||||
>
|
||||
{metar.flight_category ? metar.flight_category : 'UNKN'}
|
||||
</span>
|
||||
</Col>
|
||||
<Col className='gutter-row' span={12}>
|
||||
</Grid.Col>
|
||||
<Grid.Col className='gutter-row' span={12}>
|
||||
{metar.wx_string && metar.wx_string.split(' ').map((wx) => <MetarIcon wx={wx} />)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={2}>Compass TBD Compass TBD Compass TBD Compass TBD Compass TB</Row>
|
||||
<Row gutter={2}>
|
||||
<Col></Col>
|
||||
</Row>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -215,7 +209,7 @@ function MetarIcon({ wx }: { wx: string }) {
|
||||
// color = '';
|
||||
// }
|
||||
return (
|
||||
<Tooltip title={title}>
|
||||
<Tooltip label={title}>
|
||||
<span className={`rounded-full`}>{icon}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import './Sidebar.css';
|
||||
|
||||
export default function Sidebar() {
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { AutoComplete, Avatar } from 'antd';
|
||||
import Link from 'next/link';
|
||||
import { AiOutlineUser } from 'react-icons/ai';
|
||||
import { useState } from 'react';
|
||||
import { getAirports } from '@/api/airport';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const DEFAULT_ICON_SIZE = 40;
|
||||
import { Autocomplete, Avatar } from '@mantine/core';
|
||||
|
||||
export default function Topbar() {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [airports, setAirports] = useState<{ key: string; value: string; label: string }[]>([]);
|
||||
const router = useRouter();
|
||||
|
||||
async function onSearch(value: string) {
|
||||
async function onChange(value: string) {
|
||||
setSearchValue(value);
|
||||
const airportData = await getAirports({ filter: value });
|
||||
setAirports(
|
||||
@@ -26,34 +24,32 @@ export default function Topbar() {
|
||||
);
|
||||
}
|
||||
|
||||
function onSelect(value: string) {
|
||||
setSearchValue('');
|
||||
function onClick(value: string) {
|
||||
router.push(`/airport/${value}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className='w-screen flex bg-gray-700 text-gray-200 justify-between'>
|
||||
<div className='flex'>
|
||||
<Link href={'/'} className='align-middle pt-2.5 px-6 text-lg'>
|
||||
<span>Aviation Weather</span>
|
||||
</Link>
|
||||
<AutoComplete
|
||||
className='w-72 relative top-2'
|
||||
autoFocus
|
||||
defaultActiveFirstOption
|
||||
value={searchValue}
|
||||
options={airports}
|
||||
onSelect={onSelect}
|
||||
onSearch={onSearch}
|
||||
onBlur={() => setSearchValue('')}
|
||||
placeholder='Search Airports...'
|
||||
/>
|
||||
</div>
|
||||
<Link className='my-1 mr-2' href={'/profile'}>
|
||||
<Avatar shape='circle' size={DEFAULT_ICON_SIZE} icon={<AiOutlineUser />} />
|
||||
<nav style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Link href={'/'} style={{ paddingLeft: '2em', paddingRight: '2em', margin: 'auto' }}>
|
||||
<span>Aviation Weather</span>
|
||||
</Link>
|
||||
</nav>
|
||||
</>
|
||||
<Autocomplete
|
||||
autoFocus
|
||||
radius='xl'
|
||||
placeholder='Search Airports...'
|
||||
limit={10}
|
||||
data={airports}
|
||||
value={searchValue}
|
||||
onChange={onChange}
|
||||
onBlur={() => setSearchValue('')}
|
||||
/>
|
||||
</div>
|
||||
<Link className='' href={'/profile'}>
|
||||
<Avatar>
|
||||
<AiOutlineUser />
|
||||
</Avatar>
|
||||
</Link>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
5
weather-ui/src/js/theme.ts
Normal file
5
weather-ui/src/js/theme.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { createTheme } from '@mantine/core';
|
||||
|
||||
export const theme = createTheme({});
|
||||
@@ -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;
|
||||
@@ -1,7 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: [],
|
||||
corePlugins: {
|
||||
preflight: false
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user