Skip to content

Commit 21c0cf5

Browse files
committed
feat: loading skeleton
1 parent fa3939c commit 21c0cf5

File tree

5 files changed

+189
-44
lines changed

5 files changed

+189
-44
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { Button } from "@chakra-ui/button";
2+
import { ButtonGroup, Select, Stack } from "@chakra-ui/react";
3+
import { ReactNode } from "react";
4+
5+
export interface PaginationProps {
6+
page?: number;
7+
size?: number;
8+
totalPages?: number;
9+
totalElements?: number;
10+
sizes?: number[];
11+
onChange?: (page: number, size: number) => void;
12+
}
13+
14+
export const Pagination = ({
15+
onChange,
16+
page = 1,
17+
size = 10,
18+
totalPages = 1,
19+
totalElements = 0,
20+
sizes = [10, 20, 25, 50]
21+
}: PaginationProps) => {
22+
const intervalNumbers = (x: number, y: number): number[] => {
23+
let result = [];
24+
let i = x + 1;
25+
while (i < y) {
26+
result.push(i);
27+
i++;
28+
}
29+
return result;
30+
};
31+
32+
const interval = (numbers: number[]): ReactNode => {
33+
const validNumbers = numbers.filter(item => item > -1 && item < totalPages);
34+
const buttons = validNumbers.map(intervalNumber => (
35+
<Button
36+
key={intervalNumber}
37+
onClick={() => handleChange(intervalNumber, size)}
38+
variant={page === intervalNumber ? 'solid' : 'outline'}
39+
disabled={intervalNumber === page}>
40+
{intervalNumber + 1}
41+
</Button>
42+
));
43+
return buttons;
44+
}
45+
46+
const handleChange = (page: number, size: number) => {
47+
onChange && onChange(page, size);
48+
}
49+
50+
return (
51+
<Stack direction="row" spacing="5">
52+
<ButtonGroup size='sm' isAttached variant="outline">
53+
<Button
54+
onClick={() => handleChange(0, size)}
55+
key="Primeira"
56+
disabled={page === 0}>
57+
Primeira
58+
</Button>
59+
60+
{page > 0 && (
61+
<Button
62+
onClick={() => handleChange(page - 1, size)}
63+
key="Anterior"
64+
disabled={page === 0}>
65+
Anterior
66+
</Button>
67+
)}
68+
69+
{interval(intervalNumbers(page - 3, page + 3))}
70+
71+
{page + 1 < totalPages && (
72+
<Button
73+
onClick={() => handleChange(page + 1, size)}
74+
key="Proxima"
75+
disabled={page === totalPages}>
76+
Proxima
77+
</Button>
78+
)}
79+
80+
<Button
81+
onClick={() => handleChange(totalPages - 1, size)}
82+
key="Ultima"
83+
disabled={page + 1 === totalPages}>
84+
Ultima
85+
</Button>
86+
</ButtonGroup>
87+
88+
<Select
89+
onChange={({ target: { value } }) => handleChange(0, Number(value))}
90+
borderRadius="5"
91+
w="52"
92+
size="sm"
93+
placeholder='Itens por pagina'>
94+
{sizes.map(option => (
95+
<option key={option} value={option}>{option}</option>
96+
))}
97+
</Select>
98+
</Stack>
99+
)
100+
}

web/src/components/table/index.tsx

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
import { ReactNode } from "react";
22

33
import { Table as ChakraUiTable, TableContainer, Tbody, Td, Th, Thead, Tr, TableHeadProps, TableColumnHeaderProps } from "@chakra-ui/table";
4+
import { Box, Skeleton } from "@chakra-ui/react";
5+
import { Random } from "../../utils/random";
46

57
export interface Column<T> {
68
title?: string;
9+
disableSkeleton?: boolean;
710
options?: TableColumnHeaderProps
811
property?: keyof T;
912
defaultValue?: string;
1013
render?: (row: T) => ReactNode;
1114
}
1215

1316
export interface Options<T> {
17+
loading?: boolean;
18+
variant?: 'simple' | 'striped' | 'unstyled';
19+
size?: 'sm' | 'md' | 'lg';
1420
content: T[];
1521
columns: Column<T>[];
1622
}
1723

