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}'
|
@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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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))?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"jsxSingleQuote": true,
|
"jsxSingleQuote": true,
|
||||||
"printWidth": 120
|
"printWidth": 120
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
|
'postcss-preset-mantine': {},
|
||||||
'postcss-import': {},
|
'postcss-import': {},
|
||||||
'tailwindcss/nesting': {},
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {}
|
autoprefixer: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
<Topbar />
|
<ModalsProvider>
|
||||||
<Sidebar />
|
<Topbar />
|
||||||
{children}
|
<Sidebar />
|
||||||
</StyledComponentsRegistry>
|
{children}
|
||||||
|
</ModalsProvider>
|
||||||
|
</MantineProvider>
|
||||||
</RecoilRootWrapper>
|
</RecoilRootWrapper>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 { 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='© <a href="https://www.osm.org/copyright">OpenStreetMap</a> contributors'
|
attribution='© <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'
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import './Sidebar.css';
|
import './Sidebar.css';
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
<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 />} />
|
|
||||||
</Link>
|
</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,
|
html,
|
||||||
body {
|
body {
|
||||||
padding: 0;
|
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