├── .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 |
3 |
4 |
5 |
6 |
7 |
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 |
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 |
6 |
--------------------------------------------------------------------------------
/src/assets/income.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/assets/outcome.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/total.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
53 |
54 | R$ 5.000,00
55 |
56 |
57 |
58 | Saídas
59 |
60 |
61 | R$ 1.000,00
62 |
63 |
64 |
65 | Total
66 |
67 |
68 | R$ 4000,00
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Título |
77 | Preço |
78 | Categoria |
79 | Data |
80 |
81 |
82 |
83 |
84 |
85 | Computer |
86 | R$ 5.000,00 |
87 | Sell |
88 | 20/04/2020 |
89 |
90 |
91 | Website Hosting |
92 | - R$ 1.000,00 |
93 | Hosting |
94 | 19/04/2020 |
95 |
96 |
97 |
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 |
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 |
--------------------------------------------------------------------------------