18-
export function Table<T>({ content, columns }: Options<T>) {
24+
export function Table<T>({ loading, content, columns, size, variant }: Options<T>) {
1925
return (
2026
<TableContainer>
21-
<ChakraUiTable>
27+
<ChakraUiTable minHeight="520px" size={size} variant={variant}>
2228
<Thead>
2329
<Tr>
2430
{columns.map(({ title: name, property, options }, index) => (
@@ -29,27 +35,52 @@ export function Table<T>({ content, columns }: Options<T>) {
2935
</Tr>
3036
</Thead>
3137
<Tbody>
38+
{content && content.length === 0 && (
39+
Array(10)
40+
.fill(0)
41+
.map((_, rowIndex) => (
42+
<Tr key={rowIndex}>
43+
{columns.map((_, columnIndex) => (
44+
<Td key={columnIndex}>
45+
<Box py="2">
46+
<Skeleton height="12px" borderRadius="3" width={`${Random.between(25, 100)}%`} />
47+
</Box>
48+
</Td>
49+
))}
50+
</Tr>
51+
))
52+
)}
3253
{content.map((row, index) => (
3354
<Tr key={index}>
3455
{columns.map(
35-
({ render, property, defaultValue }, prop) => {
56+
({ render, property, defaultValue, disableSkeleton }, tdIndex) => {
57+
if (loading && !disableSkeleton) {
58+
return (
59+
<Td key={tdIndex}>
60+
<Box py="2.5">
61+
<Skeleton height="13px" borderRadius="3" width={`${Random.between(25, 100)}%`} />
62+
</Box>
63+
</Td>
64+
)
65+
}
66+
3667
if (property) {
3768
const types = ["number", "boolean", "string"];
3869
const value = row[property];
3970

4071
if (value && types.includes(typeof value)) {
41-
return <Td key={prop}>{`${value}`}</Td>;
72+
return <Td key={tdIndex}>{`${value}`}</Td>;
4273
}
4374
}
4475

4576
if (render) {
4677
const element = render(row);
4778
if (element) {
48-
return <Td key={prop}>{element}</Td>;
79+
return <Td key={tdIndex}>{element}</Td>;
4980
}
5081
}
5182

52-
return <Td key={prop}>{defaultValue ?? ""}</Td>;
83+
return <Td key={tdIndex}>{defaultValue ?? ""}</Td>;
5384
}
5485
)}
5586
</Tr>

web/src/pages/users/index.tsx

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { Button } from "@chakra-ui/button";
22
import { useDisclosure } from "@chakra-ui/hooks";
33
import { Box, Heading, HStack } from "@chakra-ui/layout";
44
import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from "@chakra-ui/modal";
5-
import { Badge, ButtonGroup, Center, Stack } from "@chakra-ui/react";
5+
import { Badge, Center, Stack } from "@chakra-ui/react";
66
import { useEffect, useState } from "react";
77
import { BsFillPenFill, BsTrash2 } from 'react-icons/bs';
88
import { Template } from "../../components/page";
9+
import { Pagination, PaginationProps } from "../../components/pagination";
910
import { Table } from "../../components/table";
1011
import { User } from "../../services/models/user";
1112
import { UsersApi } from "../../services/users";
@@ -14,19 +15,30 @@ export const Users = () => {
1415

1516
const { isOpen, onOpen, onClose } = useDisclosure()
1617

18+
const [{ page, size, totalPages }, setPagination] = useState<PaginationProps>({
19+
page: 0,
20+
size: 10,
21+
totalPages: 0
22+
});
23+
1724
const [users, setUsers] = useState<User[]>([]);
25+
const [loading, setLoading] = useState(false);
1826

19-
const getUsers = async () => {
27+
const fetchUsers = async (page?: number, size?: number) => {
28+
setLoading(true);
2029
try {
21-
const { data: page } = await UsersApi.findAll();
22-
setUsers(page.content);
30+
const { data: { content, ...pagination } } = await UsersApi.findAll(page, size);
31+
setUsers(content);
32+
setPagination(pagination);
2333
} catch (error) {
2434
console.error('error on find all users');
35+
} finally {
36+
setLoading(false);
2537
}
2638
}
2739

2840
useEffect(() => {
29-
getUsers();
41+
fetchUsers();
3042
}, [])
3143

3244
return (
@@ -42,6 +54,8 @@ export const Users = () => {
4254
</Box>
4355
<Box>
4456
<Table
57+
loading={loading}
58+
size="sm"
4559
content={users}
4660
columns={[
4761
{
@@ -56,7 +70,7 @@ export const Users = () => {
5670
title: 'Roles',
5771
render: ({ roles }) => (
5872
<Stack direction='row'>
59-
{roles.map(role => <Badge>{role}</Badge>)}
73+
{roles.map(role => <Badge key={role}>{role}</Badge>)}
6074
</Stack>
6175
)
6276
},
@@ -71,14 +85,17 @@ export const Users = () => {
7185
options: {
7286
w: '28'
7387
},
88+
disableSkeleton: true,
7489
render: () => (
7590
<HStack>
7691
<Button
92+
disabled={loading}
7793
size="sm"
7894
variant="outline">
7995
<BsFillPenFill />
8096
</Button>
8197
<Button
98+
disabled={loading}
8299
size="sm"
83100
variant="outline"
84101
onClick={onOpen}>
@@ -89,36 +106,13 @@ export const Users = () => {
89106
}
90107
]}
91108
/>
92-
<Center mt="12">
93-
<ButtonGroup size='md' isAttached variant="outline">
94-
<Button disabled>
95-
Primeira
96-
</Button>
97-
<Button>
98-
Anterior
99-
</Button>
100-
<Button>
101-
1
102-
</Button>
103-
<Button>
104-
2
105-
</Button>
106-
<Button variant="solid">
107-
3
108-
</Button>
109-
<Button>
110-
4
111-
</Button>
112-
<Button>
113-
5
114-
</Button>
115-
<Button>
116-
Proxima
117-
</Button>
118-
<Button>
119-
Ultima
120-
</Button>
121-
</ButtonGroup>
109+
<Center my="12">
110+
<Pagination
111+
page={page}
112+
size={size}
113+
totalPages={totalPages}
114+
onChange={(page, size) => fetchUsers(page, size)}
115+
/>
122116
</Center>
123117
</Box>
124118

web/src/services/users/index.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,19 @@ import { User } from "../models/user";
55

66
const RESOURCE_URL = 'users'
77

8-
export const findAll = () => {
9-
return api.get<Page<User>>(RESOURCE_URL);
8+
export const findAll = (page?: number, size?: number) => {
9+
10+
const params = new URLSearchParams();
11+
12+
if (page !== undefined) {
13+
params.append("page", page.toString());
14+
}
15+
16+
if (size !== undefined) {
17+
params.append("size", size.toString());
18+
}
19+
20+
return api.get<Page<User>>(RESOURCE_URL, { params });
1021
}
1122

1223
export const UsersApi = {

web/src/utils/random.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export class Random {
2+
public static element<T>(array: T[]): T {
3+
return array[Math.floor(Math.random() * array.length)];
4+
}
5+
6+
public static between(min: number, max: number): number {
7+
return Math.floor(Math.random() * (max - min + 1)) + min;
8+
}
9+
}

0 commit comments

Comments
 (0)