├── .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 | 2 | 3 | 4 | 5 | 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 | dashgo 3 |

4 | 5 |

6 | Made by 7 | GitHub 8 |

9 | 10 |

11 | Technologies • 12 | License 13 |

14 | 15 |

16 | dashgo Mockup 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 | 85 | 86 | 87 | 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 | 116 | 117 | {isWideVersion && } 118 | 119 | 120 | 121 | 122 | {data.users.map(user => ( 123 | 124 | 127 | 140 | {isWideVersion && } 141 | 154 | 155 | ))} 156 | 157 |
114 | 115 | UsuárioData de cadastro
125 | 126 | 128 | 129 | handlePrefetchUser(user.id)} 132 | > 133 | {user.name} 134 | 135 | 136 | {user.email} 137 | 138 | 139 | {user.created_at} 142 | {isWideVersion && ( 143 | 152 | )} 153 |
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 | --------------------------------------------------------------------------------