Revize 8754af5c
Přidáno uživatelem Václav Honzík před téměř 3 roky(ů)
frontend/package.json | ||
---|---|---|
14 | 14 |
"dotenv": "^16.0.0", |
15 | 15 |
"formik": "^2.2.9", |
16 | 16 |
"jwt-decode": "^3.1.2", |
17 |
"leaflet": "^1.8.0", |
|
17 | 18 |
"react": "^17.0.2", |
18 | 19 |
"react-dom": "^17.0.2", |
20 |
"react-leaflet": "3.2.5", |
|
19 | 21 |
"react-redux": "^7.2.6", |
20 | 22 |
"react-router-dom": "^6.2.2", |
21 | 23 |
"react-scripts": "5.0.0", |
... | ... | |
58 | 60 |
"@testing-library/react": "^12.0.0", |
59 | 61 |
"@testing-library/user-event": "^13.2.1", |
60 | 62 |
"@types/jest": "^27.0.1", |
63 |
"@types/leaflet": "^1.7.9", |
|
61 | 64 |
"@types/node": "^16.7.13", |
62 | 65 |
"@types/react": "^17.0.43", |
63 | 66 |
"@types/react-dom": "^17.0.9", |
frontend/src/config/conf.ts | ||
---|---|---|
1 |
// Configuration object for the application |
|
1 | 2 |
const conf = { |
2 | 3 |
baseUrl: |
3 | 4 |
process.env.REACT_APP_DEV_ENV === 'true' |
frontend/src/config/mapConfig.ts | ||
---|---|---|
1 |
|
|
2 |
// Map configuration interface |
|
3 |
export interface MapConfig { |
|
4 |
attribution: string, |
|
5 |
url: string |
|
6 |
defaultCoordinates: number[] // pair of numbers |
|
7 |
defaultZoom: number |
|
8 |
} |
|
9 |
|
|
10 |
const mapConfig: MapConfig = { |
|
11 |
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', |
|
12 |
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', |
|
13 |
defaultCoordinates: [33.5138, 36.2765], // Damascus, Syria |
|
14 |
defaultZoom: 8 |
|
15 |
} |
|
16 |
|
|
17 |
export default mapConfig |
frontend/src/features/Catalog/Catalog.tsx | ||
---|---|---|
1 |
import { |
|
2 |
Container, |
|
3 |
Paper, |
|
4 |
Typography, |
|
5 |
} from '@mui/material' |
|
1 |
import { Container, Paper, Typography } from '@mui/material' |
|
6 | 2 |
import CatalogTable from './CatalogTable' |
7 | 3 |
import { Fragment } from 'react' |
8 | 4 |
import CatalogFilter from './CatalogFilter' |
9 | 5 |
|
10 |
const Catalog = () => { |
|
11 |
|
|
12 |
|
|
13 |
return ( |
|
14 |
<Fragment> |
|
15 |
<Paper |
|
16 |
sx={{ py: 2, mt: 2 }} |
|
17 |
variant="outlined" |
|
18 |
style={{ minHeight: '50vh' }} |
|
19 |
> |
|
20 |
<Container sx={{ mt: 4 }}> |
|
21 |
<Typography variant="h3" sx={{mb: 2}} fontWeight="bold" >Catalog</Typography> |
|
22 |
<CatalogFilter /> |
|
23 |
<CatalogTable /> |
|
24 |
</Container> |
|
25 |
</Paper> |
|
26 |
</Fragment> |
|
27 |
) |
|
28 |
} |
|
6 |
// Catalog page |
|
7 |
const Catalog = () => ( |
|
8 |
<Fragment> |
|
9 |
<Typography variant="h3" sx={{ mb: 2 }} fontWeight="bold"> |
|
10 |
Catalog |
|
11 |
</Typography> |
|
12 |
<Paper |
|
13 |
sx={{ py: 2, mt: 2 }} |
|
14 |
variant="outlined" |
|
15 |
style={{ minHeight: '50vh' }} |
|
16 |
> |
|
17 |
<Container sx={{ mt: 4 }}> |
|
18 |
<CatalogFilter /> |
|
19 |
<CatalogTable /> |
|
20 |
</Container> |
|
21 |
</Paper> |
|
22 |
</Fragment> |
|
23 |
) |
|
29 | 24 |
|
30 | 25 |
export default Catalog |
frontend/src/features/Catalog/CatalogFilter.tsx | ||
---|---|---|
1 |
import { Button, Collapse, Grid, Stack, TextField } from '@mui/material' |
|
2 |
import { Fragment, useState } from 'react' |
|
3 |
import { useDispatch } from 'react-redux' |
|
4 |
import { setFilter, CatalogFilter as Filter } from './catalogSlice' |
|
1 |
import { |
|
2 |
Button, |
|
3 |
Collapse, |
|
4 |
Divider, |
|
5 |
Grid, |
|
6 |
Stack, |
|
7 |
TextField, |
|
8 |
} from '@mui/material' |
|
9 |
import { Fragment } from 'react' |
|
10 |
import { useDispatch, useSelector } from 'react-redux' |
|
11 |
import { setFilter, setFilterOpen } from './catalogSlice' |
|
5 | 12 |
import { fetchItems } from './catalogThunks' |
13 |
import FilterListIcon from '@mui/icons-material/FilterList' |
|
14 |
import FilterListOffIcon from '@mui/icons-material/FilterListOff' |
|
15 |
import ManageSearchIcon from '@mui/icons-material/ManageSearch' |
|
16 |
import { RootState } from '../redux/store' |
|
6 | 17 |
|
7 | 18 |
const CatalogFilter = () => { |
8 | 19 |
const dispatch = useDispatch() |
9 | 20 |
|
10 |
const [filterOpen, setFilterOpen] = useState(false)
|
|
21 |
const filterOpen = useSelector((state: RootState) => state.catalog.filterOpen)
|
|
11 | 22 |
const toggleFilter = () => { |
12 |
setFilterOpen(!filterOpen)
|
|
23 |
dispatch(setFilterOpen(!filterOpen))
|
|
13 | 24 |
} |
14 | 25 |
|
15 | 26 |
// current filter object |
16 |
const filter: Filter = {}
|
|
27 |
const filter = useSelector((state: RootState) => state.catalog.filter)
|
|
17 | 28 |
const applyFilter = () => { |
18 | 29 |
dispatch(fetchItems()) |
19 | 30 |
} |
20 | 31 |
|
21 |
|
|
22 | 32 |
return ( |
23 | 33 |
<Fragment> |
24 |
<Button variant="outlined" color="primary" onClick={toggleFilter}> |
|
34 |
<Button |
|
35 |
startIcon={ |
|
36 |
filterOpen ? <FilterListOffIcon /> : <FilterListIcon /> |
|
37 |
} |
|
38 |
// variant="outlined" |
|
39 |
color="primary" |
|
40 |
onClick={toggleFilter} |
|
41 |
> |
|
25 | 42 |
Filter |
26 | 43 |
</Button> |
27 | 44 |
<Collapse in={filterOpen} timeout="auto" unmountOnExit> |
... | ... | |
33 | 50 |
size="small" |
34 | 51 |
id="name" |
35 | 52 |
label="Name" |
36 |
onChange={(e: any) => {
|
|
37 |
filter.name = e.target.value
|
|
53 |
onChange={(e: any) => { |
|
54 |
filter.name = e.target.value |
|
38 | 55 |
dispatch(setFilter(filter)) |
39 | 56 |
}} |
57 |
value={filter.name} |
|
40 | 58 |
/> |
41 | 59 |
<TextField |
42 | 60 |
size="small" |
... | ... | |
45 | 63 |
onChange={(e: any) => { |
46 | 64 |
filter.type = e.target.value |
47 | 65 |
dispatch(setFilter(filter)) |
48 |
}} |
|
66 |
}} |
|
67 |
value={filter.type} |
|
49 | 68 |
/> |
50 | 69 |
</Stack> |
51 | 70 |
<Stack direction="row" spacing={2}> |
... | ... | |
58 | 77 |
size="small" |
59 | 78 |
id="stateOrTerritory" |
60 | 79 |
label="State or territory" |
61 |
onChange={(e: any) => {
|
|
62 |
filter.country = e.target.value
|
|
80 |
onChange={(e: any) => { |
|
81 |
filter.country = e.target.value |
|
63 | 82 |
dispatch(setFilter(filter)) |
64 | 83 |
}} |
84 |
value={filter.country} |
|
65 | 85 |
/> |
66 | 86 |
<TextField |
67 | 87 |
size="small" |
... | ... | |
77 | 97 |
justifyContent="flex-start" |
78 | 98 |
alignItems="flex-end" |
79 | 99 |
> |
80 |
<Button variant="outlined" onClick={applyFilter}>Search</Button> |
|
100 |
<Button startIcon={<ManageSearchIcon/>} variant="contained" onClick={applyFilter}> |
|
101 |
Search |
|
102 |
</Button> |
|
81 | 103 |
</Stack> |
82 | 104 |
</Grid> |
83 | 105 |
</Grid> |
84 | 106 |
</Collapse> |
107 |
{filterOpen ? <Divider sx={{ mb: 5, mt: 1 }} /> : null} |
|
85 | 108 |
</Fragment> |
86 | 109 |
) |
87 | 110 |
} |
frontend/src/features/Catalog/CatalogItemDetail.tsx | ||
---|---|---|
1 | 1 |
import { |
2 |
Box,
|
|
2 |
Button,
|
|
3 | 3 |
Divider, |
4 | 4 |
Grid, |
5 | 5 |
Paper, |
6 |
Skeleton, |
|
7 | 6 |
Typography, |
8 | 7 |
} from '@mui/material' |
9 | 8 |
import { Fragment, useEffect, useState } from 'react' |
... | ... | |
12 | 11 |
import { CatalogItemDto } from '../../swagger/data-contracts' |
13 | 12 |
import ShowErrorIfPresent from '../Reusables/ShowErrorIfPresent' |
14 | 13 |
import ContentLoading from '../Reusables/ContentLoading' |
14 |
import CatalogItemMap from './CatalogItemMap' |
|
15 |
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos' |
|
16 |
import { Link as RouterLink } from 'react-router-dom' |
|
15 | 17 |
|
16 | 18 |
const apiError = |
17 | 19 |
'Error while fetching data from the server, please try again later.' |
... | ... | |
45 | 47 |
} |
46 | 48 |
|
47 | 49 |
fetchItem() |
48 |
}, []) |
|
50 |
}, [itemId])
|
|
49 | 51 |
|
50 | 52 |
// Maps catalogItem property to corresponding table row |
51 | 53 |
const mapToRow = (rowName: string, items: string[]) => ( |
... | ... | |
56 | 58 |
</Grid> |
57 | 59 |
<Grid item xs={4} sx={{ ml: 'auto' }}> |
58 | 60 |
{items.map((item) => ( |
59 |
<Typography>{item}</Typography> |
|
61 |
<Typography key={item}>{item}</Typography>
|
|
60 | 62 |
))} |
61 | 63 |
</Grid> |
62 | 64 |
</Grid> |
... | ... | |
101 | 103 |
|
102 | 104 |
return ( |
103 | 105 |
// TODO remove min height |
104 |
<Paper style={{ minHeight: '100vh', borderRadius: 0 }} elevation={2}> |
|
106 |
<Fragment> |
|
107 |
<Button |
|
108 |
startIcon={<ArrowBackIosIcon />} |
|
109 |
variant="contained" |
|
110 |
component={RouterLink} |
|
111 |
to="/catalog" |
|
112 |
color="primary" |
|
113 |
sx={{ mb: 2 }} |
|
114 |
> |
|
115 |
Return To Catalog |
|
116 |
</Button> |
|
105 | 117 |
<ShowErrorIfPresent err={err} /> |
106 | 118 |
|
107 |
{isItemLoading && !err ? <ContentLoading /> : null} |
|
108 |
{!isItemLoading && item ? ( |
|
109 |
<Grid container justifyContent="space-around"> |
|
110 |
<Grid item xs={6} sx={{ px: 2 }}> |
|
111 |
{rows.map((row, idx) => { |
|
112 |
const maxIdx = rows.length - 1 |
|
113 |
return ( |
|
114 |
<Fragment> |
|
115 |
{mapToRow( |
|
116 |
row.rowName as string, |
|
117 |
row.items as string[] |
|
118 |
)} |
|
119 |
{idx === maxIdx ? null : <Divider />} |
|
120 |
</Fragment> |
|
121 |
) |
|
122 |
})} |
|
123 |
</Grid> |
|
119 |
<Paper style={{ minHeight: '100vh' }} variant="outlined"> |
|
124 | 120 |
|
125 |
<Grid item xs={6}> |
|
126 |
<Box sx={{ px: 2, py: 4 }}> |
|
127 |
<Typography |
|
128 |
variant="h4" |
|
129 |
sx={{ mb: 4 }} |
|
130 |
fontWeight="bold" |
|
131 |
> |
|
132 |
Map |
|
133 |
</Typography> |
|
121 |
{isItemLoading && !err ? <ContentLoading /> : null} |
|
122 |
{!isItemLoading && item ? ( |
|
123 |
<Grid container justifyContent="space-around"> |
|
124 |
<Grid item xs={12} md={6} sx={{ px: 2 }}> |
|
125 |
{rows.map((row, idx) => { |
|
126 |
const maxIdx = rows.length - 1 |
|
127 |
return ( |
|
128 |
<Fragment> |
|
129 |
{mapToRow( |
|
130 |
row.rowName as string, |
|
131 |
row.items as string[] |
|
132 |
)} |
|
133 |
{idx === maxIdx ? null : <Divider />} |
|
134 |
</Fragment> |
|
135 |
) |
|
136 |
})} |
|
137 |
</Grid> |
|
134 | 138 |
|
135 |
<Skeleton |
|
136 |
animation="pulse" |
|
137 |
variant="rectangular" |
|
138 |
width="100%" |
|
139 |
height={400} |
|
140 |
/> |
|
141 |
</Box> |
|
139 |
<Grid item md={6} xs={12}> |
|
140 |
<CatalogItemMap item={item} /> |
|
141 |
</Grid> |
|
142 | 142 |
</Grid> |
143 |
</Grid>
|
|
144 |
) : null}
|
|
145 |
</Paper>
|
|
143 |
) : null}
|
|
144 |
</Paper>
|
|
145 |
</Fragment>
|
|
146 | 146 |
) |
147 | 147 |
} |
148 | 148 |
|
frontend/src/features/Catalog/CatalogItemMap.tsx | ||
---|---|---|
1 |
import { Box, Typography } from '@mui/material' |
|
2 |
import { Fragment, FunctionComponent } from 'react' |
|
3 |
import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet' |
|
4 |
import mapConfig from '../../config/mapConfig' |
|
5 |
import { CatalogItemDto } from '../../swagger/data-contracts' |
|
6 |
|
|
7 |
const CatalogItemMap: FunctionComponent<{ item: CatalogItemDto }> = ({ |
|
8 |
item, |
|
9 |
}) => { |
|
10 |
const [lat, long] = [ |
|
11 |
item.longitude ?? mapConfig.defaultCoordinates[0], |
|
12 |
item.latitude ?? mapConfig.defaultCoordinates[1], |
|
13 |
] |
|
14 |
|
|
15 |
return ( |
|
16 |
<Fragment> |
|
17 |
<Box sx={{ px: 2, py: 4 }} style={{ height: '100%', minHeight: '450px', maxHeight: '70vh'}}> |
|
18 |
<MapContainer center={[long, lat]} zoom={7} style={{ height: '100%', minHeight: '100%'}}> |
|
19 |
<TileLayer attribution={mapConfig.attribution} url={mapConfig.url} /> |
|
20 |
{!item.longitude || !item.latitude ? null : ( |
|
21 |
<Marker position={[long, lat]}> |
|
22 |
<Popup>{item.name}</Popup> |
|
23 |
</Marker> |
|
24 |
)} |
|
25 |
</MapContainer> |
|
26 |
{!item.longitude || !item.latitude ? ( |
|
27 |
<Typography color="error" align="center" fontWeight="bold">Location Unavailable</Typography> |
|
28 |
) : null} |
|
29 |
</Box> |
|
30 |
</Fragment> |
|
31 |
) |
|
32 |
} |
|
33 |
|
|
34 |
export default CatalogItemMap |
frontend/src/features/Catalog/CatalogTable.tsx | ||
---|---|---|
7 | 7 |
TableHead, |
8 | 8 |
TablePagination, |
9 | 9 |
TableRow, |
10 |
Typography, |
|
10 | 11 |
} from '@mui/material' |
11 | 12 |
import { Fragment, useEffect, useState } from 'react' |
12 | 13 |
import { Link as RouterLink } from 'react-router-dom' |
... | ... | |
15 | 16 |
import ContentLoading from '../Reusables/ContentLoading' |
16 | 17 |
import { RootState } from '../redux/store' |
17 | 18 |
import { useDispatch, useSelector } from 'react-redux' |
18 |
import { consumeError, setLoading } from './catalogSlice' |
|
19 |
import { |
|
20 |
consumeError, |
|
21 |
setLoading, |
|
22 |
setRowsPerPage, |
|
23 |
ShowAllItemsOption, |
|
24 |
} from './catalogSlice' |
|
19 | 25 |
import { fetchItems } from './catalogThunks' |
20 | 26 |
|
21 | 27 |
// Catalog table component |
22 | 28 |
const CatalogTable = () => { |
23 | 29 |
const [page, setPage] = useState(0) // currently shown page |
24 |
const rowsPerPage = [5, 10, 15, 20] // number of rows per page |
|
25 | 30 |
|
26 |
// Selected rows per page |
|
27 |
const [selectedRowsPerPage, setSelectedRowsPerPage] = useState( |
|
28 |
rowsPerPage[0] |
|
31 |
const dispatch = useDispatch() |
|
32 |
|
|
33 |
// Rows per page |
|
34 |
const rowsPerPageOptions = useSelector( |
|
35 |
(state: RootState) => state.catalog.rowsPerPageOptions |
|
36 |
) |
|
37 |
const rowsPerPage = useSelector( |
|
38 |
(state: RootState) => state.catalog.rowsPerPage |
|
29 | 39 |
) |
30 | 40 |
|
31 |
// Subscribe to the store
|
|
41 |
// Items, loading and error from api
|
|
32 | 42 |
const items = useSelector((state: RootState) => state.catalog.items) |
33 | 43 |
const loading = useSelector((state: RootState) => state.catalog.loading) |
34 | 44 |
const apiError = useSelector((state: RootState) => state.catalog.error) |
35 | 45 |
|
36 |
const [displayError, setDisplayError] = useState<string | undefined>(undefined) |
|
46 |
// Local state to display any error relevant error |
|
47 |
const [displayError, setDisplayError] = useState<string | undefined>( |
|
48 |
undefined |
|
49 |
) |
|
50 |
const [unload, setUnload] = useState(true) |
|
37 | 51 |
|
38 | 52 |
// When changing rows per page set the selected number and reset to the first page |
39 | 53 |
const onRowsPerPageChange = ( |
40 | 54 |
event: React.ChangeEvent<HTMLInputElement> |
41 | 55 |
) => { |
42 |
setSelectedRowsPerPage(Number(event.target.value))
|
|
56 |
dispatch(setRowsPerPage(Number(event.target.value)))
|
|
43 | 57 |
setPage(0) |
44 | 58 |
} |
45 | 59 |
|
46 |
const dispatch = useDispatch() |
|
47 |
|
|
48 | 60 |
useEffect(() => { |
49 | 61 |
// Fetch items when the component is mounted |
50 | 62 |
// This will automatically search whenever the filter changes |
... | ... | |
64 | 76 |
} |
65 | 77 |
}, [apiError, dispatch]) |
66 | 78 |
|
67 |
|
|
68 | 79 |
// Name of columns in the header |
69 | 80 |
const columns = [ |
70 | 81 |
'Name', |
... | ... | |
76 | 87 |
'Certainty', |
77 | 88 |
] |
78 | 89 |
|
79 |
const mapValueOrDefault = (value?: string) => ( |
|
80 |
<TableCell align="center">{value || 'N/A'}</TableCell> |
|
90 |
const mapValueOrDefault = (value?: string, textStyle?: any) => ( |
|
91 |
<TableCell align="center"> |
|
92 |
<Typography |
|
93 |
sx={{ |
|
94 |
...textStyle, |
|
95 |
}} |
|
96 |
> |
|
97 |
{value || 'N/A'} |
|
98 |
</Typography> |
|
99 |
</TableCell> |
|
81 | 100 |
) |
82 | 101 |
|
83 | 102 |
// Maps catalogItem to corresponding table row |
... | ... | |
88 | 107 |
<Link |
89 | 108 |
component={RouterLink} |
90 | 109 |
to={`/catalog/${item.id as string}`} |
110 |
onClick={() => setUnload(false)} |
|
91 | 111 |
> |
92 | 112 |
{item.name} |
93 | 113 |
</Link> |
94 | 114 |
</TableCell> |
95 |
{mapValueOrDefault(item.alternativeNames?.join(', '))} |
|
115 |
{mapValueOrDefault(item.alternativeNames?.join(', '), { |
|
116 |
display: '-webkit-box', |
|
117 |
overflow: 'hidden', |
|
118 |
WebkitBoxOrient: 'vertical', |
|
119 |
wordBreak: 'break-all', |
|
120 |
WebkitLineClamp: 2, |
|
121 |
})} |
|
96 | 122 |
{mapValueOrDefault(item.writtenForms?.join(', '))} |
97 | 123 |
{mapValueOrDefault(item.types?.join(', '))} |
98 | 124 |
{mapValueOrDefault(item.countries?.join(', '))} |
99 | 125 |
{mapValueOrDefault( |
100 | 126 |
item.latitude && item.longitude |
101 |
? `${item.latitude}, ${item.longitude}` |
|
127 |
? `${item.latitude.toFixed(2)}, ${item.longitude.toFixed( |
|
128 |
2 |
|
129 |
)}` |
|
102 | 130 |
: undefined |
103 | 131 |
)} |
104 | 132 |
{mapValueOrDefault( |
... | ... | |
112 | 140 |
<ShowErrorIfPresent err={displayError} /> |
113 | 141 |
{loading && !displayError ? <ContentLoading /> : null} |
114 | 142 |
{!loading && !displayError ? ( |
115 |
<Fragment>
|
|
116 |
<TableContainer> |
|
143 |
<Fragment> |
|
144 |
<TableContainer sx={{ minHeight: '50vh', maxHeight: '50vh' }}>
|
|
117 | 145 |
<Table |
118 | 146 |
stickyHeader |
119 | 147 |
sx={{ minWidth: 400 }} |
... | ... | |
131 | 159 |
<TableBody> |
132 | 160 |
{items |
133 | 161 |
.slice( |
134 |
page * rowsPerPage[0],
|
|
135 |
page * rowsPerPage[0] + rowsPerPage[0]
|
|
162 |
page * rowsPerPage, |
|
163 |
page * rowsPerPage + rowsPerPage
|
|
136 | 164 |
) |
137 | 165 |
.map((row, idx) => ( |
138 | 166 |
<TableRow hover tabIndex={-1} key={idx}> |
... | ... | |
143 | 171 |
</Table> |
144 | 172 |
</TableContainer> |
145 | 173 |
<TablePagination |
146 |
rowsPerPageOptions={rowsPerPage} |
|
174 |
rowsPerPageOptions={rowsPerPageOptions.map((item) => ({ |
|
175 |
value: |
|
176 |
item === ShowAllItemsOption |
|
177 |
? items.length |
|
178 |
: item, |
|
179 |
label: item as string, |
|
180 |
}))} |
|
147 | 181 |
component="div" |
148 | 182 |
count={items.length} |
149 |
rowsPerPage={selectedRowsPerPage}
|
|
183 |
rowsPerPage={rowsPerPage}
|
|
150 | 184 |
page={page} |
151 | 185 |
onPageChange={(_, newPage) => setPage(newPage)} |
152 | 186 |
onRowsPerPageChange={onRowsPerPageChange} |
frontend/src/features/Catalog/catalogSlice.tsx | ||
---|---|---|
11 | 11 |
export interface CatalogState { |
12 | 12 |
items: CatalogItemDto[] // list of all fetched items |
13 | 13 |
filter: CatalogFilter // filter object |
14 |
filterOpen: boolean |
|
14 | 15 |
loading: boolean // whether the catalog is loading |
15 | 16 |
error?: string |
17 |
rowsPerPage: number |
|
18 |
rowsPerPageOptions: any[] |
|
16 | 19 |
} |
17 | 20 |
|
21 |
export const ShowAllItemsOption = 'All' |
|
22 |
|
|
18 | 23 |
const initialState: CatalogState = { |
19 | 24 |
items: [], |
20 | 25 |
filter: {}, |
26 |
filterOpen: false, |
|
21 | 27 |
loading: true, |
22 | 28 |
error: undefined, |
29 |
rowsPerPage: 20, |
|
30 |
rowsPerPageOptions: [5, 10, 20, 50, 100, 1000, ShowAllItemsOption], |
|
23 | 31 |
} |
24 | 32 |
|
25 | 33 |
const catalogSlice = createSlice({ |
... | ... | |
28 | 36 |
reducers: { |
29 | 37 |
setFilter: (state, action) => ({ |
30 | 38 |
...state, |
31 |
filter: {...action.payload},
|
|
39 |
filter: { ...action.payload },
|
|
32 | 40 |
}), |
33 | 41 |
clearFilter: (state, action) => ({ |
34 | 42 |
...state, |
35 | 43 |
loading: true, |
36 | 44 |
filter: {}, |
37 | 45 |
}), |
46 |
setFilterOpen: (state, action) => ({ |
|
47 |
...state, |
|
48 |
filterOpen: action.payload, |
|
49 |
}), |
|
38 | 50 |
clear: (state) => ({ ...initialState }), |
39 | 51 |
setLoading: (state) => ({ ...state, loading: true }), |
40 | 52 |
consumeError: (state) => ({ ...state, error: undefined }), |
53 |
setRowsPerPage: (state, action) => ({ |
|
54 |
...state, |
|
55 |
rowsPerPage: action.payload, |
|
56 |
}), |
|
41 | 57 |
}, |
42 | 58 |
extraReducers: (builder) => { |
43 | 59 |
builder.addCase(fetchItems.pending, (state) => ({ |
... | ... | |
57 | 73 |
}, |
58 | 74 |
}) |
59 | 75 |
|
60 |
export const { setFilter, clearFilter, clear, setLoading, consumeError } = catalogSlice.actions |
|
76 |
export const { |
|
77 |
setFilter, |
|
78 |
clearFilter, |
|
79 |
setFilterOpen, |
|
80 |
clear, |
|
81 |
setLoading, |
|
82 |
consumeError, |
|
83 |
setRowsPerPage, |
|
84 |
} = catalogSlice.actions |
|
61 | 85 |
const reducer = catalogSlice.reducer |
62 | 86 |
export default reducer |
frontend/src/features/Navigation/Navigation.tsx | ||
---|---|---|
17 | 17 |
import ListItemText from '@mui/material/ListItemText' |
18 | 18 |
import InboxIcon from '@mui/icons-material/MoveToInbox' |
19 | 19 |
import MailIcon from '@mui/icons-material/Mail' |
20 |
import { FunctionComponent } from 'react' |
|
20 |
import { Fragment, FunctionComponent } from 'react'
|
|
21 | 21 |
import NavigationMenu from './NavigationMenu' |
22 | 22 |
import { Paper } from '@mui/material' |
23 | 23 |
|
... | ... | |
84 | 84 |
} |
85 | 85 |
|
86 | 86 |
return ( |
87 |
<Box sx={{ display: 'flex' }}> |
|
88 |
<CssBaseline /> |
|
89 |
<AppBar position="fixed" open={open}> |
|
90 |
<Toolbar> |
|
91 |
<IconButton |
|
92 |
color="inherit" |
|
93 |
aria-label="open drawer" |
|
94 |
onClick={onOpenDrawer} |
|
95 |
edge="start" |
|
96 |
sx={{ mr: 2, ...(open && { display: 'none' }) }} |
|
97 |
> |
|
98 |
<MenuIcon /> |
|
99 |
</IconButton> |
|
100 |
<Typography variant="h6" noWrap component="div"> |
|
101 |
Assyrian Toponyms App Prototype |
|
102 |
</Typography> |
|
103 |
</Toolbar> |
|
104 |
</AppBar> |
|
105 |
<NavigationMenu |
|
106 |
open={open} |
|
107 |
drawerWidth={drawerWidth} |
|
108 |
setOpen={setOpen} |
|
109 |
/> |
|
110 |
<Main open={open} sx={{ mt: 2 }}> |
|
111 |
<Paper style={{ minHeight: '100vh', borderRadius: 0 }}> |
|
112 |
<DrawerHeader /> |
|
113 |
{children} |
|
114 |
</Paper> |
|
115 |
</Main> |
|
116 |
</Box> |
|
87 |
<Fragment> |
|
88 |
<Paper style={{ minHeight: '100vh', borderRadius: 0 }}> |
|
89 |
<Box sx={{ display: 'flex' }}> |
|
90 |
{/* <CssBaseline /> */} |
|
91 |
<AppBar position="fixed" open={open}> |
|
92 |
<Toolbar> |
|
93 |
<IconButton |
|
94 |
color="inherit" |
|
95 |
aria-label="open drawer" |
|
96 |
onClick={onOpenDrawer} |
|
97 |
edge="start" |
|
98 |
sx={{ ...(open && { display: 'none' }) }} |
|
99 |
> |
|
100 |
<MenuIcon /> |
|
101 |
</IconButton> |
|
102 |
<Typography variant="h6" noWrap component="div"> |
|
103 |
Assyrian Toponyms App Prototype |
|
104 |
</Typography> |
|
105 |
</Toolbar> |
|
106 |
</AppBar> |
|
107 |
<NavigationMenu |
|
108 |
open={open} |
|
109 |
drawerWidth={drawerWidth} |
|
110 |
setOpen={setOpen} |
|
111 |
/> |
|
112 |
<Main open={open} sx={{ mt: 2 }}> |
|
113 |
<DrawerHeader /> |
|
114 |
{children} |
|
115 |
</Main> |
|
116 |
</Box> |
|
117 |
</Paper> |
|
118 |
</Fragment> |
|
117 | 119 |
) |
118 | 120 |
} |
119 | 121 |
|
frontend/src/features/Reusables/ShowErrorIfPresent.tsx | ||
---|---|---|
9 | 9 |
const ShowErrorIfPresent: FunctionComponent<ShowErrorProps> = ({ err }) => ( |
10 | 10 |
<Fragment> |
11 | 11 |
{err ? ( |
12 |
<Typography align="center" variant="h6" fontWeight="400">
|
|
12 |
<Typography sx={{mb: 1}} align="center" variant="h6" color="error" fontWeight="bold">
|
|
13 | 13 |
{err} |
14 | 14 |
</Typography> |
15 | 15 |
) : null} |
Také k dispozici: Unified diff
Table scrolling
re #9534