Revize 0d90d67b
Přidáno uživatelem Václav Honzík před téměř 3 roky(ů)
frontend/src/App.tsx | ||
---|---|---|
11 | 11 |
import CatalogItemDetail from './features/Catalog/CatalogItemDetail' |
12 | 12 |
import Navigation from './features/Navigation/Navigation' |
13 | 13 |
import TrackingTool from './features/TrackingTool/TrackingTool' |
14 |
import Logout from './features/Auth/Logout' |
|
15 |
import ThemeWrapper from './features/Theme/ThemeWrapper' |
|
14 | 16 |
|
15 | 17 |
const App = () => { |
16 |
const theme: Theme = useSelector((state: RootState) => state.theme.theme) |
|
17 | 18 |
|
18 | 19 |
return ( |
19 |
<ThemeProvider theme={theme}>
|
|
20 |
<ThemeWrapper>
|
|
20 | 21 |
<Navigation> |
21 | 22 |
<Container> |
22 | 23 |
<Routes> |
... | ... | |
27 | 28 |
element={<CatalogItemDetail />} |
28 | 29 |
/> |
29 | 30 |
<Route path="/login" element={<Login />} /> |
31 |
<Route path="/logout" element={<Logout />} /> |
|
30 | 32 |
<Route path="/map" element={<TrackingTool />} /> |
31 | 33 |
<Route path="*" element={<NotFound />} /> |
32 | 34 |
</Routes> |
33 | 35 |
</Container> |
34 | 36 |
</Navigation> |
35 |
</ThemeProvider>
|
|
37 |
</ThemeWrapper>
|
|
36 | 38 |
) |
37 | 39 |
} |
38 | 40 |
|
frontend/src/features/Auth/Login.tsx | ||
---|---|---|
50 | 50 |
return ( |
51 | 51 |
<Fragment> |
52 | 52 |
<Typography variant="h3">Login</Typography> |
53 |
<p>Credentials = admin:password</p> |
|
54 | 53 |
|
55 | 54 |
<form onSubmit={formik.handleSubmit}> |
56 | 55 |
<TextField |
frontend/src/features/Auth/Logout.tsx | ||
---|---|---|
1 |
import { useEffect } from 'react' |
|
2 |
import { useDispatch, useSelector } from 'react-redux' |
|
3 |
import { Navigate } from 'react-router-dom' |
|
4 |
import { RootState } from '../redux/store' |
|
5 |
import { logout } from './userSlice' |
|
6 |
|
|
7 |
// Component that logs the user out if they are logged in |
|
8 |
const Logout = () => { |
|
9 |
const userLoggedIn = useSelector( |
|
10 |
(state: RootState) => state.user.isLoggedIn |
|
11 |
) |
|
12 |
|
|
13 |
const dispatch = useDispatch() |
|
14 |
|
|
15 |
// Check whether the user is logged in and if so log them out |
|
16 |
useEffect(() => { |
|
17 |
if (userLoggedIn) { |
|
18 |
dispatch(logout()) |
|
19 |
} |
|
20 |
}, [dispatch, userLoggedIn]) |
|
21 |
|
|
22 |
return <Navigate to="/" /> |
|
23 |
} |
|
24 |
|
|
25 |
export default Logout |
frontend/src/features/Auth/userSlice.ts | ||
---|---|---|
52 | 52 |
return ({ ...state, ...action.payload }) |
53 | 53 |
}) |
54 | 54 |
builder.addCase(logIn.rejected, (state, action) => { |
55 |
if (action.payload && typeof action.payload === 'string') {
|
|
56 |
return ({ ...state, lastErr: action.payload })
|
|
55 |
if (action.payload && typeof action.error.message === 'string') {
|
|
56 |
return ({ ...state, lastErr: action.error.message })
|
|
57 | 57 |
} |
58 | 58 |
}) |
59 | 59 |
}, |
frontend/src/features/Catalog/CatalogFilter.tsx | ||
---|---|---|
6 | 6 |
Stack, |
7 | 7 |
TextField, |
8 | 8 |
} from '@mui/material' |
9 |
import { Fragment } from 'react' |
|
9 |
import { Fragment, useEffect } from 'react'
|
|
10 | 10 |
import { useDispatch, useSelector } from 'react-redux' |
11 | 11 |
import { setFilter, setFilterOpen } from './catalogSlice' |
12 | 12 |
import { fetchItems } from './catalogThunks' |
... | ... | |
29 | 29 |
dispatch(fetchItems()) |
30 | 30 |
} |
31 | 31 |
|
32 |
useEffect(() => { |
|
33 |
console.log(filter) |
|
34 |
}, [filter]) |
|
35 |
|
|
32 | 36 |
return ( |
33 | 37 |
<Fragment> |
34 | 38 |
<Button |
... | ... | |
51 | 55 |
id="name" |
52 | 56 |
label="Name" |
53 | 57 |
onChange={(e: any) => { |
54 |
filter.name = e.target.value |
|
55 |
dispatch(setFilter(filter)) |
|
58 |
dispatch(setFilter({ |
|
59 |
...filter, |
|
60 |
name: e.target.value |
|
61 |
})) |
|
56 | 62 |
}} |
57 | 63 |
value={filter.name} |
58 | 64 |
/> |
... | ... | |
61 | 67 |
id="type" |
62 | 68 |
label="Type" |
63 | 69 |
onChange={(e: any) => { |
64 |
filter.type = e.target.value |
|
65 |
dispatch(setFilter(filter)) |
|
70 |
dispatch(setFilter({ |
|
71 |
...filter, |
|
72 |
type: e.target.value |
|
73 |
})) |
|
66 | 74 |
}} |
67 | 75 |
value={filter.type} |
68 | 76 |
/> |
... | ... | |
78 | 86 |
id="stateOrTerritory" |
79 | 87 |
label="State or territory" |
80 | 88 |
onChange={(e: any) => { |
81 |
filter.country = e.target.value |
|
82 |
dispatch(setFilter(filter)) |
|
89 |
dispatch(setFilter({ |
|
90 |
...filter, |
|
91 |
country: e.target.value |
|
92 |
})) |
|
83 | 93 |
}} |
84 | 94 |
value={filter.country} |
85 | 95 |
/> |
frontend/src/features/Catalog/catalogSlice.tsx | ||
---|---|---|
68 | 68 |
builder.addCase(fetchItems.rejected, (state, action) => ({ |
69 | 69 |
...state, |
70 | 70 |
loading: false, |
71 |
error: action.payload as string,
|
|
71 |
error: action.error.message as string,
|
|
72 | 72 |
})) |
73 | 73 |
}, |
74 | 74 |
}) |
frontend/src/features/Home/Home.tsx | ||
---|---|---|
5 | 5 |
import { RootState } from '../redux/store' |
6 | 6 |
|
7 | 7 |
const Home = () => { |
8 |
const dispatch = useDispatch() |
|
9 |
|
|
10 |
const userLoggedIn = useSelector( |
|
11 |
(state: RootState) => state.user.isLoggedIn |
|
12 |
) |
|
13 |
|
|
14 | 8 |
return ( |
15 | 9 |
<Fragment> |
16 | 10 |
<h1>Home</h1> |
17 |
{userLoggedIn ? ( |
|
18 |
<Button |
|
19 |
size="large" |
|
20 |
variant="contained" |
|
21 |
color="primary" |
|
22 |
onClick={() => dispatch(logout())} |
|
23 |
> |
|
24 |
Logout |
|
25 |
</Button> |
|
26 |
) : null} |
|
27 | 11 |
</Fragment> |
28 | 12 |
) |
29 | 13 |
} |
frontend/src/features/Navigation/Navigation.tsx | ||
---|---|---|
5 | 5 |
import CssBaseline from '@mui/material/CssBaseline' |
6 | 6 |
import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar' |
7 | 7 |
import Toolbar from '@mui/material/Toolbar' |
8 |
import List from '@mui/material/List' |
|
9 | 8 |
import Typography from '@mui/material/Typography' |
10 |
import Divider from '@mui/material/Divider' |
|
11 | 9 |
import IconButton from '@mui/material/IconButton' |
12 | 10 |
import MenuIcon from '@mui/icons-material/Menu' |
13 |
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' |
|
14 |
import ChevronRightIcon from '@mui/icons-material/ChevronRight' |
|
15 |
import ListItem from '@mui/material/ListItem' |
|
16 |
import ListItemIcon from '@mui/material/ListItemIcon' |
|
17 |
import ListItemText from '@mui/material/ListItemText' |
|
18 |
import InboxIcon from '@mui/icons-material/MoveToInbox' |
|
19 |
import MailIcon from '@mui/icons-material/Mail' |
|
20 | 11 |
import { Fragment, FunctionComponent } from 'react' |
21 | 12 |
import NavigationMenu from './NavigationMenu' |
22 |
import { Paper } from '@mui/material' |
|
13 |
import { Paper, Stack } from '@mui/material' |
|
14 |
import { useDispatch, useSelector } from 'react-redux' |
|
15 |
import { toggleTheme } from '../Theme/themeSlice' |
|
16 |
import { RootState } from '../redux/store' |
|
17 |
|
|
18 |
import DarkModeIcon from '@mui/icons-material/DarkMode' |
|
19 |
import LightModeIcon from '@mui/icons-material/LightMode' |
|
23 | 20 |
|
24 | 21 |
const drawerWidth = 240 |
25 | 22 |
|
... | ... | |
83 | 80 |
setOpen(true) |
84 | 81 |
} |
85 | 82 |
|
83 |
const colorThemeMode = useSelector( |
|
84 |
(state: RootState) => state.theme.paletteMode |
|
85 |
) |
|
86 |
|
|
87 |
const dispatch = useDispatch() |
|
88 |
|
|
89 |
const onToggleTheme = () => { |
|
90 |
dispatch(toggleTheme()) |
|
91 |
} |
|
92 |
|
|
86 | 93 |
return ( |
87 | 94 |
<Fragment> |
88 | 95 |
<Paper style={{ minHeight: '100vh', borderRadius: 0 }}> |
... | ... | |
99 | 106 |
> |
100 | 107 |
<MenuIcon /> |
101 | 108 |
</IconButton> |
102 |
<Typography variant="h6" noWrap component="div"> |
|
103 |
Assyrian Toponyms App Prototype |
|
104 |
</Typography> |
|
109 |
{/* <Box> */} |
|
110 |
<Typography variant="h6" noWrap component="div"> |
|
111 |
Assyrian Toponyms App Prototype |
|
112 |
</Typography> |
|
113 |
<Stack sx={{ml: 'auto'}} alignItems="flex-end"> |
|
114 |
{colorThemeMode === 'dark' ? ( |
|
115 |
<IconButton onClick={onToggleTheme}> |
|
116 |
<LightModeIcon /> |
|
117 |
</IconButton> |
|
118 |
) : ( |
|
119 |
<IconButton onClick={onToggleTheme}> |
|
120 |
<DarkModeIcon /> |
|
121 |
</IconButton> |
|
122 |
)} |
|
123 |
</Stack> |
|
124 |
{/* </Box> */} |
|
105 | 125 |
</Toolbar> |
106 | 126 |
</AppBar> |
107 | 127 |
<NavigationMenu |
frontend/src/features/Navigation/navigationMenuItems.ts | ||
---|---|---|
6 | 6 |
import PersonIcon from '@mui/icons-material/Person' |
7 | 7 |
import LoginIcon from '@mui/icons-material/Login' |
8 | 8 |
import { SvgIconTypeMap } from '@mui/material' |
9 |
import DataSaverOffIcon from '@mui/icons-material/DataSaverOff' |
|
10 |
import LogoutIcon from '@mui/icons-material/Logout' |
|
9 | 11 |
|
10 | 12 |
export interface NavigationMenuItem { |
11 | 13 |
name: string |
... | ... | |
18 | 20 |
|
19 | 21 |
const visitorRole = 'VISITOR' |
20 | 22 |
const visitorRoleOnly = 'VISITOR_ONLY' |
23 |
const loggedInRole = 'LOGGED_IN' |
|
21 | 24 |
const visitorAccess = new Set([visitorRole]) |
25 |
const adminAccess = new Set(['ADMIN']) |
|
26 |
const loggedInAccess = new Set([loggedInRole]) |
|
22 | 27 |
|
23 | 28 |
const items: NavigationMenuItem[] = [ |
24 | 29 |
{ |
... | ... | |
53 | 58 |
{ |
54 | 59 |
name: 'Admin', |
55 | 60 |
path: '/admin', |
56 |
accessibleTo: new Set(['ADMIN']),
|
|
61 |
accessibleTo: adminAccess,
|
|
57 | 62 |
icon: PersonIcon, |
58 |
position: 4,
|
|
63 |
position: 5,
|
|
59 | 64 |
}, |
60 | 65 |
// TODO move this to the top |
61 | 66 |
{ |
... | ... | |
65 | 70 |
icon: LoginIcon, |
66 | 71 |
position: 5, |
67 | 72 |
}, |
73 |
{ |
|
74 |
name: 'Statistics', |
|
75 |
path: '/stats', |
|
76 |
accessibleTo: adminAccess, |
|
77 |
icon: DataSaverOffIcon, |
|
78 |
position: 4, |
|
79 |
}, |
|
80 |
{ |
|
81 |
name: 'Logout', |
|
82 |
path: '/logout', |
|
83 |
accessibleTo: loggedInAccess, |
|
84 |
icon: LogoutIcon, |
|
85 |
position: 1337, |
|
86 |
} |
|
68 | 87 |
] |
69 | 88 |
|
70 | 89 |
const getNavigationItems = (_userRoles: string[]): NavigationMenuItem[] => { |
... | ... | |
74 | 93 |
userRoles.push( visitorRole, visitorRoleOnly) |
75 | 94 |
} else { |
76 | 95 |
userRoles.push(visitorRole) |
96 |
userRoles.push(loggedInRole) |
|
77 | 97 |
} |
78 | 98 |
|
79 | 99 |
return items // else return everything the user has privileges to |
frontend/src/features/Reusables/ButtonOpenableDialog.tsx | ||
---|---|---|
1 |
import { Button, Dialog } from '@mui/material' |
|
2 |
import { Fragment, FunctionComponent, ReactNode, useState } from 'react' |
|
3 |
|
|
4 |
export interface ButtonOpenableDialogProps { |
|
5 |
onOpenCallback?: () => void // this callback is always executed when the dialog is opened |
|
6 |
onCloseCallback?: () => void // this callback is always executed when the dialog is closed |
|
7 |
buttonText: string // the text of the button that opens the dialog |
|
8 |
buttonColor: // the color of the button that opens the dialog |
|
9 |
'primary' | 'secondary' | 'warning' | 'error' | 'success' | 'inherit' |
|
10 |
buttonVariant: 'text' | 'outlined' | 'contained' // the variant of the button that opens the dialog |
|
11 |
children: ReactNode // the content of the dialog |
|
12 |
maxWidth?: 'xs' | 'sm' | 'md' | 'lg' // the max width of the dialog |
|
13 |
} |
|
14 |
|
|
15 |
// Generic dialog that can be opened by a button and closed by clicking on the backdrop. |
|
16 |
const ButtonOpenableDialog: FunctionComponent<ButtonOpenableDialogProps> = ({ |
|
17 |
onOpenCallback, |
|
18 |
onCloseCallback, |
|
19 |
buttonText, |
|
20 |
buttonColor, |
|
21 |
buttonVariant, |
|
22 |
children, |
|
23 |
maxWidth, |
|
24 |
}) => { |
|
25 |
const [open, setOpen] = useState(false) |
|
26 |
|
|
27 |
// Change maxWidth to large if its undefined |
|
28 |
maxWidth = maxWidth ?? 'lg' |
|
29 |
|
|
30 |
const onOpen = () => { |
|
31 |
if (onOpenCallback) { |
|
32 |
// execute the callback if exists |
|
33 |
onOpenCallback() |
|
34 |
} |
|
35 |
setOpen(true) |
|
36 |
} |
|
37 |
const onClose = () => { |
|
38 |
if (onCloseCallback) { |
|
39 |
// execute the callback if exists |
|
40 |
onCloseCallback() |
|
41 |
} |
|
42 |
setOpen(false) |
|
43 |
} |
|
44 |
|
|
45 |
return ( |
|
46 |
<Fragment> |
|
47 |
<Button |
|
48 |
onClick={onOpen} |
|
49 |
color={buttonColor} |
|
50 |
variant={buttonVariant} |
|
51 |
> |
|
52 |
{buttonText} |
|
53 |
</Button> |
|
54 |
<Dialog |
|
55 |
fullWidth={true} |
|
56 |
open={open} |
|
57 |
onClose={onClose} |
|
58 |
maxWidth={maxWidth} |
|
59 |
> |
|
60 |
{children} |
|
61 |
</Dialog> |
|
62 |
</Fragment> |
|
63 |
) |
|
64 |
} |
|
65 |
|
|
66 |
export default ButtonOpenableDialog |
frontend/src/features/Theme/ThemeWrapper.tsx | ||
---|---|---|
1 |
import { PaletteMode } from "@mui/material" |
|
2 |
import { createTheme, Theme, ThemeProvider } from '@mui/material/styles' |
|
3 |
import { FunctionComponent, ReactNode, useEffect, useState } from "react" |
|
4 |
import { useSelector } from "react-redux" |
|
5 |
import { RootState } from "../redux/store" |
|
6 |
|
|
7 |
export interface ThemeWrapperProps { |
|
8 |
children: ReactNode |
|
9 |
} |
|
10 |
|
|
11 |
const ThemeWrapper: FunctionComponent<ThemeWrapperProps> = ({ children }) => { |
|
12 |
|
|
13 |
const buildTheme = (paletteMode: PaletteMode) => |
|
14 |
createTheme({ |
|
15 |
palette: { |
|
16 |
mode: paletteMode, |
|
17 |
}, |
|
18 |
shape: { |
|
19 |
borderRadius: 16 |
|
20 |
}, |
|
21 |
typography: { |
|
22 |
fontFamily: [ |
|
23 |
'-apple-system', |
|
24 |
'BlinkMacSystemFont', |
|
25 |
'"Segoe UI"', |
|
26 |
'Roboto', |
|
27 |
'"Helvetica Neue"', |
|
28 |
'Arial', |
|
29 |
'sans-serif', |
|
30 |
'"Apple Color Emoji"', |
|
31 |
'"Segoe UI Emoji"', |
|
32 |
'"Segoe UI Symbol"', |
|
33 |
].join(','), |
|
34 |
}, |
|
35 |
}) |
|
36 |
|
|
37 |
const paletteMode = useSelector( |
|
38 |
(state: RootState) => state.theme.paletteMode |
|
39 |
) |
|
40 |
|
|
41 |
const [theme, setTheme] = useState<Theme>(buildTheme(paletteMode)) |
|
42 |
useEffect(() => { |
|
43 |
setTheme(() => { |
|
44 |
return buildTheme(paletteMode) |
|
45 |
}) |
|
46 |
}, [paletteMode]) |
|
47 |
|
|
48 |
return ( |
|
49 |
<ThemeProvider theme={theme}> |
|
50 |
{children} |
|
51 |
</ThemeProvider> |
|
52 |
) |
|
53 |
} |
|
54 |
|
|
55 |
export default ThemeWrapper |
frontend/src/features/Theme/themeReducer.ts | ||
---|---|---|
1 |
import { createTheme, Theme } from '@mui/material/styles' |
|
2 |
import { AnyAction } from 'redux' |
|
3 |
import { persist } from '../../utils/statePersistence' |
|
4 |
|
|
5 |
export interface ThemeState { |
|
6 |
theme: Theme |
|
7 |
themeType: 'Light' | 'Dark' |
|
8 |
} |
|
9 |
|
|
10 |
const statePersistName = 'theme' |
|
11 |
|
|
12 |
const initialTheme = createTheme({ |
|
13 |
palette: { |
|
14 |
mode: 'light' |
|
15 |
}, |
|
16 |
typography: { |
|
17 |
fontFamily: [ |
|
18 |
'-apple-system', |
|
19 |
'BlinkMacSystemFont', |
|
20 |
'"Segoe UI"', |
|
21 |
'Roboto', |
|
22 |
'"Helvetica Neue"', |
|
23 |
'Arial', |
|
24 |
'sans-serif', |
|
25 |
'"Apple Color Emoji"', |
|
26 |
'"Segoe UI Emoji"', |
|
27 |
'"Segoe UI Symbol"', |
|
28 |
].join(','), |
|
29 |
} |
|
30 |
}) |
|
31 |
const initialState: ThemeState = { |
|
32 |
theme: initialTheme, |
|
33 |
themeType: 'Light', |
|
34 |
} |
|
35 |
|
|
36 |
export enum ThemeStateActions { |
|
37 |
TOGGLE_THEME = 'TOGGLE_THEME', |
|
38 |
SET_LIGHT_MODE = 'SET_LIGHT_MODE', |
|
39 |
SET_DARK_MODE = 'SET_DARK_MODE', |
|
40 |
} |
|
41 |
|
|
42 |
const themeReducer = (state: ThemeState = initialState, action: AnyAction) => { |
|
43 |
// TODO add all the actions |
|
44 |
switch (action.type) { |
|
45 |
case ThemeStateActions.TOGGLE_THEME: |
|
46 |
return persist(statePersistName, state) |
|
47 |
|
|
48 |
default: |
|
49 |
return state |
|
50 |
} |
|
51 |
} |
|
52 |
|
|
53 |
export default themeReducer |
frontend/src/features/Theme/themeSlice.ts | ||
---|---|---|
1 |
import { createSlice } from '@reduxjs/toolkit' |
|
2 |
import { persistReducer } from 'redux-persist' |
|
3 |
import storage from 'redux-persist/lib/storage' |
|
4 |
import { PaletteMode } from '@mui/material' |
|
5 |
|
|
6 |
export interface ThemeState { |
|
7 |
paletteMode: PaletteMode |
|
8 |
} |
|
9 |
|
|
10 |
const persistConfig = { |
|
11 |
key: 'theme', |
|
12 |
storage, // localStorage for browsers |
|
13 |
} |
|
14 |
|
|
15 |
const initialState: ThemeState = { |
|
16 |
paletteMode: 'light', |
|
17 |
} |
|
18 |
|
|
19 |
const themeSlice = createSlice({ |
|
20 |
name: 'theme', |
|
21 |
initialState, |
|
22 |
reducers: { |
|
23 |
toggleTheme: (state) => ({ |
|
24 |
...state, |
|
25 |
paletteMode: state.paletteMode === 'light' ? 'dark' : 'light', |
|
26 |
}), |
|
27 |
}, |
|
28 |
}) |
|
29 |
|
|
30 |
const themeReducer = persistReducer(persistConfig, themeSlice.reducer) |
|
31 |
// const themeReducer = themeSlice.reducer |
|
32 |
export const { toggleTheme } = themeSlice.actions |
|
33 |
export default themeReducer |
frontend/src/features/TrackingTool/FileUpload.tsx | ||
---|---|---|
1 |
import { |
|
2 |
Button, |
|
3 |
DialogContent, |
|
4 |
DialogTitle, |
|
5 |
Link, |
|
6 |
Stack, |
|
7 |
Typography, |
|
8 |
} from '@mui/material' |
|
9 |
import { useFormik } from 'formik' |
|
10 |
import { Fragment, useState } from 'react' |
|
11 |
import * as yup from 'yup' |
|
12 |
import axiosInstance from '../../api/api' |
|
13 |
import ButtonOpenableDialog from '../Reusables/ButtonOpenableDialog' |
|
14 |
import AttachmentIcon from '@mui/icons-material/Attachment' |
|
15 |
import DeleteIcon from '@mui/icons-material/Delete' |
|
16 |
import SendIcon from '@mui/icons-material/Send' |
|
17 |
|
|
18 |
const FileUpload = () => { |
|
19 |
const [filename, setFilename] = useState<string | undefined>(undefined) |
|
20 |
|
|
21 |
const validationSchema = yup.object().shape({ |
|
22 |
file: yup.mixed().required('File is required'), |
|
23 |
}) |
|
24 |
|
|
25 |
const [submitButtonEnabled, setSubmitButtonEnabled] = useState(true) |
|
26 |
|
|
27 |
const formik = useFormik({ |
|
28 |
initialValues: { |
|
29 |
file: undefined, |
|
30 |
}, |
|
31 |
validationSchema, |
|
32 |
onSubmit: async (values) => { |
|
33 |
// TODO actually send the file somewhere |
|
34 |
// TODO implement me |
|
35 |
|
|
36 |
const formData = new FormData() |
|
37 |
// @ts-ignore for now |
|
38 |
formData.append('file', values.file as File) |
|
39 |
|
|
40 |
const { data } = await axiosInstance.post('/path', formData, { |
|
41 |
headers: { |
|
42 |
'Content-Type': 'multipart/form-data', |
|
43 |
}, |
|
44 |
}) |
|
45 |
}, |
|
46 |
}) |
|
47 |
|
|
48 |
// Callback when user selects the file |
|
49 |
const onFileSelected = (event: any) => { |
|
50 |
const file = event.currentTarget.files[0] |
|
51 |
if (file) { |
|
52 |
setFilename(file.name) |
|
53 |
formik.setFieldValue('file', file) |
|
54 |
} |
|
55 |
} |
|
56 |
|
|
57 |
const onClose = () => { |
|
58 |
setFilename(undefined) |
|
59 |
formik.resetForm() |
|
60 |
} |
|
61 |
|
|
62 |
const onClearSelectedFile = () => { |
|
63 |
setFilename(undefined) |
|
64 |
formik.setFieldValue('file', undefined) |
|
65 |
} |
|
66 |
|
|
67 |
return ( |
|
68 |
<ButtonOpenableDialog |
|
69 |
buttonText="Upload File" |
|
70 |
buttonColor="primary" |
|
71 |
buttonVariant="contained" |
|
72 |
onCloseCallback={onClose} |
|
73 |
maxWidth="xs" |
|
74 |
> |
|
75 |
<DialogTitle>Upload New File</DialogTitle> |
|
76 |
<DialogContent> |
|
77 |
<form onSubmit={formik.handleSubmit}> |
|
78 |
{!filename ? ( |
|
79 |
<Fragment> |
|
80 |
<Stack |
|
81 |
direction="row" |
|
82 |
justifyContent="flex-end" |
|
83 |
alignItems="center" |
|
84 |
> |
|
85 |
<Button |
|
86 |
variant="contained" |
|
87 |
color="primary" |
|
88 |
component="label" |
|
89 |
// size="small" |
|
90 |
startIcon={<AttachmentIcon />} |
|
91 |
> |
|
92 |
Select File |
|
93 |
<input |
|
94 |
id="file" |
|
95 |
name="file" |
|
96 |
type="file" |
|
97 |
hidden |
|
98 |
onChange={onFileSelected} |
|
99 |
/> |
|
100 |
</Button> |
|
101 |
</Stack> |
|
102 |
</Fragment> |
|
103 |
) : ( |
|
104 |
<Fragment> |
|
105 |
<Stack direction="row" spacing={1}> |
|
106 |
<Typography |
|
107 |
sx={{ |
|
108 |
// textOverflow: 'ellipsis', |
|
109 |
// overflow: 'hidden', |
|
110 |
}} |
|
111 |
variant="body1" |
|
112 |
> |
|
113 |
Selected File:{' '} |
|
114 |
</Typography> |
|
115 |
<Typography |
|
116 |
sx={{ |
|
117 |
textOverflow: 'ellipsis', |
|
118 |
overflow: 'hidden', |
|
119 |
}} |
|
120 |
// color="text.secondary" |
|
121 |
component={Link} |
|
122 |
// download={(formik.values?.file as File).} |
|
123 |
// align="right" |
|
124 |
> |
|
125 |
{filename} |
|
126 |
</Typography> |
|
127 |
</Stack> |
|
128 |
<Stack |
|
129 |
direction="row" |
|
130 |
justifyContent="flex-end" |
|
131 |
alignItems="center" |
|
132 |
spacing={2} |
|
133 |
sx={{mt: 2}} |
|
134 |
> |
|
135 |
<Button |
|
136 |
// sx={{ mb: 2, mt: 1 }} |
|
137 |
variant="contained" |
|
138 |
size="small" |
|
139 |
endIcon={<DeleteIcon />} |
|
140 |
onClick={onClearSelectedFile} |
|
141 |
> |
|
142 |
Remove Selection |
|
143 |
</Button> |
|
144 |
<Button size="small" type="submit" variant="contained" startIcon={<SendIcon />}> |
|
145 |
Submit |
|
146 |
</Button> |
|
147 |
</Stack> |
|
148 |
|
|
149 |
|
|
150 |
</Fragment> |
|
151 |
)} |
|
152 |
</form> |
|
153 |
</DialogContent> |
|
154 |
</ButtonOpenableDialog> |
|
155 |
) |
|
156 |
} |
|
157 |
|
|
158 |
export default FileUpload |
frontend/src/features/TrackingTool/PlaintextUpload.tsx | ||
---|---|---|
1 |
import { |
|
2 |
Button, |
|
3 |
DialogContent, |
|
4 |
DialogTitle, |
|
5 |
Stack, |
|
6 |
TextField, |
|
7 |
} from '@mui/material' |
|
8 |
import { useFormik } from 'formik' |
|
9 |
import { Fragment } from 'react' |
|
10 |
import ButtonOpenableDialog from '../Reusables/ButtonOpenableDialog' |
|
11 |
import SendIcon from '@mui/icons-material/Send' |
|
12 |
import ClearIcon from '@mui/icons-material/Clear' |
|
13 |
|
|
14 |
const PlaintextUpload = () => { |
|
15 |
const formik = useFormik({ |
|
16 |
initialValues: { |
|
17 |
text: '', |
|
18 |
}, |
|
19 |
onSubmit: () => { |
|
20 |
}, |
|
21 |
}) |
|
22 |
|
|
23 |
const resetForm = () => { |
|
24 |
formik.resetForm() |
|
25 |
} |
|
26 |
|
|
27 |
return ( |
|
28 |
<Fragment> |
|
29 |
<ButtonOpenableDialog |
|
30 |
buttonText="Plaintext" |
|
31 |
buttonColor="primary" |
|
32 |
buttonVariant="contained" |
|
33 |
> |
|
34 |
<DialogTitle>Plaintext Input</DialogTitle> |
|
35 |
<DialogContent> |
|
36 |
<form onSubmit={formik.handleChange}> |
|
37 |
<TextField |
|
38 |
sx={{ my: 2 }} |
|
39 |
fullWidth |
|
40 |
multiline |
|
41 |
label="Plaintext input" |
|
42 |
rows={10} |
|
43 |
name="text" |
|
44 |
value={formik.values.text} |
|
45 |
onChange={formik.handleChange} |
|
46 |
/> |
|
47 |
<Stack |
|
48 |
alignItems="flex-end" |
|
49 |
justifyContent="flex-end" |
|
50 |
spacing={2} |
|
51 |
direction="row" |
|
52 |
> |
|
53 |
<Button |
|
54 |
variant="contained" |
|
55 |
color="secondary" |
|
56 |
onClick={resetForm} |
|
57 |
startIcon={<ClearIcon />} |
|
58 |
> |
|
59 |
Clear |
|
60 |
</Button> |
|
61 |
<Button type="submit" variant="contained" startIcon={<SendIcon />}> |
|
62 |
Submit |
|
63 |
</Button> |
|
64 |
</Stack> |
|
65 |
</form> |
|
66 |
</DialogContent> |
|
67 |
</ButtonOpenableDialog> |
|
68 |
</Fragment> |
|
69 |
) |
|
70 |
} |
|
71 |
|
|
72 |
export default PlaintextUpload |
frontend/src/features/TrackingTool/TrackingTool.tsx | ||
---|---|---|
4 | 4 |
import { MapContainer, Marker, Polyline, Popup, TileLayer } from 'react-leaflet' |
5 | 5 |
import mapConfig from '../../config/mapConfig' |
6 | 6 |
import TextPath from 'react-leaflet-textpath' |
7 |
import PlaintextUpload from './PlaintextUpload' |
|
8 |
import FileUpload from './FileUpload' |
|
7 | 9 |
|
8 | 10 |
// Page with tracking tool |
9 | 11 |
const TrackingTool = () => { |
10 |
const generateDummyPath = () => {
|
|
12 |
const createDummyPathCoords = () => {
|
|
11 | 13 |
// Sample dummy path to display |
12 | 14 |
const dummyPath = [] |
13 | 15 |
for (let i = 0; i < 10; i += 1) { |
... | ... | |
25 | 27 |
return dummyPath |
26 | 28 |
} |
27 | 29 |
|
28 |
const createDummyPathPolylines = () => {
|
|
29 |
const dummyPath = generateDummyPath()
|
|
30 |
const createDummyPath = () => { |
|
31 |
const coords = createDummyPathCoords()
|
|
30 | 32 |
const polylines: any[] = [] |
31 | 33 |
|
32 |
if (dummyPath.length < 2) {
|
|
34 |
if (coords.length < 2) {
|
|
33 | 35 |
return [] |
34 | 36 |
} |
35 | 37 |
|
36 |
for (let i = 0; i < dummyPath.length - 1; i += 1) {
|
|
38 |
for (let i = 0; i < coords.length - 1; i += 1) {
|
|
37 | 39 |
polylines.push( |
38 | 40 |
// <Polyline |
39 | 41 |
// key={i} |
... | ... | |
51 | 53 |
// </Polyline> |
52 | 54 |
<TextPath |
53 | 55 |
positions={[ |
54 |
[dummyPath[i].latitude, dummyPath[i].longitude],
|
|
55 |
[dummyPath[i + 1].latitude, dummyPath[i + 1].longitude],
|
|
56 |
[coords[i].latitude, coords[i].longitude],
|
|
57 |
[coords[i + 1].latitude, coords[i + 1].longitude],
|
|
56 | 58 |
]} |
57 | 59 |
text="►" |
58 | 60 |
attributes={{ |
59 | 61 |
'font-size': 25, |
60 |
'fill': 'blue'
|
|
62 |
fill: 'blue',
|
|
61 | 63 |
}} |
62 | 64 |
repeat |
63 | 65 |
center |
... | ... | |
68 | 70 |
) |
69 | 71 |
} |
70 | 72 |
|
71 |
return polylines
|
|
73 |
return [polylines, coords]
|
|
72 | 74 |
} |
73 | 75 |
|
74 |
const polylines = createDummyPathPolylines()
|
|
76 |
const [polylines, coords] = createDummyPath()
|
|
75 | 77 |
|
76 | 78 |
return ( |
77 | 79 |
<Fragment> |
... | ... | |
94 | 96 |
> |
95 | 97 |
Upload: |
96 | 98 |
</Typography> |
97 |
<Button |
|
98 |
variant="contained" |
|
99 |
color="primary" |
|
100 |
startIcon={<AddIcon />} |
|
101 |
> |
|
102 |
Plaintext |
|
103 |
</Button> |
|
104 |
<Button |
|
105 |
variant="contained" |
|
106 |
color="primary" |
|
107 |
startIcon={<AddIcon />} |
|
108 |
> |
|
109 |
File |
|
110 |
</Button> |
|
99 |
<PlaintextUpload /> |
|
100 |
<FileUpload /> |
|
111 | 101 |
</Stack> |
112 | 102 |
</Grid> |
113 | 103 |
<Grid |
... | ... | |
132 | 122 |
attribution={mapConfig.attribution} |
133 | 123 |
url={mapConfig.url} |
134 | 124 |
/> |
125 |
{coords.map(({ latitude, longitude }, idx) => ( |
|
126 |
<Marker position={[latitude, longitude]} /> |
|
127 |
))} |
|
135 | 128 |
{polylines} |
136 | 129 |
</MapContainer> |
137 | 130 |
</Grid> |
frontend/src/features/redux/store.ts | ||
---|---|---|
2 | 2 |
import { persistStore } from 'redux-persist' |
3 | 3 |
import thunk from 'redux-thunk' |
4 | 4 |
import userReducer from '../Auth/userSlice' |
5 |
import themeReducer from '../Theme/themeReducer'
|
|
5 |
import themeReducer from '../Theme/themeSlice'
|
|
6 | 6 |
import catalogReducer from '../Catalog/catalogSlice' |
7 | 7 |
import { composeWithDevTools } from 'redux-devtools-extension' |
8 | 8 |
|
... | ... | |
16 | 16 |
catalog: catalogReducer, |
17 | 17 |
}), |
18 | 18 |
process.env.REACT_APP_DEV_ENV === 'true' |
19 |
? composeEnhancers( |
|
19 |
? composeEnhancers( // ComposeEnhancers will inject redux-devtools-extension
|
|
20 | 20 |
applyMiddleware(thunk) // Thunk middleware so we can async fetch data from the api |
21 | 21 |
) |
22 | 22 |
: applyMiddleware(thunk) |
Také k dispozici: Unified diff
map dialogs + catalog filter fix
re #9547 #9545