├── .github
├── logo.svg
└── mockup.png
├── .gitignore
├── LICENSE
├── README.md
├── next-env.d.ts
├── package.json
├── public
└── favicon.ico
├── src
├── components
│ ├── ActiveLink
│ │ └── index.tsx
│ ├── Form
│ │ ├── Input.tsx
│ │ └── index.tsx
│ ├── Header
│ │ ├── NotificationsNav.tsx
│ │ ├── Profile.tsx
│ │ ├── SearchBox.tsx
│ │ └── index.tsx
│ ├── Logo
│ │ └── index.tsx
│ ├── Pagination
│ │ ├── PaginationItem.tsx
│ │ └── index.tsx
│ ├── Sidebar
│ │ ├── NavLink.tsx
│ │ ├── NavSection.tsx
│ │ ├── SidebarNav.tsx
│ │ └── index.tsx
│ └── index.tsx
├── contexts
│ └── SidebarDrawerContext.tsx
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── dashboard.tsx
│ ├── index.tsx
│ └── users
│ │ ├── create.tsx
│ │ └── index.tsx
├── services
│ ├── api.ts
│ ├── hooks
│ │ └── userUsers.ts
│ ├── mirage
│ │ └── index.ts
│ └── queryClient.ts
└── styles
│ └── theme.ts
├── tsconfig.json
└── yarn.lock
/.github/logo.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/.github/mockup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EliasGcf/dashgo/602efd2b90715879be0328ec9e4dd97178c775a4/.github/mockup.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 EliasGcf
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Technologies •
12 | License
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## 🚀 Technologies
20 |
21 | - [ReactJS](https://reactjs.org/)
22 | - [TypeScript](https://www.typescriptlang.org/)
23 | - [Chakra UI](https://chakra-ui.com/)
24 | - [Next.js](https://nextjs.org/)
25 | - [React Hook Form](https://react-hook-form.com/)
26 | - [React Query](https://react-query.tanstack.com/)
27 | - [Mirage JS](https://miragejs.com/)
28 |
29 |
30 | ## 📝 License
31 |
32 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
33 |
34 | ---
35 |
36 |
37 | Made with 💜 by Elias Gabriel
38 |
39 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dashgo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@chakra-ui/core": "^0.8.0",
12 | "@chakra-ui/react": "^1.4.2",
13 | "@emotion/react": "^11.1.5",
14 | "@emotion/styled": "^11.1.5",
15 | "@hookform/resolvers": "^2.0.0",
16 | "apexcharts": "^3.26.0",
17 | "axios": "^0.21.1",
18 | "framer-motion": "^4.1.2",
19 | "next": "10.1.3",
20 | "react": "17.0.2",
21 | "react-apexcharts": "^1.3.7",
22 | "react-dom": "17.0.2",
23 | "react-hook-form": "^7.0.0",
24 | "react-icons": "^4.2.0",
25 | "react-query": "^3.13.5",
26 | "yup": "^0.32.9"
27 | },
28 | "devDependencies": {
29 | "@types/faker": "^5.5.1",
30 | "@types/node": "^14.14.37",
31 | "@types/react": "^17.0.3",
32 | "@types/react-dom": "^17.0.3",
33 | "faker": "^5.5.3",
34 | "miragejs": "^0.1.41",
35 | "typescript": "^4.2.3"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EliasGcf/dashgo/602efd2b90715879be0328ec9e4dd97178c775a4/public/favicon.ico
--------------------------------------------------------------------------------
/src/components/ActiveLink/index.tsx:
--------------------------------------------------------------------------------
1 | import Link, { LinkProps } from 'next/link';
2 | import { useRouter } from 'next/router';
3 | import { cloneElement, ReactElement } from 'react';
4 |
5 | type ActiveLinkProps = LinkProps & {
6 | children: ReactElement;
7 | shouldMatchExactHref?: boolean;
8 | };
9 |
10 | export function ActiveLink({
11 | children,
12 | shouldMatchExactHref = false,
13 | ...rest
14 | }: ActiveLinkProps) {
15 | const { asPath } = useRouter();
16 |
17 | let isActive = false;
18 |
19 | if (shouldMatchExactHref && (asPath === rest.href || asPath === rest.as)) {
20 | isActive = true;
21 | }
22 |
23 | if (
24 | !shouldMatchExactHref &&
25 | (asPath.startsWith(rest.href.toString()) ||
26 | asPath.startsWith(rest.as?.toString()))
27 | ) {
28 | isActive = true;
29 | }
30 |
31 | return (
32 |
33 | {cloneElement(children, { color: isActive ? 'pink.400' : 'gray.50' })}
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/Form/Input.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FormControl,
3 | FormErrorMessage,
4 | FormLabel,
5 | Input as ChakraInput,
6 | InputProps as ChakraInputProps,
7 | } from '@chakra-ui/react';
8 | import { forwardRef, ForwardRefRenderFunction } from 'react';
9 | import { FieldError } from 'react-hook-form';
10 |
11 | type InputProps = ChakraInputProps & {
12 | name: string;
13 | label?: string;
14 | error?: FieldError;
15 | };
16 |
17 | const InputBase: ForwardRefRenderFunction = (
18 | { name, label, error = null, ...rest },
19 | ref,
20 | ) => {
21 | return (
22 |
23 | {label && {label}}
24 |
35 |
36 | {!!error && {error.message}}
37 |
38 | );
39 | };
40 |
41 | export const Input = forwardRef(InputBase);
42 |
--------------------------------------------------------------------------------
/src/components/Form/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Input';
2 |
--------------------------------------------------------------------------------
/src/components/Header/NotificationsNav.tsx:
--------------------------------------------------------------------------------
1 | import { HStack, Icon } from '@chakra-ui/react';
2 | import { RiNotificationLine, RiUserAddLine } from 'react-icons/ri';
3 |
4 | export function NotificationsNav() {
5 | return (
6 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Header/Profile.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, Box, Flex, Text } from '@chakra-ui/react';
2 |
3 | type ProfileProps = {
4 | showProfileData?: boolean;
5 | };
6 |
7 | export function Profile({ showProfileData = true }: ProfileProps) {
8 | return (
9 |
10 | {showProfileData && (
11 |
12 | Elias Gabriel
13 |
14 | elias.gabriel@rocketseat.team
15 |
16 |
17 | )}
18 |
19 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Header/SearchBox.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Icon, Input } from '@chakra-ui/react';
2 | import { RiSearchLine } from 'react-icons/ri';
3 |
4 | export function SearchBox() {
5 | return (
6 |
20 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Icon, IconButton, useBreakpointValue } from '@chakra-ui/react';
2 |
3 | import { useSidebarDrawer } from '../../contexts/SidebarDrawerContext';
4 |
5 | import { Logo } from '../Logo';
6 | import { NotificationsNav } from './NotificationsNav';
7 | import { Profile } from './Profile';
8 | import { SearchBox } from './SearchBox';
9 | import { RiMenuLine } from 'react-icons/ri';
10 |
11 | export function Header() {
12 | const { onOpen } = useSidebarDrawer();
13 |
14 | const isWideVersion = useBreakpointValue({
15 | base: false,
16 | lg: true,
17 | });
18 |
19 | return (
20 |
30 | {!isWideVersion && (
31 | }
34 | fontSize="24"
35 | variant="unstyled"
36 | onClick={onOpen}
37 | marginRight="2"
38 | >
39 | )}
40 |
41 |
42 |
43 | {isWideVersion && }
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Logo/index.tsx:
--------------------------------------------------------------------------------
1 | import { Text, TextProps } from '@chakra-ui/react';
2 |
3 | type LogoProps = TextProps;
4 |
5 | export function Logo({ ...rest }: LogoProps) {
6 | return (
7 |
14 | dashgo
15 |
16 | .
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Pagination/PaginationItem.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@chakra-ui/react';
2 |
3 | type PaginationItemProps = {
4 | number: number;
5 | isCurrent?: boolean;
6 | onPageChange: (page: number) => void;
7 | };
8 |
9 | export function PaginationItem({
10 | isCurrent = false,
11 | number,
12 | onPageChange,
13 | }: PaginationItemProps) {
14 | if (isCurrent) {
15 | return (
16 |
26 | );
27 | }
28 |
29 | return (
30 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Pagination/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Stack, Text } from '@chakra-ui/react';
2 |
3 | import { PaginationItem } from './PaginationItem';
4 |
5 | type PaginationProps = {
6 | totalCountOfRegisters: number;
7 | registersPerPage?: number;
8 | currentPage?: number;
9 | onPageChange: (page: number) => void;
10 | };
11 |
12 | const siblingsCount = 1;
13 |
14 | function generatePagesArray(from: number, to: number) {
15 | return [...new Array(to - from)]
16 | .map((_, index) => from + index + 1)
17 | .filter(page => page > 0);
18 | }
19 |
20 | export function Pagination({
21 | totalCountOfRegisters,
22 | registersPerPage = 10,
23 | currentPage = 1,
24 | onPageChange,
25 | }: PaginationProps) {
26 | const lastPage = Math.floor(totalCountOfRegisters / registersPerPage);
27 |
28 | const previousPage =
29 | currentPage > 1
30 | ? generatePagesArray(currentPage - 1 - siblingsCount, currentPage - 1)
31 | : [];
32 |
33 | const nextPages =
34 | currentPage < lastPage
35 | ? generatePagesArray(
36 | currentPage,
37 | Math.min(currentPage + siblingsCount, lastPage),
38 | )
39 | : [];
40 |
41 | return (
42 |
49 |
50 | 0 - 10 de 100
51 |
52 |
53 | {currentPage > 1 + siblingsCount && (
54 | <>
55 |
56 | {currentPage > 2 + siblingsCount && (
57 |
58 | ...
59 |
60 | )}
61 | >
62 | )}
63 |
64 | {previousPage.length > 0 &&
65 | previousPage.map(page => (
66 |
71 | ))}
72 |
73 |
78 |
79 | {nextPages.length > 0 &&
80 | nextPages.map(page => (
81 |
86 | ))}
87 |
88 | {currentPage + siblingsCount < lastPage && (
89 | <>
90 | {currentPage + 1 + siblingsCount < lastPage && (
91 |
92 | ...
93 |
94 | )}
95 |
96 | >
97 | )}
98 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/Sidebar/NavLink.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Icon,
3 | Link as ChakraLink,
4 | Text,
5 | LinkProps as ChakraLinkProps,
6 | } from '@chakra-ui/react';
7 | import { ElementType } from 'react';
8 | import { ActiveLink } from '../ActiveLink';
9 |
10 | type NavLinkProps = ChakraLinkProps & {
11 | icon: ElementType;
12 | href: string;
13 | children: string;
14 | shouldMatchExactHref?: boolean;
15 | };
16 |
17 | export function NavLink({
18 | icon,
19 | href,
20 | children,
21 | shouldMatchExactHref,
22 | ...rest
23 | }: NavLinkProps) {
24 | return (
25 |
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Sidebar/NavSection.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Icon, Link, Stack, Text } from '@chakra-ui/react';
2 | import { ReactNode } from 'react';
3 | import { RiDashboardLine, RiContactsLine } from 'react-icons/ri';
4 |
5 | type NavSectionProps = {
6 | title: string;
7 | children: ReactNode;
8 | };
9 |
10 | export function NavSection({ title, children }: NavSectionProps) {
11 | return (
12 |
13 |
14 | {title}
15 |
16 |
17 | {children}
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Sidebar/SidebarNav.tsx:
--------------------------------------------------------------------------------
1 | import { Stack } from '@chakra-ui/react';
2 | import {
3 | RiDashboardLine,
4 | RiContactsLine,
5 | RiInputMethodLine,
6 | RiGitMergeLine,
7 | RiLogoutCircleLine,
8 | } from 'react-icons/ri';
9 |
10 | import { NavLink } from './NavLink';
11 | import { NavSection } from './NavSection';
12 |
13 | export function SidebarNav() {
14 | return (
15 |
16 |
17 |
18 | Dashboard
19 |
20 |
21 | Usuários
22 |
23 |
24 |
25 |
26 | Formulários
27 |
28 |
29 | Automação
30 |
31 |
32 |
33 |
34 | Sair
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/Sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Drawer,
4 | DrawerBody,
5 | DrawerCloseButton,
6 | DrawerContent,
7 | DrawerHeader,
8 | DrawerOverlay,
9 | useBreakpointValue,
10 | } from '@chakra-ui/react';
11 | import { useSidebarDrawer } from '../../contexts/SidebarDrawerContext';
12 |
13 | import { SidebarNav } from './SidebarNav';
14 |
15 | export function Sidebar() {
16 | const { isOpen, onClose } = useSidebarDrawer();
17 |
18 | const isDrawerSidebar = useBreakpointValue({
19 | base: true,
20 | lg: false,
21 | });
22 |
23 | if (isDrawerSidebar) {
24 | return (
25 |
26 |
27 |
28 |
29 | Navegação
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | return (
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './ActiveLink';
2 | export * from './Header';
3 | export * from './Logo';
4 | export * from './Pagination';
5 | export * from './Sidebar';
6 |
--------------------------------------------------------------------------------
/src/contexts/SidebarDrawerContext.tsx:
--------------------------------------------------------------------------------
1 | import { useDisclosure, UseDisclosureReturn } from '@chakra-ui/react';
2 | import { useRouter } from 'next/router';
3 | import { createContext, ReactNode, useContext, useEffect } from 'react';
4 |
5 | type SidebarDrawerProviderProps = {
6 | children: ReactNode;
7 | };
8 |
9 | type SidebarDrawerContextData = UseDisclosureReturn;
10 |
11 | const SidebarDrawerContext = createContext({} as SidebarDrawerContextData);
12 |
13 | export function SidebarDrawerProvider({
14 | children,
15 | }: SidebarDrawerProviderProps) {
16 | const disclosure = useDisclosure();
17 | const router = useRouter();
18 |
19 | useEffect(() => {
20 | disclosure.onClose();
21 | }, [router.asPath]);
22 |
23 | return (
24 |
25 | {children}
26 |
27 | );
28 | }
29 |
30 | export const useSidebarDrawer = () => useContext(SidebarDrawerContext);
31 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { AppProps } from 'next/app';
2 | import { ChakraProvider } from '@chakra-ui/react';
3 | import { QueryClientProvider } from 'react-query';
4 | import { ReactQueryDevtools } from 'react-query/devtools';
5 |
6 | import { SidebarDrawerProvider } from '../contexts/SidebarDrawerContext';
7 | import { queryClient } from '../services/queryClient';
8 | import { makeServer } from '../services/mirage';
9 | import { theme } from '../styles/theme';
10 |
11 | // const isDev = process.env.NODE_ENV === 'development';
12 |
13 | // if (isDev) {
14 | makeServer();
15 | // }
16 |
17 | function MyApp({ Component, pageProps }: AppProps) {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | export default MyApp;
32 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Head, Html, Main, NextScript } from 'next/document';
2 |
3 | export default class MyDocument extends Document {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/pages/dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Flex, SimpleGrid, Text, theme } from '@chakra-ui/react';
2 | import { ApexOptions } from 'apexcharts';
3 | import dynamic from 'next/dynamic';
4 |
5 | import { Header } from '../components/Header';
6 | import { Sidebar } from '../components/Sidebar';
7 |
8 | const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
9 |
10 | const options: ApexOptions = {
11 | chart: {
12 | toolbar: { show: false },
13 | zoom: { enabled: false },
14 | foreColor: theme.colors.gray['500'],
15 | },
16 | grid: { show: false },
17 | dataLabels: { enabled: false },
18 | tooltip: { enabled: false },
19 | xaxis: {
20 | type: 'datetime',
21 | axisBorder: { color: theme.colors.gray['600'] },
22 | axisTicks: { color: theme.colors.gray['600'] },
23 | categories: [
24 | '2021-04-02T00:00:00:00.000Z',
25 | '2021-04-03T00:00:00:00.000Z',
26 | '2021-04-04T00:00:00:00.000Z',
27 | '2021-04-05T00:00:00:00.000Z',
28 | '2021-04-06T00:00:00:00.000Z',
29 | '2021-04-07T00:00:00:00.000Z',
30 | ],
31 | },
32 | fill: {
33 | opacity: 0.3,
34 | type: 'gradient',
35 | gradient: {
36 | shade: 'dark',
37 | opacityFrom: 0.7,
38 | opacityTo: 0.3,
39 | },
40 | },
41 | };
42 |
43 | const series = [{ name: 'series1', data: [57, 36, 13, 29, 6, 19] }];
44 |
45 | export default function Dashboard() {
46 | return (
47 |
48 |
49 |
50 |
57 |
58 |
59 |
60 |
66 |
67 | Inscritos da semana
68 |
69 |
70 |
71 |
72 |
78 |
79 | Taxa de abertura
80 |
81 |
82 |
83 |
84 |
85 |
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Flex, Stack } from '@chakra-ui/react';
2 | import { SubmitHandler, useForm } from 'react-hook-form';
3 | import { yupResolver } from '@hookform/resolvers/yup';
4 | import * as yup from 'yup';
5 | import { useRouter } from 'next/router';
6 |
7 | import { Input } from '../components/Form';
8 | import { Logo } from '../components';
9 |
10 | type SignInFormData = {
11 | email: string;
12 | password: string;
13 | };
14 |
15 | const signInFormSchema = yup.object().shape({
16 | email: yup.string().email('E-mail inválido').required('E-mail obrigatório'),
17 | password: yup.string().required('Senha obrigatória'),
18 | });
19 |
20 | export default function SignIn() {
21 | const router = useRouter();
22 |
23 | const { register, handleSubmit, formState } = useForm({
24 | resolver: yupResolver(signInFormSchema),
25 | });
26 |
27 | const handleSignIn: SubmitHandler = async data => {
28 | await new Promise(resolve => setTimeout(resolve, 2000));
29 | await router.push('/dashboard');
30 | };
31 |
32 | return (
33 |
40 |
41 |
42 |
52 |
53 |
60 |
67 |
68 |
69 |
78 |
79 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/src/pages/users/create.tsx:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 | import Link from 'next/link';
3 | import { useRouter } from 'next/router';
4 | import { useMutation } from 'react-query';
5 | import { yupResolver } from '@hookform/resolvers/yup';
6 | import { SubmitHandler, useForm } from 'react-hook-form';
7 | import {
8 | Box,
9 | Button,
10 | Divider,
11 | Flex,
12 | Heading,
13 | HStack,
14 | SimpleGrid,
15 | VStack,
16 | } from '@chakra-ui/react';
17 |
18 | import { Input } from '../../components/Form';
19 | import { Header, Sidebar } from '../../components';
20 | import { api } from '../../services/api';
21 | import { queryClient } from '../../services/queryClient';
22 |
23 | type CreateUserFormData = {
24 | name: string;
25 | email: string;
26 | password: string;
27 | password_confirmation: string;
28 | };
29 |
30 | const createUserFormSchema = yup.object().shape({
31 | name: yup.string().required('Nome obrigatório'),
32 | email: yup.string().email('E-mail inválido').required('E-mail obrigatório'),
33 | password: yup
34 | .string()
35 | .min(6, 'No minimo 6 caracteres')
36 | .required('Senha obrigatória'),
37 | password_confirmation: yup
38 | .string()
39 | .oneOf([null, yup.ref('password')], 'As senhas precisam ser iguais'),
40 | });
41 |
42 | export default function CreateUser() {
43 | const router = useRouter();
44 |
45 | const createUser = useMutation(
46 | async (user: CreateUserFormData) => {
47 | const response = await api.post('users', {
48 | user: {
49 | ...user,
50 | created_at: new Date(),
51 | },
52 | });
53 |
54 | return response.data.user;
55 | },
56 | {
57 | onSuccess: () => {
58 | queryClient.invalidateQueries('users');
59 | },
60 | },
61 | );
62 |
63 | const {
64 | register,
65 | handleSubmit,
66 | formState,
67 | reset,
68 | } = useForm({
69 | resolver: yupResolver(createUserFormSchema),
70 | });
71 |
72 | const handleCreateUser: SubmitHandler = async data => {
73 | await createUser.mutateAsync(data);
74 | reset();
75 | router.push('/users');
76 | };
77 |
78 | return (
79 |
80 |
81 |
82 |
89 |
90 |
91 |
99 |
100 | Criar usuário
101 |
102 |
103 |
104 |
105 |
106 |
112 |
119 |
120 |
121 |
122 |
129 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
145 |
146 |
153 |
154 |
155 |
156 |
157 |
158 | );
159 | }
160 |
--------------------------------------------------------------------------------
/src/pages/users/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import NextLink from 'next/link';
3 | import { RiAddLine, RiPencilLine, RiRefreshLine } from 'react-icons/ri';
4 | import {
5 | Box,
6 | Button,
7 | Checkbox,
8 | Flex,
9 | Heading,
10 | Icon,
11 | Link,
12 | Spinner,
13 | Stack,
14 | Table,
15 | Tbody,
16 | Td,
17 | Text,
18 | Th,
19 | Thead,
20 | Tr,
21 | useBreakpointValue,
22 | } from '@chakra-ui/react';
23 |
24 | import { useUsers } from '../../services/hooks/userUsers';
25 | import { Header, Pagination, Sidebar } from '../../components';
26 | import { queryClient } from '../../services/queryClient';
27 | import { api } from '../../services/api';
28 |
29 | const TEN_MINUTES_IN_MILLISECONDS = 1000 * 60 * 10;
30 |
31 | export default function UsersList() {
32 | const [page, setPage] = useState(1);
33 | const { data, isLoading, error, isFetching, refetch } = useUsers(page);
34 |
35 | const isWideVersion = useBreakpointValue({
36 | base: false,
37 | lg: true,
38 | });
39 |
40 | async function handlePrefetchUser(userId: string) {
41 | await queryClient.prefetchQuery(
42 | ['user', { userId }],
43 | async () => {
44 | const response = await api.get(`users/${userId}`);
45 |
46 | return response.data;
47 | },
48 | { staleTime: TEN_MINUTES_IN_MILLISECONDS },
49 | );
50 | }
51 |
52 | return (
53 |
54 |
55 |
56 |
63 |
64 |
65 |
66 |
67 |
68 | Usuários
69 | {!isLoading && isFetching && (
70 |
71 | )}
72 |
73 |
74 |
75 | }
80 | onClick={() => refetch()}
81 | disabled={isLoading || isFetching}
82 | >
83 | Atualizar
84 |
85 |
86 |
87 | }
93 | >
94 | Criar novo usuário
95 |
96 |
97 |
98 |
99 |
100 | {isLoading ? (
101 |
102 |
103 |
104 | ) : error ? (
105 |
106 | Falha ao obter dados dos usuários
107 |
108 | ) : (
109 | <>
110 |
111 |
112 |
113 |
114 |
115 | |
116 | Usuário |
117 | {isWideVersion && Data de cadastro | }
118 | |
119 |
120 |
121 |
122 | {data.users.map(user => (
123 |
124 |
125 |
126 | |
127 |
128 |
129 | handlePrefetchUser(user.id)}
132 | >
133 | {user.name}
134 |
135 |
136 | {user.email}
137 |
138 |
139 | |
140 | {isWideVersion && {user.created_at} | }
141 |
142 | {isWideVersion && (
143 | }
149 | >
150 | Editar
151 |
152 | )}
153 | |
154 |
155 | ))}
156 |
157 |
158 |
159 |
164 | >
165 | )}
166 |
167 |
168 |
169 | );
170 | }
171 |
--------------------------------------------------------------------------------
/src/services/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const api = axios.create({
4 | baseURL: '/api',
5 | });
6 |
7 | export { api };
8 |
--------------------------------------------------------------------------------
/src/services/hooks/userUsers.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from 'react-query';
2 | import { api } from '../api';
3 |
4 | type User = {
5 | id: string;
6 | name: string;
7 | email: string;
8 | created_at: string;
9 | };
10 |
11 | type GetUsersResponse = {
12 | users: User[];
13 | totalCount: number;
14 | };
15 |
16 | const TEN_MINUTES_IN_MILLISECONDS = 1000 * 60 * 10;
17 |
18 | async function getUsers(page: number): Promise {
19 | const { data, headers } = await api.get<{ users: User[] }>('users', {
20 | params: { page },
21 | });
22 |
23 | const totalCount = Number(headers['x-total-count']);
24 |
25 | const users = data.users.map(user => ({
26 | id: user.id,
27 | name: user.name,
28 | email: user.email,
29 | created_at: new Date(user.created_at).toLocaleDateString('pt-BR', {
30 | day: '2-digit',
31 | month: 'long',
32 | year: 'numeric',
33 | }),
34 | }));
35 |
36 | return { users, totalCount };
37 | }
38 |
39 | type UserUsersOptions = {
40 | initialData: {
41 | users: User[];
42 | totalCount: number;
43 | };
44 | };
45 |
46 | function useUsers(page: number, options?: UserUsersOptions) {
47 | return useQuery(['users', { page }], () => getUsers(page), {
48 | staleTime: TEN_MINUTES_IN_MILLISECONDS,
49 | ...options,
50 | });
51 | }
52 |
53 | export { useUsers, getUsers };
54 | export type { User };
55 |
--------------------------------------------------------------------------------
/src/services/mirage/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createServer,
3 | Factory,
4 | Model,
5 | Response,
6 | ActiveModelSerializer,
7 | } from 'miragejs';
8 | import faker from 'faker';
9 |
10 | type User = {
11 | name: string;
12 | email: string;
13 | created_at: string;
14 | };
15 |
16 | function makeServer() {
17 | const server = createServer({
18 | serializers: {
19 | application: ActiveModelSerializer,
20 | },
21 |
22 | models: {
23 | user: Model.extend>({}),
24 | },
25 |
26 | factories: {
27 | user: Factory.extend({
28 | name() {
29 | return faker.name.findName();
30 | },
31 | email() {
32 | return faker.internet.email().toLocaleLowerCase();
33 | },
34 | created_at() {
35 | return faker.date.recent(10);
36 | },
37 | }),
38 | },
39 |
40 | seeds(server) {
41 | server.createList('user', 20);
42 | },
43 |
44 | routes() {
45 | this.namespace = 'api';
46 | this.timing = 750;
47 |
48 | this.get('/users', function (schema, request) {
49 | const { page = 1, per_page = 10 } = request.queryParams;
50 |
51 | const pageAsNumber = Number(page);
52 | const perPageAsNumber = Number(per_page);
53 |
54 | const total = schema.all('user').length;
55 |
56 | const pageStart = (pageAsNumber - 1) * perPageAsNumber;
57 | const pageEnd = pageStart + perPageAsNumber;
58 |
59 | const users = this.serialize(schema.all('user')).users.slice(
60 | pageStart,
61 | pageEnd,
62 | );
63 |
64 | return new Response(200, { 'x-total-count': String(total) }, { users });
65 | });
66 | this.get('/users/:id');
67 | this.post('/users');
68 |
69 | this.namespace = '';
70 | this.passthrough();
71 | },
72 | });
73 |
74 | return server;
75 | }
76 |
77 | export { makeServer };
78 |
--------------------------------------------------------------------------------
/src/services/queryClient.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from 'react-query';
2 |
3 | const queryClient = new QueryClient();
4 |
5 | export { queryClient };
6 |
--------------------------------------------------------------------------------
/src/styles/theme.ts:
--------------------------------------------------------------------------------
1 | import { extendTheme } from '@chakra-ui/react';
2 |
3 | export const theme = extendTheme({
4 | colors: {
5 | gray: {
6 | '900': '#181B23',
7 | '800': '#1F2029',
8 | '700': '#353646',
9 | '600': '#4B4D63',
10 | '500': '#616480',
11 | '400': '#797D9A',
12 | '300': '#9699B0',
13 | '200': '#B3B5C6',
14 | '100': '#D1D2DC',
15 | '50': '#EEEEF2',
16 | },
17 | },
18 | fonts: {
19 | heading: 'Roboto',
20 | body: 'Roboto',
21 | },
22 | styles: {
23 | global: {
24 | body: {
25 | bg: 'gray.900',
26 | color: 'gray.50',
27 | },
28 | },
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve"
20 | },
21 | "include": [
22 | "next-env.d.ts",
23 | "**/*.ts",
24 | "**/*.tsx"
25 | ],
26 | "exclude": [
27 | "node_modules"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------