Revize 4f42fa52
Přidáno uživatelem Václav Honzík před téměř 3 roky(ů)
frontend/src/App.tsx | ||
---|---|---|
13 | 13 |
import TrackingTool from './features/TrackingTool/TrackingTool' |
14 | 14 |
import Logout from './features/Auth/Logout' |
15 | 15 |
import ThemeWrapper from './features/Theme/ThemeWrapper' |
16 |
import Notification from './features/Notification/Notification' |
|
17 |
import { Fragment } from 'react' |
|
16 | 18 | |
17 | 19 |
const App = () => { |
18 | ||
19 | 20 |
return ( |
20 | 21 |
<ThemeWrapper> |
21 |
<Navigation> |
|
22 |
<Box sx={{mx: 10}}> |
|
23 |
<Routes> |
|
24 |
<Route path="/" element={<Home />} /> |
|
25 |
<Route path="/catalog" element={<Catalog />} /> |
|
26 |
<Route |
|
27 |
path="/catalog/:itemId" |
|
28 |
element={<CatalogItemDetail />} |
|
29 |
/> |
|
30 |
<Route path="/login" element={<Login />} /> |
|
31 |
<Route path="/logout" element={<Logout />} /> |
|
32 |
<Route path="/map" element={<TrackingTool />} /> |
|
33 |
<Route path="*" element={<NotFound />} /> |
|
34 |
</Routes> |
|
35 |
</Box> |
|
36 |
</Navigation> |
|
22 |
<Fragment> |
|
23 |
<Notification /> |
|
24 |
<Navigation> |
|
25 |
<Box sx={{ mx: 10 }}> |
|
26 |
<Routes> |
|
27 |
<Route path="/" element={<Home />} /> |
|
28 |
<Route path="/catalog" element={<Catalog />} /> |
|
29 |
<Route |
|
30 |
path="/catalog/:itemId" |
|
31 |
element={<CatalogItemDetail />} |
|
32 |
/> |
|
33 |
<Route path="/login" element={<Login />} /> |
|
34 |
<Route path="/logout" element={<Logout />} /> |
|
35 |
<Route path="/map" element={<TrackingTool />} /> |
|
36 |
<Route path="*" element={<NotFound />} /> |
|
37 |
</Routes> |
|
38 |
</Box> |
|
39 |
</Navigation> |
|
40 |
</Fragment> |
|
37 | 41 |
</ThemeWrapper> |
38 | 42 |
) |
39 | 43 |
} |
frontend/src/features/Auth/LoginDialog.tsx | ||
---|---|---|
1 |
import { Fragment, FunctionComponent, useState } from 'react' |
|
2 |
import Dialog, { DialogProps } from '@mui/material/Dialog' |
|
3 |
import { |
|
4 |
Button, |
|
5 |
DialogContent, |
|
6 |
Link, |
|
7 |
Stack, |
|
8 |
TextField, |
|
9 |
Typography, |
|
10 |
} from '@mui/material' |
|
11 |
import { useFormik } from 'formik' |
|
12 |
import * as yup from 'yup' |
|
13 |
import { useDispatch } from 'react-redux' |
|
14 |
import { showNotification } from '../Notification/notificationSlice' |
|
15 |
import axiosInstance from '../../api/api' |
|
16 |
import { Link as RouterLink } from 'react-router-dom' |
|
17 | ||
18 |
export interface CreateIndexDialogProps { |
|
19 |
maxWidth?: DialogProps['maxWidth'] |
|
20 |
} |
|
21 | ||
22 |
const RegisterDialog: FunctionComponent<CreateIndexDialogProps> = ({ |
|
23 |
maxWidth, |
|
24 |
}) => { |
|
25 |
const [open, setOpen] = useState(false) |
|
26 |
const [submitButtonEnabled, setSubmitButtonEnabled] = useState(true) |
|
27 | ||
28 |
const dispatch = useDispatch() |
|
29 | ||
30 |
const hideDialog = () => { |
|
31 |
setOpen(false) |
|
32 |
} |
|
33 | ||
34 |
const showDialog = () => { |
|
35 |
setOpen(true) |
|
36 |
} |
|
37 | ||
38 |
const validationSchema = yup.object().shape({ |
|
39 |
email: yup.string().email().required('Email is required'), |
|
40 |
password: yup.string().required('Password is required'), |
|
41 |
}) |
|
42 | ||
43 |
const formik = useFormik({ |
|
44 |
initialValues: { |
|
45 |
name: '', |
|
46 |
password: '', |
|
47 |
}, |
|
48 |
validationSchema, |
|
49 |
onSubmit: async (values) => { |
|
50 |
setSubmitButtonEnabled(false) |
|
51 |
let userRegistered = false |
|
52 |
try { |
|
53 |
const { status } = await axiosInstance.post( |
|
54 |
`/users/${values.name}`, |
|
55 |
values |
|
56 |
) |
|
57 | ||
58 |
switch (status) { |
|
59 |
case 200: |
|
60 |
dispatch({ |
|
61 |
message: 'User was created successfully', |
|
62 |
severity: 'success', |
|
63 |
}) |
|
64 |
userRegistered = true |
|
65 |
break |
|
66 |
case 204: |
|
67 |
dispatch( |
|
68 |
showNotification({ |
|
69 |
message: 'User already exists', |
|
70 |
severity: 'error', |
|
71 |
}) |
|
72 |
) |
|
73 |
break |
|
74 |
default: |
|
75 |
dispatch({ |
|
76 |
message: |
|
77 |
'Unknown error ocurred, the user was not registered. Please try again later', |
|
78 |
severity: 'error', |
|
79 |
}) |
|
80 |
} |
|
81 |
} catch (err: any) { |
|
82 |
dispatch( |
|
83 |
showNotification({ |
|
84 |
message: 'The user could not be registered 😥', |
|
85 |
severity: 'error', |
|
86 |
}) |
|
87 |
) |
|
88 |
} |
|
89 | ||
90 |
if (userRegistered) { |
|
91 |
onClose() |
|
92 |
} |
|
93 | ||
94 |
// Always fetch new indices |
|
95 |
// TODO actually fetch the users |
|
96 |
// dispatch(fetchUsers()) |
|
97 |
setSubmitButtonEnabled(true) |
|
98 |
}, |
|
99 |
}) |
|
100 | ||
101 |
// Method called on closing the dialog |
|
102 |
const onClose = () => { |
|
103 |
hideDialog() |
|
104 |
formik.resetForm() |
|
105 |
} |
|
106 | ||
107 |
return ( |
|
108 |
<Fragment> |
|
109 |
<Stack |
|
110 |
direction="row" |
|
111 |
justifyContent="flex-end" |
|
112 |
alignItems="center" |
|
113 |
> |
|
114 |
<Button variant="outlined" color="primary" onClick={showDialog}> |
|
115 |
Create new Index |
|
116 |
</Button> |
|
117 |
</Stack> |
|
118 | ||
119 |
<Dialog |
|
120 |
open={open} |
|
121 |
fullWidth={true} |
|
122 |
onClose={onClose} |
|
123 |
maxWidth={maxWidth || 'lg'} |
|
124 |
> |
|
125 |
<Typography sx={{ ml: 2, mt: 2 }} variant="h5" fontWeight="600"> |
|
126 |
Login |
|
127 |
</Typography> |
|
128 |
<DialogContent> |
|
129 |
<form onSubmit={formik.handleSubmit}> |
|
130 |
<TextField |
|
131 |
fullWidth |
|
132 |
label="Name" |
|
133 |
name="name" |
|
134 |
sx={{ mb: 2 }} |
|
135 |
value={formik.values.name} |
|
136 |
onChange={formik.handleChange} |
|
137 |
error={ |
|
138 |
Boolean(formik.errors.name) && |
|
139 |
formik.touched.name |
|
140 |
} |
|
141 |
helperText={ |
|
142 |
formik.errors.name && |
|
143 |
formik.touched.name && |
|
144 |
formik.errors.name |
|
145 |
} |
|
146 |
/> |
|
147 |
<TextField |
|
148 |
fullWidth |
|
149 |
label="Password" |
|
150 |
name="password" |
|
151 |
type="password" |
|
152 |
sx={{ mb: 2 }} |
|
153 |
value={formik.values.password} |
|
154 |
onChange={formik.handleChange} |
|
155 |
error={ |
|
156 |
Boolean(formik.errors.password) && |
|
157 |
formik.touched.password |
|
158 |
} |
|
159 |
helperText={ |
|
160 |
formik.errors.password && |
|
161 |
formik.touched.password && |
|
162 |
formik.errors.password |
|
163 |
} |
|
164 |
/> |
|
165 |
<Fragment> |
|
166 |
<Button |
|
167 |
type="submit" |
|
168 |
variant="contained" |
|
169 |
disabled={!submitButtonEnabled} |
|
170 |
fullWidth |
|
171 |
> |
|
172 |
Log in |
|
173 |
</Button> |
|
174 |
</Fragment> |
|
175 |
</form> |
|
176 | ||
177 |
<Link component={RouterLink} to="/resetPassword">Forgot password?</Link> |
|
178 |
</DialogContent> |
|
179 |
</Dialog> |
|
180 |
</Fragment> |
|
181 |
) |
|
182 |
} |
|
183 | ||
184 |
export default RegisterDialog |
frontend/src/features/Notification/Notification.tsx | ||
---|---|---|
1 |
import { Alert, AlertColor, Snackbar } from '@mui/material' |
|
2 |
import { Fragment, useEffect, useState } from 'react' |
|
3 |
import { useDispatch, useSelector } from 'react-redux' |
|
4 |
import { RootState } from '../redux/store' |
|
5 |
import { consumeNotification } from './notificationSlice' |
|
6 | ||
7 |
// Represents notification component that will be displayed on the screen |
|
8 |
const Notification = () => { |
|
9 |
const dispatch = useDispatch() |
|
10 |
const notification = useSelector((state: RootState) => state.notification) |
|
11 | ||
12 |
const [displayMessage, setDisplayMessage] = useState('') |
|
13 |
const [open, setOpen] = useState(false) |
|
14 |
const [severity, setSeverity] = useState<AlertColor>('info') |
|
15 |
const [autohideDuration, setAutohideDuration] = useState<number | null>( |
|
16 |
null |
|
17 |
) |
|
18 | ||
19 |
const closeNotification = () => { |
|
20 |
setOpen(false) |
|
21 |
setAutohideDuration(null) |
|
22 |
} |
|
23 | ||
24 |
// Set the message to be displayed if something is set |
|
25 |
useEffect(() => { |
|
26 |
if (notification.message) { |
|
27 |
setDisplayMessage(notification.message) |
|
28 |
setSeverity(notification.severity as AlertColor) |
|
29 |
if (notification.autohideSecs) { |
|
30 |
setAutohideDuration(notification.autohideSecs * 1000) |
|
31 |
} |
|
32 |
// Consume the message from store |
|
33 |
dispatch(consumeNotification()) |
|
34 | ||
35 |
// Show the message in the notification |
|
36 |
setOpen(true) |
|
37 |
} |
|
38 |
}, [notification, dispatch]) |
|
39 | ||
40 |
return ( |
|
41 |
<Fragment> |
|
42 |
<Snackbar |
|
43 |
open={open} |
|
44 |
autoHideDuration={autohideDuration} |
|
45 |
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} |
|
46 |
> |
|
47 |
<Alert severity={severity} onClose={closeNotification}> |
|
48 |
{displayMessage} |
|
49 |
</Alert> |
|
50 |
</Snackbar> |
|
51 |
</Fragment> |
|
52 |
) |
|
53 |
} |
|
54 | ||
55 |
export default Notification |
frontend/src/features/Notification/notificationSlice.ts | ||
---|---|---|
1 |
import { AlertColor } from '@mui/material' |
|
2 |
import { createSlice } from '@reduxjs/toolkit' |
|
3 | ||
4 |
export interface NotificationState { |
|
5 |
message?: string |
|
6 |
severity: AlertColor |
|
7 |
autohideSecs?: number |
|
8 |
} |
|
9 | ||
10 |
const initialState = { |
|
11 |
message: undefined, |
|
12 |
severity: 'info', |
|
13 |
autohideSecs: undefined |
|
14 |
} |
|
15 | ||
16 |
const notificationSlice = createSlice({ |
|
17 |
name: 'notification', |
|
18 |
initialState, |
|
19 |
reducers: { |
|
20 |
showNotification: (state, action) => ({ |
|
21 |
...state, |
|
22 |
message: action.payload.message, |
|
23 |
severity: action.payload.severity, |
|
24 |
autohideSecs: action.payload.autohideSecs, |
|
25 |
}), |
|
26 |
// consumes the message so it is not displayed after the page gets refreshed |
|
27 |
consumeNotification: (state) => ({ |
|
28 |
...initialState, |
|
29 |
}), |
|
30 |
}, |
|
31 |
}) |
|
32 | ||
33 |
const notificationReducer = notificationSlice.reducer |
|
34 |
export const { showNotification, consumeNotification } = |
|
35 |
notificationSlice.actions |
|
36 |
export default notificationReducer |
frontend/src/features/TrackingTool/TrackingTool.tsx | ||
---|---|---|
6 | 6 |
import TextPath from 'react-leaflet-textpath' |
7 | 7 |
import PlaintextUpload from './PlaintextUpload' |
8 | 8 |
import FileUpload from './FileUpload' |
9 |
import L from 'leaflet' |
|
10 |
import DeleteIcon from '@mui/icons-material/Delete' |
|
9 | 11 | |
10 | 12 |
// Page with tracking tool |
11 | 13 |
const TrackingTool = () => { |
... | ... | |
64 | 66 |
repeat |
65 | 67 |
center |
66 | 68 |
weight={10} |
67 |
> |
|
68 |
<Popup>Caesar 🥗 War Path (Allegedly)</Popup> |
|
69 |
</TextPath> |
|
69 |
></TextPath> |
|
70 | 70 |
) |
71 | 71 |
} |
72 | 72 | |
... | ... | |
123 | 123 |
url={mapConfig.url} |
124 | 124 |
/> |
125 | 125 |
{coords.map(({ latitude, longitude }, idx) => ( |
126 |
<Marker position={[latitude, longitude]} /> |
|
126 |
<Marker |
|
127 |
position={[latitude, longitude]} |
|
128 |
/> |
|
127 | 129 |
))} |
128 | 130 |
{polylines} |
129 | 131 |
</MapContainer> |
130 | 132 |
</Grid> |
131 |
|
|
132 | 133 |
</Grid> |
133 | 134 |
</Fragment> |
134 | 135 |
) |
frontend/src/features/redux/store.ts | ||
---|---|---|
5 | 5 |
import themeReducer from '../Theme/themeSlice' |
6 | 6 |
import catalogReducer from '../Catalog/catalogSlice' |
7 | 7 |
import { composeWithDevTools } from 'redux-devtools-extension' |
8 |
import notificationReducer from '../Notification/notificationSlice' |
|
8 | 9 | |
9 | 10 |
const composeEnhancers = composeWithDevTools({}) |
10 | 11 | |
... | ... | |
14 | 15 |
user: userReducer, |
15 | 16 |
theme: themeReducer, |
16 | 17 |
catalog: catalogReducer, |
18 |
notification: notificationReducer |
|
17 | 19 |
}), |
18 | 20 |
process.env.REACT_APP_DEV_ENV === 'true' |
19 | 21 |
? composeEnhancers( // ComposeEnhancers will inject redux-devtools-extension |
Také k dispozici: Unified diff
Login dialog + slice for notifications
re #9628