Updated UI with accounts and fixed routing
This commit is contained in:
54
ui/package-lock.json
generated
54
ui/package-lock.json
generated
@@ -19,7 +19,8 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-leaflet": "^5.0.0"
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-router": "^7.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
@@ -1551,6 +1552,12 @@
|
||||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||
@@ -2417,6 +2424,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -4222,6 +4238,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz",
|
||||
"integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.6.0",
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"turbo-stream": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
@@ -4446,6 +4486,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -4573,6 +4619,12 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/turbo-stream": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-leaflet": "^5.0.0"
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-router": "^7.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
/* Ensure that the html and body take up the full height */
|
||||
html,
|
||||
body,
|
||||
#root,
|
||||
.App {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Set up Flexbox layout */
|
||||
.App {
|
||||
display: flex;
|
||||
@@ -16,11 +5,6 @@ body,
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.map-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { useEffect, useState } from 'react';
|
||||
import { Airport } from '@lib/airport.types.ts';
|
||||
import AirportDrawer from '@components/AirportDrawer.tsx';
|
||||
import { getWeatherMapUrl } from '@lib/rainViewer.ts';
|
||||
// import { IconRadar } from '@tabler/icons-react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { UnstyledButton } from '@mantine/core';
|
||||
// Fix Leaflet's default icon path issues with Webpack
|
||||
@@ -107,7 +106,7 @@ function App() {
|
||||
style={{ bottom: '80px' }}
|
||||
className={`map-button ${showRadar ? 'active' : ''}`}
|
||||
>
|
||||
Radar
|
||||
R
|
||||
</UnstyledButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
18
ui/src/components/Administration.tsx
Normal file
18
ui/src/components/Administration.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Header } from '@components/Header';
|
||||
import { Navigate } from 'react-router';
|
||||
import { useUserContext } from '@components/context/UserContext.tsx';
|
||||
|
||||
export function Administration() {
|
||||
const { user } = useUserContext();
|
||||
|
||||
if (user == undefined) {
|
||||
return <Navigate to={'/'} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
Todo: administration {user?.email}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -34,18 +34,18 @@ export default function AirportMarker({
|
||||
);
|
||||
}
|
||||
|
||||
function getMarkerColor(flightCategory: 'VFR' | 'MVFR' | 'LIFR' | 'IFR' | 'UNKN'): string {
|
||||
function getMarkerInfo(flightCategory: 'VFR' | 'MVFR' | 'LIFR' | 'IFR' | 'UNKN'): [string, number] {
|
||||
switch (flightCategory) {
|
||||
case 'IFR':
|
||||
return '#ff0100';
|
||||
return ['#ff0100', 5];
|
||||
case 'LIFR':
|
||||
return '#7f007f';
|
||||
return ['#7f007f', 6];
|
||||
case 'MVFR':
|
||||
return '#00f';
|
||||
return ['#00f', 7];
|
||||
case 'VFR':
|
||||
return '#018000';
|
||||
return ['#018000', 8];
|
||||
case 'UNKN':
|
||||
return '#696969';
|
||||
return ['#696969', 4];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ function createCustomIcon(airport: Airport): L.DivIcon {
|
||||
background-color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;">
|
||||
justify-content: center;
|
||||
z-index: {info[1]}">
|
||||
<span style="color: black; font-size: 8px; font-weight: bold;">H</span>
|
||||
</div>
|
||||
`,
|
||||
@@ -72,15 +73,16 @@ function createCustomIcon(airport: Airport): L.DivIcon {
|
||||
} else {
|
||||
// Default to a filled circle.
|
||||
const flightCategory = airport.latest_metar?.flight_category || 'UNKN';
|
||||
const color = getMarkerColor(flightCategory);
|
||||
const info = getMarkerInfo(flightCategory);
|
||||
if (flightCategory == 'UNKN') {
|
||||
return L.divIcon({
|
||||
html: `
|
||||
<div style="
|
||||
background-color: ${color};
|
||||
background-color: ${info[0]};
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;">
|
||||
border-radius: 50%;
|
||||
z-index: {info[1]}">
|
||||
</div>
|
||||
`,
|
||||
className: '',
|
||||
@@ -91,11 +93,12 @@ function createCustomIcon(airport: Airport): L.DivIcon {
|
||||
return L.divIcon({
|
||||
html: `
|
||||
<div style="
|
||||
background-color: ${color};
|
||||
background-color: ${info[0]};
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;">
|
||||
border: 2px solid #fff;
|
||||
z-index: {info[1]}">
|
||||
</div>
|
||||
`,
|
||||
className: '',
|
||||
|
||||
@@ -157,7 +157,7 @@ export function HeaderModal({ type, toggle, login, register }: HeaderModalProps)
|
||||
/>
|
||||
<PasswordInput
|
||||
label='Password'
|
||||
description='Passwords must be at least 10 characters long, contain at least one number, one uppercase letter, one lowercase letter, and one special character.'
|
||||
description='Passwords must be at least 8 characters long, contain at least one number, one uppercase letter, one lowercase letter, and one special character.'
|
||||
placeholder='Your password'
|
||||
required
|
||||
mt='md'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { User } from '@/lib/account.types';
|
||||
// import { setPicture } from "@/api/users";
|
||||
import { Menu, UnstyledButton, Group, Avatar, Card, FileButton, Grid, Button, Text } from '@mantine/core';
|
||||
import { useNavigate } from 'react-router';
|
||||
// import './styles.css';
|
||||
|
||||
interface HeaderUserProps {
|
||||
@@ -10,6 +11,8 @@ interface HeaderUserProps {
|
||||
}
|
||||
|
||||
export default function HeaderUser({ user, profilePicture, logout }: HeaderUserProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Menu shadow='md' width={200} openDelay={100} closeDelay={400} zIndex={1000}>
|
||||
<Menu.Target>
|
||||
@@ -66,7 +69,7 @@ export default function HeaderUser({ user, profilePicture, logout }: HeaderUserP
|
||||
</Text>
|
||||
<Grid mt='xl'>
|
||||
<Grid.Col span={6}>
|
||||
<Button fullWidth radius='md' size='xs' variant='default'>
|
||||
<Button fullWidth radius='md' size='xs' variant='default' onClick={() => navigate('/profile')}>
|
||||
Profile
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
@@ -75,9 +78,9 @@ export default function HeaderUser({ user, profilePicture, logout }: HeaderUserP
|
||||
Logout
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
{user.role == 'admin' && (
|
||||
{user.role == 'ADMIN' && (
|
||||
<Grid.Col span={12}>
|
||||
<Button fullWidth radius='md' size='xs' variant='default'>
|
||||
<Button fullWidth radius='md' size='xs' variant='default' onClick={() => navigate('/administration')}>
|
||||
Administration
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { Avatar, Box, Burger, Button, Group, Text } from '@mantine/core';
|
||||
import { useDisclosure, useToggle } from '@mantine/hooks';
|
||||
import classes from './Header.module.css';
|
||||
import { HeaderModal } from '@components/Header/HeaderModal.tsx';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import Cookies from 'js-cookie';
|
||||
import { User } from '@lib/account.types.ts';
|
||||
import { login, logout, register } from '@lib/account.ts';
|
||||
import HeaderUser from '@components/Header/HeaderUser.tsx';
|
||||
import { useUserContext } from '@components/context/UserContext.tsx';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
// const links = [
|
||||
// { link: '/', label: 'Map' },
|
||||
@@ -16,9 +15,9 @@ import HeaderUser from '@components/Header/HeaderUser.tsx';
|
||||
// ];
|
||||
|
||||
export function Header() {
|
||||
const { user, setUser } = useUserContext();
|
||||
const [opened, { toggle }] = useDisclosure(false);
|
||||
const [modalType, modalToggle] = useToggle([undefined, 'login', 'register', 'reset']);
|
||||
const [user, setUser] = useState<User | undefined>(undefined);
|
||||
// const [active, setActive] = useState(links[0].link);
|
||||
|
||||
// const navItems = links.map((link) => (
|
||||
@@ -62,8 +61,8 @@ export function Header() {
|
||||
|
||||
async function logoutUser(): Promise<void> {
|
||||
await logout();
|
||||
Cookies.remove('logged_in');
|
||||
setUser(undefined);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
async function registerUser({
|
||||
@@ -131,12 +130,14 @@ export function Header() {
|
||||
<Box>
|
||||
<header className={classes.header}>
|
||||
<Group justify='space-between' h='100%'>
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom='sm' size='sm' />
|
||||
<Group align='center' gap='xs'>
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom='xs' size='sm' />
|
||||
<Avatar src='/logo.svg' alt='logo' />
|
||||
<Link to='/'>
|
||||
<Avatar src='/logo.svg' alt='logo' onClick={toggle} />
|
||||
</Link>
|
||||
<Text>Aviation Data</Text>
|
||||
</Group>
|
||||
{/*<Group gap={5} visibleFrom='xs' className={classes.navGroup}>*/}
|
||||
{/*<Group gap={5} visibleFrom='sm' className={classes.navGroup}>*/}
|
||||
{/* {navItems}*/}
|
||||
{/*</Group>*/}
|
||||
<Group align='center' gap='xs'>
|
||||
|
||||
18
ui/src/components/Profile.tsx
Normal file
18
ui/src/components/Profile.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Header } from '@components/Header';
|
||||
import { useUserContext } from '@components/context/UserContext.tsx';
|
||||
import { Navigate } from 'react-router';
|
||||
|
||||
export function Profile() {
|
||||
const { user } = useUserContext();
|
||||
|
||||
if (user == undefined) {
|
||||
return <Navigate to={'/'} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
Todo: profile {user?.email}
|
||||
</>
|
||||
);
|
||||
}
|
||||
18
ui/src/components/context/UserContext.tsx
Normal file
18
ui/src/components/context/UserContext.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { User } from '@lib/account.types.ts';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
interface UserContextType {
|
||||
user?: User;
|
||||
setUser: (user: User | undefined) => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const UserContext = createContext<UserContextType>({
|
||||
user: undefined,
|
||||
setUser: () => {},
|
||||
loading: true
|
||||
});
|
||||
|
||||
export function useUserContext(): UserContextType {
|
||||
return useContext(UserContext);
|
||||
}
|
||||
33
ui/src/components/context/UserProvider.tsx
Normal file
33
ui/src/components/context/UserProvider.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { UserContext } from './UserContext.tsx';
|
||||
import { refresh } from '@lib/account.ts';
|
||||
import { User } from '@lib/account.types.ts';
|
||||
import { Center, Loader } from '@mantine/core';
|
||||
|
||||
export function UserProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
refresh().then((refreshUser) => {
|
||||
if (refreshUser) {
|
||||
setUser(refreshUser);
|
||||
} else {
|
||||
setUser(undefined);
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ user, setUser, loading }}>
|
||||
{loading ? (
|
||||
<Center style={{ height: '100vh' }}>
|
||||
<Loader size='xl' />
|
||||
</Center>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Ensure that the html and body take up the full height */
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { getRequest, postRequest } from '.';
|
||||
import { RegisterUser, ResponseAuth, User } from './account.types';
|
||||
import { RegisterUser, User } from './account.types';
|
||||
|
||||
export async function login(email: string, password: string): Promise<User | undefined> {
|
||||
const response = await postRequest('account/login', { email, password });
|
||||
@@ -24,40 +23,11 @@ export async function logout() {
|
||||
return await postRequest('account/logout', {});
|
||||
}
|
||||
|
||||
export async function refresh(refresh_token_rotation?: boolean): Promise<ResponseAuth | undefined> {
|
||||
const response = await getRequest('account/refresh', { refresh_token_rotation });
|
||||
export async function refresh(): Promise<User | undefined> {
|
||||
const response = await getRequest('account/session');
|
||||
if (response?.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function me(): Promise<ResponseAuth | undefined> {
|
||||
const response = await getRequest('account/me');
|
||||
if (response?.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the logged_in cookie every interval. By default, the interval is 14 minutes.
|
||||
* @param interval
|
||||
* @returns interval id
|
||||
*/
|
||||
export function refreshLoggedIn(interval = 840000) {
|
||||
let loggedIn = Cookies.get('logged_in');
|
||||
const id = setInterval(async () => {
|
||||
const cookie = Cookies.get('logged_in');
|
||||
if (cookie != loggedIn) {
|
||||
loggedIn = cookie;
|
||||
const response = await refresh(true);
|
||||
if (!response) {
|
||||
Cookies.remove('logged_in');
|
||||
}
|
||||
}
|
||||
}, interval);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
export interface ResponseAuth {
|
||||
token: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface RegisterUser {
|
||||
email: string;
|
||||
password: string;
|
||||
|
||||
@@ -4,6 +4,10 @@ import './index.css';
|
||||
import App from './App.tsx';
|
||||
import { createTheme, MantineProvider } from '@mantine/core';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import { UserProvider } from '@components/context/UserProvider.tsx';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router';
|
||||
import { Profile } from '@components/Profile.tsx';
|
||||
import { Administration } from '@components/Administration.tsx';
|
||||
|
||||
const theme = createTheme({
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
@@ -11,9 +15,17 @@ const theme = createTheme({
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<MantineProvider theme={theme} defaultColorScheme={'dark'}>
|
||||
<Notifications zIndex={2000} />
|
||||
<App />
|
||||
</MantineProvider>
|
||||
<BrowserRouter>
|
||||
<MantineProvider theme={theme} defaultColorScheme={'dark'}>
|
||||
<Notifications zIndex={2000} />
|
||||
<UserProvider>
|
||||
<Routes>
|
||||
<Route path='/' element={<App />} />
|
||||
<Route path='/profile' element={<Profile />} />
|
||||
<Route path='/administration' element={<Administration />} />
|
||||
</Routes>
|
||||
</UserProvider>
|
||||
</MantineProvider>
|
||||
</BrowserRouter>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user