Revize 2da5627e
Přidáno uživatelem Václav Honzík před asi 3 roky(ů)
frontend/package.json | ||
---|---|---|
6 | 6 |
"dependencies": { |
7 | 7 |
"@emotion/react": "^11.8.2", |
8 | 8 |
"@emotion/styled": "^11.8.1", |
9 |
"@mui/icons-material": "^5.5.1", |
|
9 | 10 |
"@mui/material": "^5.5.2", |
10 | 11 |
"@testing-library/jest-dom": "^5.14.1", |
11 | 12 |
"@testing-library/react": "^12.0.0", |
frontend/src/api/axios.ts | ||
---|---|---|
1 |
import axios from 'axios' |
|
2 |
import Config from '../config/Config' |
|
3 |
|
|
4 |
export default axios.create({ |
|
5 |
baseURL: Config.baseUrl, |
|
6 |
|
|
7 |
}) |
|
8 |
|
frontend/src/config/Config.ts | ||
---|---|---|
1 |
const Config = { |
|
2 |
baseUrl: process.env.REACT_APP_API_BASE_URL || 'http://localhost:8080' |
|
3 |
} |
|
4 |
|
|
5 |
export default Config |
frontend/src/features/Auth/AuthService.ts | ||
---|---|---|
1 |
import axios from "../../api/axios" |
|
2 |
import { UserDto } from "../../swagger/data-contracts" |
|
3 |
import ApiCallError from "../../utils/ApiCallError" |
|
4 |
|
|
5 |
export const getAccessToken = () => localStorage.getItem('accessToken') as string | null |
|
6 |
export const getRefreshToken = () => localStorage.getItem('refreshToken') as string | null |
|
7 |
|
|
8 |
export const setAccessToken = (accessToken: string) => localStorage.setItem('accessToken', accessToken) |
|
9 |
export const setRefreshToken = (refreshToken: string) => localStorage.setItem('refreshToken', refreshToken) |
|
10 |
|
|
11 |
|
|
12 |
export const sendRegisterRequest = (userDto: UserDto) => { |
|
13 |
|
|
14 |
} |
|
15 |
|
|
16 |
export const sendLoginRequest = async (username: string, password: string, setLoggedInState: (loggedIn: boolean) => void) => { |
|
17 |
try { |
|
18 |
const { data } = await axios.post('/login', {username, password}) |
|
19 |
|
|
20 |
if (!data) { |
|
21 |
throw new ApiCallError("An authentication error has occurred. Please try again later") |
|
22 |
} |
|
23 |
|
|
24 |
// TODO - set state as logged in |
|
25 |
const { accessToken, refreshToken } = data |
|
26 |
setAccessToken(accessToken) |
|
27 |
setRefreshToken(refreshToken) |
|
28 |
setLoggedInState(true) |
|
29 |
} |
|
30 |
catch (err) { |
|
31 |
|
|
32 |
} |
|
33 |
} |
frontend/src/features/Auth/Register.tsx | ||
---|---|---|
1 |
|
|
2 |
export default {} |
frontend/src/features/Catalog/Catalog.tsx | ||
---|---|---|
1 |
import { |
|
2 |
Box, |
|
3 |
Button, |
|
4 |
Container, |
|
5 |
Grid, |
|
6 |
Paper, |
|
7 |
Stack, |
|
8 |
TextField, |
|
9 |
} from '@mui/material' |
|
1 | 10 |
import { useEffect, useState } from 'react' |
2 | 11 |
import { CatalogDto } from '../../swagger/data-contracts' |
12 |
import CatalogTable from './CatalogTable' |
|
3 | 13 |
|
4 | 14 |
const Catalog = () => { |
5 |
// List of all catalog items |
|
6 |
const [catalogItems, setCatalogItems] = useState<CatalogDto[]>([]) |
|
7 |
|
|
8 |
// Whether the request has been processed |
|
9 |
const [isLoading, setIsLoading] = useState(true) |
|
10 |
|
|
11 |
useEffect(() => { |
|
12 |
|
|
13 |
}, [isLoading]) |
|
15 |
const createRow = (): CatalogDto => ({ |
|
16 |
name: 'Cell', |
|
17 |
certainty: 100, |
|
18 |
longitude: 1, |
|
19 |
latitude: 2, |
|
20 |
writtenForms: ['Written Form'], |
|
21 |
bibliography: ['Bibliography'], |
|
22 |
countries: ['Country'], |
|
23 |
alternativeNames: ['Alternative Name'], |
|
24 |
}) |
|
25 |
const data: CatalogDto[] = Array(25).fill(createRow()) |
|
14 | 26 |
|
15 | 27 |
return ( |
16 | 28 |
<> |
17 |
<h1>Catalog</h1> |
|
29 |
<Paper sx={{ py: 2 }}> |
|
30 |
<Container sx={{ mt: 4 }}> |
|
31 |
<Button variant="contained">Filter</Button> |
|
32 |
<Grid container spacing={1} alignItems="stretch"> |
|
33 |
<Grid item xs={6}> |
|
34 |
<Stack |
|
35 |
direction="column" |
|
36 |
spacing={1} |
|
37 |
sx={{ mt: 2 }} |
|
38 |
> |
|
39 |
<Stack direction="row" spacing={2}> |
|
40 |
<TextField id="name" label="Name" /> |
|
41 |
<TextField id="type" label="Type" /> |
|
42 |
<TextField |
|
43 |
id="coordinates" |
|
44 |
label="Coordinates" |
|
45 |
/> |
|
46 |
</Stack> |
|
47 |
<Stack direction="row" spacing={2}> |
|
48 |
<TextField |
|
49 |
id="writtenForm" |
|
50 |
label="Written form" |
|
51 |
/> |
|
52 |
<TextField |
|
53 |
id="stateOrTerritory" |
|
54 |
label="State or territory" |
|
55 |
/> |
|
56 |
<TextField id="groupBy" label="Group by" /> |
|
57 |
</Stack> |
|
58 |
</Stack> |
|
59 |
</Grid> |
|
60 |
<Grid item xs sx={{ mt: 'auto', ml: 1, mb: 1 }}> |
|
61 |
<Stack |
|
62 |
direction="row" |
|
63 |
justifyContent="flex-start" |
|
64 |
alignItems="flex-end" |
|
65 |
> |
|
66 |
<Button variant="outlined">Search</Button> |
|
67 |
</Stack> |
|
68 |
</Grid> |
|
69 |
</Grid> |
|
70 |
|
|
71 |
<Box sx={{ mt: 4 }}> |
|
72 |
<CatalogTable data={data} /> |
|
73 |
</Box> |
|
74 |
</Container> |
|
75 |
</Paper> |
|
18 | 76 |
</> |
19 | 77 |
) |
20 | 78 |
} |
frontend/src/features/Catalog/CatalogTable.tsx | ||
---|---|---|
1 |
import { useTheme } from '@mui/material/styles' |
|
2 |
import { |
|
3 |
Box, |
|
4 |
Paper, |
|
5 |
Table, |
|
6 |
TableBody, |
|
7 |
TableCell, |
|
8 |
TableContainer, |
|
9 |
TableFooter, |
|
10 |
TablePagination, |
|
11 |
TableRow, |
|
12 |
} from '@mui/material' |
|
13 |
import TablePaginationActions, { TablePaginationActionsProps } from '@mui/material/TablePagination/TablePaginationActions' |
|
14 |
import { FunctionComponent, useState } from 'react' |
|
15 |
import IconButton from '@mui/material/IconButton' |
|
16 |
import FirstPageIcon from '@mui/icons-material/FirstPage' |
|
17 |
import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft' |
|
18 |
import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight' |
|
19 |
import LastPageIcon from '@mui/icons-material/LastPage' |
|
20 |
import { CatalogDto } from '../../swagger/data-contracts' |
|
21 |
|
|
22 |
|
|
23 |
const CatalogTableActions: FunctionComponent<TablePaginationActionsProps> = ({ |
|
24 |
count, |
|
25 |
page, |
|
26 |
rowsPerPage, |
|
27 |
onPageChange: onChangePage, |
|
28 |
}) => { |
|
29 |
const theme = useTheme() |
|
30 |
|
|
31 |
const handleFirstPageButtonClick = ( |
|
32 |
event: React.MouseEvent<HTMLButtonElement> |
|
33 |
) => { |
|
34 |
onChangePage(event, 0) |
|
35 |
} |
|
36 |
|
|
37 |
const handleBackButtonClick = ( |
|
38 |
event: React.MouseEvent<HTMLButtonElement> |
|
39 |
) => { |
|
40 |
onChangePage(event, page - 1) |
|
41 |
} |
|
42 |
|
|
43 |
const handleNextButtonClick = ( |
|
44 |
event: React.MouseEvent<HTMLButtonElement> |
|
45 |
) => { |
|
46 |
onChangePage(event, page + 1) |
|
47 |
} |
|
48 |
|
|
49 |
const handleLastPageButtonClick = ( |
|
50 |
event: React.MouseEvent<HTMLButtonElement> |
|
51 |
) => { |
|
52 |
onChangePage(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)) |
|
53 |
} |
|
54 |
|
|
55 |
return ( |
|
56 |
<Box sx={{ flexShrink: 0, ml: 2.5 }}> |
|
57 |
<IconButton |
|
58 |
onClick={handleFirstPageButtonClick} |
|
59 |
disabled={page === 0} |
|
60 |
aria-label="first page" |
|
61 |
> |
|
62 |
{theme.direction === 'rtl' ? ( |
|
63 |
<LastPageIcon /> |
|
64 |
) : ( |
|
65 |
<FirstPageIcon /> |
|
66 |
)} |
|
67 |
</IconButton> |
|
68 |
<IconButton |
|
69 |
onClick={handleBackButtonClick} |
|
70 |
disabled={page === 0} |
|
71 |
aria-label="previous page" |
|
72 |
> |
|
73 |
{theme.direction === 'rtl' ? ( |
|
74 |
<KeyboardArrowRight /> |
|
75 |
) : ( |
|
76 |
<KeyboardArrowLeft /> |
|
77 |
)} |
|
78 |
</IconButton> |
|
79 |
<IconButton |
|
80 |
onClick={handleNextButtonClick} |
|
81 |
disabled={page >= Math.ceil(count / rowsPerPage) - 1} |
|
82 |
aria-label="next page" |
|
83 |
> |
|
84 |
{theme.direction === 'rtl' ? ( |
|
85 |
<KeyboardArrowLeft /> |
|
86 |
) : ( |
|
87 |
<KeyboardArrowRight /> |
|
88 |
)} |
|
89 |
</IconButton> |
|
90 |
<IconButton |
|
91 |
onClick={handleLastPageButtonClick} |
|
92 |
disabled={page >= Math.ceil(count / rowsPerPage) - 1} |
|
93 |
aria-label="last page" |
|
94 |
> |
|
95 |
{theme.direction === 'rtl' ? ( |
|
96 |
<FirstPageIcon /> |
|
97 |
) : ( |
|
98 |
<LastPageIcon /> |
|
99 |
)} |
|
100 |
</IconButton> |
|
101 |
</Box> |
|
102 |
) |
|
103 |
} |
|
104 |
|
|
105 |
function createData(name: string, calories: number, fat: number) { |
|
106 |
return { name, calories, fat } |
|
107 |
} |
|
108 |
|
|
109 |
const rows = [ |
|
110 |
createData('Cupcake', 305, 3.7), |
|
111 |
createData('Donut', 452, 25.0), |
|
112 |
createData('Eclair', 262, 16.0), |
|
113 |
createData('Frozen yoghurt', 159, 6.0), |
|
114 |
createData('Gingerbread', 356, 16.0), |
|
115 |
createData('Honeycomb', 408, 3.2), |
|
116 |
createData('Ice cream sandwich', 237, 9.0), |
|
117 |
createData('Jelly Bean', 375, 0.0), |
|
118 |
createData('KitKat', 518, 26.0), |
|
119 |
createData('Lollipop', 392, 0.2), |
|
120 |
createData('Marshmallow', 318, 0), |
|
121 |
createData('Nougat', 360, 19.0), |
|
122 |
createData('Oreo', 437, 18.0), |
|
123 |
].sort((a, b) => (a.calories < b.calories ? -1 : 1)) |
|
124 |
|
|
125 |
|
|
126 |
export interface CatalogTableProps { |
|
127 |
data: CatalogDto[] |
|
128 |
} |
|
129 |
|
|
130 |
const CatalogTable: FunctionComponent<CatalogTableProps> = ({ data }) => { |
|
131 |
const [page, setPage] = useState(0) |
|
132 |
const [rowsPerPage, setRowsPerPage] = useState(5) |
|
133 |
|
|
134 |
// Avoid a layout jump when reaching the last page with empty rows. |
|
135 |
const emptyRows = |
|
136 |
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0 |
|
137 |
|
|
138 |
const handleChangePage = ( |
|
139 |
event: React.MouseEvent<HTMLButtonElement> | null, |
|
140 |
newPage: number |
|
141 |
) => { |
|
142 |
setPage(newPage) |
|
143 |
} |
|
144 |
|
|
145 |
const handleChangeRowsPerPage = ( |
|
146 |
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> |
|
147 |
) => { |
|
148 |
setRowsPerPage(parseInt(event.target.value, 10)) |
|
149 |
setPage(0) |
|
150 |
} |
|
151 |
|
|
152 |
return ( |
|
153 |
<TableContainer component={Paper}> |
|
154 |
<Table sx={{ minWidth: 500 }} aria-label="custom pagination table"> |
|
155 |
<TableBody> |
|
156 |
{(rowsPerPage > 0 |
|
157 |
? data.slice( |
|
158 |
page * rowsPerPage, |
|
159 |
page * rowsPerPage + rowsPerPage |
|
160 |
) |
|
161 |
: data |
|
162 |
).map((row) => ( |
|
163 |
<TableRow key={row.name}> |
|
164 |
<TableCell component="th" scope="row"> |
|
165 |
{row.name} |
|
166 |
</TableCell> |
|
167 |
<TableCell style={{ width: 160 }} align="right"> |
|
168 |
{row?.alternativeNames ? row.alternativeNames.join(', ') : ''} |
|
169 |
</TableCell> |
|
170 |
<TableCell style={{ width: 160 }} align="right"> |
|
171 |
{row?.writtenForms ? row.writtenForms.join(', ') : ''} |
|
172 |
</TableCell> |
|
173 |
<TableCell style={{ width: 160 }} align="right"> |
|
174 |
{row?.writtenForms ? row.writtenForms.join(', ') : ''} |
|
175 |
</TableCell> |
|
176 |
</TableRow> |
|
177 |
))} |
|
178 |
{emptyRows > 0 && ( |
|
179 |
<TableRow style={{ height: 53 * emptyRows }}> |
|
180 |
<TableCell colSpan={6} /> |
|
181 |
</TableRow> |
|
182 |
)} |
|
183 |
</TableBody> |
|
184 |
<TableFooter> |
|
185 |
<TableRow> |
|
186 |
<TablePagination |
|
187 |
rowsPerPageOptions={[ |
|
188 |
5, |
|
189 |
10, |
|
190 |
25, |
|
191 |
{ label: 'All', value: -1 }, |
|
192 |
]} |
|
193 |
colSpan={3} |
|
194 |
count={rows.length} |
|
195 |
rowsPerPage={rowsPerPage} |
|
196 |
page={page} |
|
197 |
SelectProps={{ |
|
198 |
inputProps: { |
|
199 |
'aria-label': 'rows per page', |
|
200 |
}, |
|
201 |
native: true, |
|
202 |
}} |
|
203 |
onPageChange={handleChangePage} |
|
204 |
onRowsPerPageChange={handleChangeRowsPerPage} |
|
205 |
ActionsComponent={CatalogTableActions} |
|
206 |
/> |
|
207 |
</TableRow> |
|
208 |
</TableFooter> |
|
209 |
</Table> |
|
210 |
</TableContainer> |
|
211 |
) |
|
212 |
} |
|
213 |
|
|
214 |
export default CatalogTable |
Také k dispozici: Unified diff
re #9130 Catalog table component template mui doc