├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── README.md ├── config-overrides.js ├── jest.config.js ├── package.json ├── prettier.config.js ├── public ├── index.html └── robots.txt ├── src ├── App.tsx ├── __tests__ │ └── App.tsx ├── assets │ ├── alert.svg │ ├── income.svg │ ├── logo.svg │ ├── outcome.svg │ └── total.svg ├── components │ ├── FileList │ │ ├── index.tsx │ │ └── styles.ts │ ├── Header │ │ ├── index.tsx │ │ └── styles.ts │ └── Upload │ │ ├── index.tsx │ │ └── styles.ts ├── index.tsx ├── pages │ ├── Dashboard │ │ ├── index.tsx │ │ └── styles.ts │ └── Import │ │ ├── index.tsx │ │ └── styles.ts ├── react-app-env.d.ts ├── routes │ └── index.tsx ├── services │ └── api.ts ├── setupTests.ts ├── styles │ └── global.ts └── utils │ └── formatValue.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.js 2 | node_modules 3 | build 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "plugin:react/recommended", 9 | "airbnb", 10 | "plugin:@typescript-eslint/recommended", 11 | "prettier/@typescript-eslint", 12 | "plugin:prettier/recommended" 13 | ], 14 | "globals": { 15 | "Atomics": "readonly", 16 | "SharedArrayBuffer": "readonly" 17 | }, 18 | "parser": "@typescript-eslint/parser", 19 | "parserOptions": { 20 | "ecmaFeatures": { 21 | "jsx": true 22 | }, 23 | "ecmaVersion": 2018, 24 | "sourceType": "module" 25 | }, 26 | "plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"], 27 | "rules": { 28 | "react/jsx-props-no-spreading": "off", 29 | "prettier/prettier": "error", 30 | "react-hooks/rules-of-hooks": "error", 31 | "react-hooks/exhaustive-deps": "warn", 32 | "react/jsx-filename-extension": [1, { "extensions": [".tsx"] }], 33 | "import/prefer-default-export": "off", 34 | "@typescript-eslint/explicit-function-return-type": [ 35 | "error", 36 | { 37 | "allowExpressions": true 38 | } 39 | ], 40 | "import/extensions": [ 41 | "error", 42 | "ignorePackages", 43 | { 44 | "ts": "never", 45 | "tsx": "never" 46 | } 47 | ] 48 | }, 49 | "settings": { 50 | "import/resolver": { 51 | "typescript": {} 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Rocketseat Education 3 |

4 | 5 |

6 | Rocketseat Project 7 | License 8 |

9 | 10 | 11 | ## 💻 Projeto 12 | 13 | gostack-template-fundamentos-reactjs 14 | 15 | ## 📝 Licença 16 | 17 | Esse projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes. 18 | 19 | --- 20 | 21 |

22 | Feito com 💜 by Rocketseat 23 |

24 | 25 | 26 | 27 | 28 |
29 |
30 | 31 |

32 | 33 | banner 34 | 35 |

36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const jestConfig = require('./jest.config'); 2 | 3 | module.exports = { 4 | jest(config) { 5 | config.preset = jestConfig.preset; 6 | config.reporters = jestConfig.reporters; 7 | return config; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "desafio-07", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.2", 7 | "filesize": "^6.1.0", 8 | "history": "^4.10.1", 9 | "polished": "^3.5.2", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-dropzone": "^10.2.2", 13 | "react-router-dom": "^5.1.2", 14 | "react-scripts": "3.4.1", 15 | "styled-components": "^5.1.0", 16 | "typescript": "~3.7.2" 17 | }, 18 | "scripts": { 19 | "start": "react-app-rewired start", 20 | "build": "react-app-rewired build", 21 | "test": "react-app-rewired test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | }, 36 | "devDependencies": { 37 | "@testing-library/jest-dom": "^4.2.4", 38 | "@testing-library/react": "^9.3.2", 39 | "@testing-library/user-event": "^7.1.2", 40 | "@types/axios": "^0.14.0", 41 | "@types/jest": "^24.0.0", 42 | "@types/node": "^12.0.0", 43 | "@types/react": "^16.9.0", 44 | "@types/react-dom": "^16.9.0", 45 | "@types/react-router-dom": "^5.1.4", 46 | "@types/styled-components": "^5.1.0", 47 | "@typescript-eslint/eslint-plugin": "^2.28.0", 48 | "@typescript-eslint/parser": "^2.28.0", 49 | "axios-mock-adapter": "^1.18.1", 50 | "eslint": "^6.8.0", 51 | "eslint-config-airbnb": "^18.1.0", 52 | "eslint-config-prettier": "^6.10.1", 53 | "eslint-import-resolver-typescript": "^2.0.0", 54 | "eslint-plugin-import": "^2.20.1", 55 | "eslint-plugin-jsx-a11y": "^6.2.3", 56 | "eslint-plugin-prettier": "^3.1.3", 57 | "eslint-plugin-react": "^7.19.0", 58 | "eslint-plugin-react-hooks": "^2.5.0", 59 | "prettier": "^2.0.4", 60 | "react-app-rewired": "^2.1.5", 61 | "ts-jest": "^25.4.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | arrowParens: 'avoid', 5 | }; 6 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React App 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | 4 | import Routes from './routes'; 5 | 6 | import GlobalStyle from './styles/global'; 7 | 8 | const App: React.FC = () => ( 9 | <> 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /src/__tests__/App.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | /* eslint-disable import/first */ 3 | 4 | jest.mock('../utils/formatValue.ts', () => ({ 5 | __esModule: true, 6 | default: jest.fn().mockImplementation((value: number) => { 7 | switch (value) { 8 | case 6000: 9 | return 'R$ 6.000,00'; 10 | case 50: 11 | return 'R$ 50,00'; 12 | case 5950: 13 | return 'R$ 5.950,00'; 14 | case 1500: 15 | return 'R$ 1.500,00'; 16 | case 4500: 17 | return 'R$ 4.500,00'; 18 | default: 19 | return ''; 20 | } 21 | }), 22 | })); 23 | 24 | import React from 'react'; 25 | import { render, fireEvent, act } from '@testing-library/react'; 26 | import MockAdapter from 'axios-mock-adapter'; 27 | import api from '../services/api'; 28 | import App from '../App'; 29 | 30 | const apiMock = new MockAdapter(api); 31 | 32 | const wait = (amount = 0): Promise => { 33 | return new Promise((resolve) => setTimeout(resolve, amount)); 34 | }; 35 | 36 | const actWait = async (amount = 0): Promise => { 37 | await act(async () => { 38 | await wait(amount); 39 | }); 40 | }; 41 | 42 | describe('Dashboard', () => { 43 | it('should be able to list the total balance inside the cards', async () => { 44 | const { getByTestId } = render(); 45 | 46 | apiMock.onGet('transactions').reply(200, { 47 | transactions: [ 48 | { 49 | id: '807da2da-4ba6-4e45-b4f8-828d900c2adf', 50 | title: 'Loan', 51 | type: 'income', 52 | value: 1500, 53 | category: { 54 | id: '12a0cff7-8691-456d-b1ad-172d777f1942', 55 | title: 'Others', 56 | created_at: '2020-04-17T19:05:34.000Z', 57 | updated_at: '2020-04-17T19:05:34.000Z', 58 | }, 59 | category_id: '12a0cff7-8691-456d-b1ad-172d777f1942', 60 | created_at: '2020-04-17T19:05:34.000Z', 61 | updated_at: '2020-04-17T19:05:34.000Z', 62 | }, 63 | { 64 | id: '3cd3b0e3-73ef-44e9-9f19-8d815eaa7bb4', 65 | title: 'Computer', 66 | type: 'income', 67 | value: 4500, 68 | category: { 69 | id: '12a0cff7-8691-456d-b1ad-172d777f1942', 70 | title: 'Sell', 71 | created_at: '2020-04-18T19:05:34.000Z', 72 | updated_at: '2020-04-17T19:05:34.000Z', 73 | }, 74 | category_id: '12a0cff7-8691-456d-b1ad-172d777f1942', 75 | created_at: '2020-04-18T19:05:34.000Z', 76 | updated_at: '2020-04-18T19:05:34.000Z', 77 | }, 78 | { 79 | id: 'fb21571c-1087-4427-800c-3c30a484decf', 80 | title: 'Website Hosting', 81 | type: 'outcome', 82 | value: 50, 83 | category: { 84 | id: '12a0cff7-8691-456d-b1ad-172d777f1942', 85 | title: 'Hosting', 86 | created_at: '2020-04-17T19:05:34.000Z', 87 | updated_at: '2020-04-17T19:05:34.000Z', 88 | }, 89 | category_id: '12a0cff7-8691-456d-b1ad-172d777f1942', 90 | created_at: '2020-04-19T19:05:34.000Z', 91 | updated_at: '2020-04-19T19:05:34.000Z', 92 | }, 93 | ], 94 | balance: { 95 | income: 6000, 96 | outcome: 50, 97 | total: 5950, 98 | }, 99 | }); 100 | 101 | await actWait(); 102 | 103 | expect(getByTestId('balance-income')).toHaveTextContent('R$ 6.000,00'); 104 | 105 | expect(getByTestId('balance-outcome')).toHaveTextContent('R$ 50,00'); 106 | 107 | expect(getByTestId('balance-total')).toHaveTextContent('R$ 5.950,00'); 108 | }); 109 | 110 | it('should be able to list the transactions', async () => { 111 | const { getByText } = render(); 112 | 113 | apiMock.onGet('transactions').reply(200, { 114 | transactions: [ 115 | { 116 | id: '807da2da-4ba6-4e45-b4f8-828d900c2adf', 117 | title: 'Loan', 118 | type: 'income', 119 | value: 1500, 120 | category: { 121 | id: '12a0cff7-8691-456d-b1ad-172d777f1942', 122 | title: 'Others', 123 | created_at: '2020-04-17T19:05:34.000Z', 124 | updated_at: '2020-04-17T19:05:34.000Z', 125 | }, 126 | category_id: '12a0cff7-8691-456d-b1ad-172d777f1942', 127 | created_at: '2020-04-17T19:05:34.000Z', 128 | updated_at: '2020-04-17T19:05:34.000Z', 129 | }, 130 | { 131 | id: '3cd3b0e3-73ef-44e9-9f19-8d815eaa7bb4', 132 | title: 'Computer', 133 | type: 'income', 134 | value: 4500, 135 | category: { 136 | id: '12a0cff7-8691-456d-b1ad-172d777f1942', 137 | title: 'Sell', 138 | created_at: '2020-04-18T19:05:34.000Z', 139 | updated_at: '2020-04-17T19:05:34.000Z', 140 | }, 141 | category_id: '12a0cff7-8691-456d-b1ad-172d777f1942', 142 | created_at: '2020-04-18T19:05:34.000Z', 143 | updated_at: '2020-04-18T19:05:34.000Z', 144 | }, 145 | { 146 | id: 'fb21571c-1087-4427-800c-3c30a484decf', 147 | title: 'Website Hosting', 148 | type: 'outcome', 149 | value: 50, 150 | category: { 151 | id: '12a0cff7-8691-456d-b1ad-172d777f1942', 152 | title: 'Hosting', 153 | created_at: '2020-04-17T19:05:34.000Z', 154 | updated_at: '2020-04-17T19:05:34.000Z', 155 | }, 156 | category_id: '12a0cff7-8691-456d-b1ad-172d777f1942', 157 | created_at: '2020-04-19T19:05:34.000Z', 158 | updated_at: '2020-04-19T19:05:34.000Z', 159 | }, 160 | ], 161 | balance: { 162 | income: 6000, 163 | outcome: 50, 164 | total: 5950, 165 | }, 166 | }); 167 | 168 | await actWait(); 169 | 170 | expect(getByText('Loan')).toBeTruthy(); 171 | expect(getByText('R$ 1.500,00')).toBeTruthy(); 172 | expect(getByText('Others')).toBeTruthy(); 173 | 174 | expect(getByText('Computer')).toBeTruthy(); 175 | expect(getByText('R$ 4.500,00')).toBeTruthy(); 176 | expect(getByText('Sell')).toBeTruthy(); 177 | 178 | expect(getByText('Website Hosting')).toBeTruthy(); 179 | expect(getByText('- R$ 50,00')).toBeTruthy(); 180 | expect(getByText('Hosting')).toBeTruthy(); 181 | }); 182 | 183 | it('should be able to navigate to the import page', async () => { 184 | const { getByText } = render(); 185 | 186 | await actWait(500); 187 | 188 | fireEvent.click(getByText('Importar')); 189 | 190 | await actWait(); 191 | 192 | expect(window.location.pathname).toEqual('/import'); 193 | }); 194 | 195 | test('should be able to upload a file', async () => { 196 | const { getByText, getByTestId } = render(); 197 | 198 | fireEvent.click(getByText('Importar')); 199 | 200 | await actWait(); 201 | 202 | const input = getByTestId('upload'); 203 | 204 | const file = new File( 205 | [ 206 | 'title, type, value, category\ 207 | Loan, income, 1500, Others\ 208 | Website Hosting, outcome, 50, Others\ 209 | Ice cream, outcome, 3, Food', 210 | ], 211 | 'import.csv', 212 | { 213 | type: 'text/csv', 214 | }, 215 | ); 216 | 217 | Object.defineProperty(input, 'files', { 218 | value: [file], 219 | }); 220 | 221 | fireEvent.change(input); 222 | 223 | await actWait(); 224 | 225 | expect(getByText('import.csv')).toBeTruthy(); 226 | 227 | await actWait(); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /src/assets/alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/income.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/outcome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/total.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/FileList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Container, FileInfo } from './styles'; 4 | 5 | interface FileProps { 6 | name: string; 7 | readableSize: string; 8 | } 9 | 10 | interface FileListProps { 11 | files: FileProps[]; 12 | } 13 | 14 | const FileList: React.FC = ({ files }: FileListProps) => { 15 | return ( 16 | 17 | {files.map((uploadedFile) => ( 18 |
  • 19 | 20 |
    21 | {uploadedFile.name} 22 | {uploadedFile.readableSize} 23 |
    24 |
    25 |
  • 26 | ))} 27 |
    28 | ); 29 | }; 30 | 31 | export default FileList; 32 | -------------------------------------------------------------------------------- /src/components/FileList/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.ul` 4 | margin-top: 20px; 5 | 6 | li { 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | color: #444; 11 | 12 | & + li { 13 | margin-top: 15px; 14 | } 15 | } 16 | `; 17 | 18 | export const FileInfo = styled.div` 19 | display: flex; 20 | align-items: center; 21 | justify-content: space-between; 22 | flex: 1; 23 | 24 | button { 25 | border: 0; 26 | background: transparent; 27 | color: #e83f5b; 28 | margin-left: 5px; 29 | cursor: pointer; 30 | } 31 | 32 | div { 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: space-between; 36 | 37 | span { 38 | font-size: 12px; 39 | color: #999; 40 | margin-top: 5px; 41 | } 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Link } from 'react-router-dom'; 4 | 5 | import { Container } from './styles'; 6 | 7 | import Logo from '../../assets/logo.svg'; 8 | 9 | interface HeaderProps { 10 | size?: 'small' | 'large'; 11 | } 12 | 13 | const Header: React.FC = ({ size = 'large' }: HeaderProps) => ( 14 | 15 |
    16 | GoFinances 17 | 22 |
    23 |
    24 | ); 25 | 26 | export default Header; 27 | -------------------------------------------------------------------------------- /src/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface ContainerProps { 4 | size?: 'small' | 'large'; 5 | } 6 | 7 | export const Container = styled.div` 8 | background: #5636d3; 9 | padding: 30px 0; 10 | 11 | header { 12 | width: 1120px; 13 | margin: 0 auto; 14 | padding: ${({ size }) => (size === 'small' ? '0 20px ' : '0 20px 150px')}; 15 | display: flex; 16 | align-items: center; 17 | justify-content: space-between; 18 | 19 | nav { 20 | a { 21 | color: #fff; 22 | text-decoration: none; 23 | font-size: 16px; 24 | transition: opacity 0.2s; 25 | 26 | & + a { 27 | margin-left: 32px; 28 | } 29 | 30 | &:hover { 31 | opacity: 0.6; 32 | } 33 | } 34 | } 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /src/components/Upload/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | import Dropzone from 'react-dropzone'; 4 | import { DropContainer, UploadMessage } from './styles'; 5 | 6 | interface UploadProps { 7 | onUpload: Function; 8 | } 9 | 10 | const Upload: React.FC = ({ onUpload }: UploadProps) => { 11 | function renderDragMessage( 12 | isDragActive: boolean, 13 | isDragRejest: boolean, 14 | ): ReactNode { 15 | if (!isDragActive) { 16 | return ( 17 | Selecione ou arraste o arquivo aqui. 18 | ); 19 | } 20 | 21 | if (isDragRejest) { 22 | return Arquivo não suportado; 23 | } 24 | 25 | return Solte o arquivo aqui; 26 | } 27 | 28 | return ( 29 | <> 30 | onUpload(files)}> 31 | {({ getRootProps, getInputProps, isDragActive, isDragReject }): any => ( 32 | 37 | 38 | {renderDragMessage(isDragActive, isDragReject)} 39 | 40 | )} 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default Upload; 47 | -------------------------------------------------------------------------------- /src/components/Upload/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css, FlattenSimpleInterpolation } from 'styled-components'; 2 | 3 | interface UploadProps { 4 | isDragActive: boolean; 5 | isDragReject: boolean; 6 | refKey?: string; 7 | [key: string]: any; 8 | type?: 'error' | 'success' | 'default'; 9 | } 10 | 11 | const dragActive = css` 12 | border-color: #12a454; 13 | `; 14 | 15 | const dragReject = css` 16 | border-color: #e83f5b; 17 | `; 18 | 19 | export const DropContainer = styled.div.attrs({ 20 | className: 'dropzone', 21 | })` 22 | border: 1.5px dashed #969cb3; 23 | border-radius: 5px; 24 | cursor: pointer; 25 | 26 | transition: height 0.2s ease; 27 | 28 | ${(props: UploadProps): false | FlattenSimpleInterpolation => 29 | props.isDragActive && dragActive} 30 | 31 | ${(props: UploadProps): false | FlattenSimpleInterpolation => 32 | props.isDragReject && dragReject} 33 | `; 34 | 35 | const messageColors = { 36 | default: '#5636D3', 37 | error: '#e83f5b', 38 | success: '#12a454', 39 | }; 40 | 41 | export const UploadMessage = styled.p` 42 | display: flex; 43 | font-size: 16px; 44 | line-height: 24px; 45 | padding: 48px 0; 46 | 47 | color: ${({ type }: UploadProps) => messageColors[type || 'default']}; 48 | 49 | justify-content: center; 50 | align-items: center; 51 | `; 52 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root'), 10 | ); 11 | -------------------------------------------------------------------------------- /src/pages/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import income from '../../assets/income.svg'; 4 | import outcome from '../../assets/outcome.svg'; 5 | import total from '../../assets/total.svg'; 6 | 7 | import api from '../../services/api'; 8 | 9 | import Header from '../../components/Header'; 10 | 11 | import formatValue from '../../utils/formatValue'; 12 | 13 | import { Container, CardContainer, Card, TableContainer } from './styles'; 14 | 15 | interface Transaction { 16 | id: string; 17 | title: string; 18 | value: number; 19 | formattedValue: string; 20 | formattedDate: string; 21 | type: 'income' | 'outcome'; 22 | category: { title: string }; 23 | created_at: Date; 24 | } 25 | 26 | interface Balance { 27 | income: string; 28 | outcome: string; 29 | total: string; 30 | } 31 | 32 | const Dashboard: React.FC = () => { 33 | // const [transactions, setTransactions] = useState([]); 34 | // const [balance, setBalance] = useState({} as Balance); 35 | 36 | useEffect(() => { 37 | async function loadTransactions(): Promise { 38 | // TODO 39 | } 40 | 41 | loadTransactions(); 42 | }, []); 43 | 44 | return ( 45 | <> 46 |
    47 | 48 | 49 | 50 |
    51 |

    Entradas

    52 | Income 53 |
    54 |

    R$ 5.000,00

    55 |
    56 | 57 |
    58 |

    Saídas

    59 | Outcome 60 |
    61 |

    R$ 1.000,00

    62 |
    63 | 64 |
    65 |

    Total

    66 | Total 67 |
    68 |

    R$ 4000,00

    69 |
    70 |
    71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
    TítuloPreçoCategoriaData
    ComputerR$ 5.000,00Sell20/04/2020
    Website Hosting- R$ 1.000,00Hosting19/04/2020
    98 |
    99 |
    100 | 101 | ); 102 | }; 103 | 104 | export default Dashboard; 105 | -------------------------------------------------------------------------------- /src/pages/Dashboard/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface CardProps { 4 | total?: boolean; 5 | } 6 | 7 | export const Container = styled.div` 8 | width: 100%; 9 | max-width: 1120px; 10 | margin: 0 auto; 11 | padding: 40px 20px; 12 | `; 13 | 14 | export const Title = styled.h1` 15 | font-size: 48px; 16 | color: #3a3a3a; 17 | `; 18 | 19 | export const CardContainer = styled.section` 20 | display: grid; 21 | grid-template-columns: repeat(3, 1fr); 22 | grid-gap: 32px; 23 | margin-top: -150px; 24 | `; 25 | 26 | export const Card = styled.div` 27 | background: ${({ total }: CardProps): string => (total ? '#FF872C' : '#fff')}; 28 | padding: 22px 32px; 29 | border-radius: 5px; 30 | color: ${({ total }: CardProps): string => (total ? '#fff' : '#363F5F')}; 31 | 32 | header { 33 | display: flex; 34 | align-items: center; 35 | justify-content: space-between; 36 | 37 | p { 38 | font-size: 16px; 39 | } 40 | } 41 | 42 | h1 { 43 | margin-top: 14px; 44 | font-size: 36px; 45 | font-weight: normal; 46 | line-height: 54px; 47 | } 48 | `; 49 | 50 | export const TableContainer = styled.section` 51 | margin-top: 64px; 52 | 53 | table { 54 | width: 100%; 55 | border-spacing: 0 8px; 56 | 57 | th { 58 | color: #969cb3; 59 | font-weight: normal; 60 | padding: 20px 32px; 61 | text-align: left; 62 | font-size: 16px; 63 | line-height: 24px; 64 | } 65 | 66 | td { 67 | padding: 20px 32px; 68 | border: 0; 69 | background: #fff; 70 | font-size: 16px; 71 | font-weight: normal; 72 | color: #969cb3; 73 | 74 | &.title { 75 | color: #363f5f; 76 | } 77 | 78 | &.income { 79 | color: #12a454; 80 | } 81 | 82 | &.outcome { 83 | color: #e83f5b; 84 | } 85 | } 86 | 87 | td:first-child { 88 | border-radius: 8px 0 0 8px; 89 | } 90 | 91 | td:last-child { 92 | border-radius: 0 8px 8px 0; 93 | } 94 | } 95 | `; 96 | -------------------------------------------------------------------------------- /src/pages/Import/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | 4 | import filesize from 'filesize'; 5 | 6 | import Header from '../../components/Header'; 7 | import FileList from '../../components/FileList'; 8 | import Upload from '../../components/Upload'; 9 | 10 | import { Container, Title, ImportFileContainer, Footer } from './styles'; 11 | 12 | import alert from '../../assets/alert.svg'; 13 | import api from '../../services/api'; 14 | 15 | interface FileProps { 16 | file: File; 17 | name: string; 18 | readableSize: string; 19 | } 20 | 21 | const Import: React.FC = () => { 22 | const [uploadedFiles, setUploadedFiles] = useState([]); 23 | const history = useHistory(); 24 | 25 | async function handleUpload(): Promise { 26 | // const data = new FormData(); 27 | 28 | // TODO 29 | 30 | try { 31 | // await api.post('/transactions/import', data); 32 | } catch (err) { 33 | // console.log(err.response.error); 34 | } 35 | } 36 | 37 | function submitFile(files: File[]): void { 38 | // TODO 39 | } 40 | 41 | return ( 42 | <> 43 |
    44 | 45 | Importar uma transação 46 | 47 | 48 | {!!uploadedFiles.length && } 49 | 50 |
    51 |

    52 | Alert 53 | Permitido apenas arquivos CSV 54 |

    55 | 58 |
    59 |
    60 |
    61 | 62 | ); 63 | }; 64 | 65 | export default Import; 66 | -------------------------------------------------------------------------------- /src/pages/Import/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { shade } from 'polished'; 3 | 4 | export const Container = styled.div` 5 | width: 100%; 6 | max-width: 736px; 7 | margin: 0 auto; 8 | padding: 40px 20px; 9 | `; 10 | 11 | export const Title = styled.h1` 12 | font-weight: 500; 13 | font-size: 36px; 14 | line-height: 54px; 15 | color: #363f5f; 16 | text-align: center; 17 | `; 18 | 19 | export const ImportFileContainer = styled.section` 20 | background: #fff; 21 | margin-top: 40px; 22 | border-radius: 5px; 23 | padding: 64px; 24 | `; 25 | 26 | export const Footer = styled.section` 27 | margin-top: 36px; 28 | display: flex; 29 | align-items: center; 30 | justify-content: space-between; 31 | 32 | p { 33 | display: flex; 34 | align-items: center; 35 | font-size: 12px; 36 | line-height: 18px; 37 | color: #969cb3; 38 | 39 | img { 40 | margin-right: 5px; 41 | } 42 | } 43 | 44 | button { 45 | background: #ff872c; 46 | color: #fff; 47 | border-radius: 5px; 48 | padding: 15px 80px; 49 | border: 0; 50 | transition: background-color 0.2s; 51 | 52 | &:hover { 53 | background: ${shade(0.2, '#ff872c')}; 54 | } 55 | } 56 | `; 57 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Switch, Route } from 'react-router-dom'; 4 | 5 | import Dashboard from '../pages/Dashboard'; 6 | import Import from '../pages/Import'; 7 | 8 | const Routes: React.FC = () => ( 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default Routes; 16 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: 'http://localhost:3333', 5 | }); 6 | 7 | export default api; 8 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/styles/global.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | export default createGlobalStyle` 4 | * { 5 | margin: 0; 6 | padding: 0; 7 | outline: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | body { 12 | background: #F0F2F5 ; 13 | -webkit-font-smoothing: antialiased 14 | } 15 | 16 | body, input, button { 17 | font: 16px "Poppins", sans-serif; 18 | } 19 | 20 | button { 21 | cursor: pointer; 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /src/utils/formatValue.ts: -------------------------------------------------------------------------------- 1 | const formatValue = (value: number): string => 2 | Intl.NumberFormat().format(value); // TODO 3 | 4 | export default formatValue; 5 | -------------------------------------------------------------------------------- /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 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------