├── .editorconfig
├── .gitignore
├── .prettierrc
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── components
│ ├── Dashboard-initial
│ │ ├── Dashboard.tsx
│ │ ├── Widget.tsx
│ │ ├── dashboard.data.ts
│ │ ├── dashboard.model.ts
│ │ └── dashboard.scss
│ └── Dashboard
│ │ ├── Dashboard.tsx
│ │ ├── Widget.tsx
│ │ ├── dashboard.config.ts
│ │ ├── dashboard.model.ts
│ │ ├── styles
│ │ ├── layouts
│ │ │ ├── _dashboard.scss
│ │ │ └── _widget.scss
│ │ ├── main.scss
│ │ ├── position
│ │ │ ├── _full.scss
│ │ │ ├── _half.scss
│ │ │ └── _quarter.scss
│ │ └── templates
│ │ │ ├── _bar-chart.scss
│ │ │ ├── _block.scss
│ │ │ └── _list.scss
│ │ └── templates
│ │ ├── BarChart.tsx
│ │ ├── Block.tsx
│ │ └── List.tsx
├── index.tsx
├── mocks
│ └── dashboard.data.ts
├── react-app-env.d.ts
└── styles
│ └── main.scss
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = false
8 | insert_final_newline = true
9 |
--------------------------------------------------------------------------------
/.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 |
25 | # cache
26 | .eslintcache
27 | node_modules/.cache/.eslintcache
28 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "overrides": [{
3 | "files": "*.tsx",
4 | "options": {
5 | "printWidth": 100,
6 | "bracketSpacing": false,
7 | "jsxBracketSameLine": true,
8 | "singleQuote": true,
9 | "trailingComma": "all",
10 | "tabWidth": 2,
11 | "useTabs": true,
12 | "arrowParens": "always"
13 | }
14 | }]
15 | }
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Criando um dashboard dinâmico e reutilizável com SOLID + React + TypeScript
4 |
5 | Desenvolver usando React é fácil. Dividir os componentes também é fácil. Fazer isso para que seja fácil the ler e entender, reutilizável, dinâmico e fácil de dar manutenção... É outra conversa.
6 |
7 | Nesse exemplo entramos de cabeça no submundo dos componentes reutilizáveis e mostramos como fazer isso seguindo os pricípios SOLID.
8 |
9 | ## O que faz esse componente?
10 |
11 | Cria multiplos dashboards, de maneira dinâmica, e de acordo com os dados que você passa para o componente ele vai criar widgets dinâmicos que podem ter posições diferentes, templates diferentes, tamanhos diferentes e cores diferentes. E tudo de maneira simples para que você precise editar o mínimo possível os arquivos principais do projeto.
12 |
13 | ## Como instalar e rodar o projeto?
14 |
15 | 1. `yarn`: para instalar após clonar o repositório.
16 | 2. `yarn start` após installar o projeto.
17 |
18 | ## Como eu instalei o projeto?
19 |
20 | Utilizei o `create-react-app` padrão recomendado na [documentação oficial](https://create-react-app.dev/docs/adding-typescript/).
21 | ```
22 | npx create-react-app my-app --template typescript
23 | ```
24 | or
25 | ```
26 | yarn create react-app my-app --template typescript
27 | ```
28 |
29 | ## Organização dos arquivos
30 |
31 | - `src/components/Dashboard-initial`: versão inicial do componente.
32 | - `src/components/Dashboard`: versão final do component.
33 |
34 |
35 | ## Pequena melhoria no código após gravação do vídeo
36 |
37 | Separei a configuração das posições no CSS. Antes estava tudo dentro do arquivo `src/components/Dashboard/styles/layouts/_widget.scss` mas como os próprios princípios SOLID sugerem, devemos dar independência aos elementes/modificadores e separar eles do código principal. Então agora você encontra eles dentro da pasta `src/components/Dashboard/styles/position`.
38 |
39 | ## Expert
40 |
41 | | [
](https://github.com/rafaelperozin) |
42 | | :-----------------------------------------------------------------------------------------------------------------: |
43 | | [Rafael Perozin](https://github.com/rafaelperozin) |
44 |
45 |
46 | ## Informações Complementares
47 |
48 | Artigo completo no Linkedin com mais detalhes sobre os principios SOLID usados com React Functional (em Inglês):
49 |
50 | https://www.linkedin.com/pulse/reusable-share-button-using-solid-principles-react-rafael-perozin/
51 |
52 | Canal no Youtube onde o Rafael Perozin vai além do código e fala de soft skills e tudo que um desenvolvedor precisa para se diferenciar no mercado e se tornar um profissional de sucesso.:
53 |
54 | https://www.youtube.com/c/rafaelperozin
55 |
56 | ## Configurações usadas no projeto
57 |
58 | Você pode ver tudo nos arquivos que estão na raiz do projeto.
59 | - `.prettierrc`
60 | - `.tsconfig.json`
61 | - `.editorconfig`
62 | - `package.json`
63 |
64 |
65 | ### Customização do meu VSCode:
66 | ```
67 | {
68 | "editor.renderWhitespace": "selection",
69 | "editor.renderControlCharacters": true,
70 | "files.insertFinalNewline": true,
71 | "window.zoomLevel": 0,
72 | "editor.fontSize": 13,
73 | "explorer.confirmDelete": false,
74 | "explorer.confirmDragAndDrop": false,
75 | "editor.insertSpaces": true,
76 | "editor.tabSize": 2,
77 | "editor.detectIndentation": true,
78 | "html.format.wrapLineLength": 140,
79 | "[jsonc]": {
80 | "editor.defaultFormatter": "HookyQR.beautify"
81 | },
82 | "[javascript]": {
83 | "editor.defaultFormatter": "vscode.typescript-language-features"
84 | },
85 | "[javascriptreact]": {
86 | "editor.defaultFormatter": "esbenp.prettier-vscode"
87 | },
88 | "[xml]": {
89 | "editor.defaultFormatter": "mikeburgh.xml-format"
90 | },
91 | "workbench.startupEditor": "newUntitledFile",
92 | "[json]": {
93 | "editor.defaultFormatter": "HookyQR.beautify"
94 | },
95 | "better-comments.multilineComments": true,
96 | "better-comments.highlightPlainText": false,
97 | "better-comments.tags": [
98 | {
99 | "tag": "fixme:",
100 | "color": "#FF2D00",
101 | "strikethrough": false,
102 | "backgroundColor": "transparent"
103 | },
104 | {
105 | "tag": "note:",
106 | "color": "#3498DB",
107 | "strikethrough": false,
108 | "backgroundColor": "transparent"
109 | },
110 | {
111 | "tag": "todo:",
112 | "color": "#FF8C00",
113 | "strikethrough": false,
114 | "backgroundColor": "transparent"
115 | },
116 | {
117 | "tag": "important:",
118 | "color": "#ffd500",
119 | "strikethrough": false,
120 | "backgroundColor": "transparent"
121 | }
122 | ],
123 | "eslint.validate": [
124 | "vue",
125 | "html",
126 | "pug",
127 | "javascript",
128 | "javascriptreact",
129 | "typescript",
130 | "typescriptreact"
131 | ],
132 | "[html]": {
133 | "editor.defaultFormatter": "HookyQR.beautify"
134 | },
135 | "[scss]": {
136 | "editor.defaultFormatter": "HookyQR.beautify"
137 | },
138 | "javascript.preferences.importModuleSpecifier": "non-relative",
139 | "javascript.preferences.quoteStyle": "single",
140 | "typescript.preferences.quoteStyle": "single",
141 | "typescript.preferences.importModuleSpecifier": "non-relative",
142 | "terminal.external.osxExec": "/Applications/iTerm.app",
143 | "terminal.integrated.fontFamily": "Meslo LG S DZ for Powerline",
144 | "terminal.explorerKind": "external",
145 | "workbench.colorCustomizations": {
146 | "[Min Dark]": {
147 | "sideBar.foreground": "#b6b6b6",
148 | },
149 | "[One Dark Pro]": {
150 | "sideBar.background": "#181818",
151 | "sideBarSectionHeader.background": "#141414",
152 | "activityBar.activeBackground": "#141414",
153 | "activityBar.background": "#181818",
154 | "editor.background": "#181818",
155 | "tab.hoverBackground": "#141414",
156 | "tab.activeBackground": "#141414",
157 | "tab.inactiveBackground": "#181818",
158 | "statusBar.background": "#141414",
159 | "editorGroupHeader.tabsBackground": "#181818",
160 | },
161 | },
162 | "editor.tokenColorCustomizations": {
163 | "textMateRules": [
164 | {
165 | "scope": "entity.other.attribute-name.class.pug",
166 | "settings": {
167 | "foreground": "#666CCC"
168 | }
169 | }
170 | ],
171 | },
172 | "[markdown]": {
173 | "editor.defaultFormatter": "yzhang.markdown-all-in-one"
174 | },
175 | "workbench.iconTheme": "material-icon-theme",
176 | "editor.formatOnPaste": true,
177 | "editor.formatOnType": true,
178 | "editor.formatOnSave": true,
179 | "editor.codeActionsOnSave": {
180 | "source.fixAll.eslint": true
181 | },
182 | "vetur.completion.autoImport": true,
183 | "editor.suggestSelection": "first",
184 | "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
185 | "java.configuration.checkProjectSettingsExclusions": false,
186 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false,
187 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false,
188 | "maven.pomfile.autoUpdateEffectivePOM": true,
189 | "vsicons.dontShowNewVersionMessage": true,
190 | "markdown.extension.print.imgToBase64": true,
191 | "files.associations": {
192 | ".prettierrc": "jsonc",
193 | ".bundleignore": "ignore",
194 | "*.conf": "nginx",
195 | ".autorc": "json"
196 | },
197 | "editor.largeFileOptimizations": false,
198 | "[typescript]": {
199 | "editor.defaultFormatter": "esbenp.prettier-vscode"
200 | },
201 | "typescript.updateImportsOnFileMove.enabled": "never",
202 | "[vue]": {
203 | "editor.defaultFormatter": "octref.vetur"
204 | },
205 | "editor.multiCursorModifier": "alt",
206 | "explorer.compactFolders": false,
207 | "maven.executable.path": "/Library/Java/Maven/bin/mvn",
208 | "[typescriptreact]": {
209 | "editor.defaultFormatter": "esbenp.prettier-vscode"
210 | },
211 | "tabnine.experimentalAutoImports": true,
212 | "[css]": {
213 | "editor.defaultFormatter": "HookyQR.beautify"
214 | },
215 | "typescript.enablePromptUseWorkspaceTsdk": true,
216 | "typescript.implementationsCodeLens.enabled": true,
217 | "javascript.suggest.completeFunctionCalls": true,
218 | "workbench.colorTheme": "One Dark Pro",
219 | "path-intellisense.extensionOnImport": true,
220 | "eslint.probe": [
221 | "javascript",
222 | "javascriptreact",
223 | "typescript",
224 | "typescriptreact",
225 | "html",
226 | "vue",
227 | "markdown",
228 | "vetur"
229 | ],
230 | "multiCommand.commands": [
231 | {
232 | "command": "multiCommand.formatTypescript",
233 | "interval": 10,
234 | "sequence": [
235 | "editor.action.organizeImports",
236 | "editor.action.formatDocument",
237 | "eslint.executeAutofix",
238 | "eslint.executeAutofix",
239 | ]
240 | },
241 | ],
242 | "sync.gist": "a90dc89436151831fd1bf8629080ade3",
243 | "sync.quietSync": true,
244 | "material-icon-theme.showWelcomeMessage": false,
245 | "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m -javaagent:\"/Users/rafaelperozin/.vscode/extensions/gabrielbb.vscode-lombok-1.0.1/server/lombok.jar\"",
246 | "prettier.requireConfig": true,
247 | "prettier.singleQuote": true,
248 | "prettier.printWidth": 100,
249 | "prettier.bracketSpacing": false,
250 | "prettier.jsxBracketSameLine": true,
251 | "prettier.trailingComma": "all",
252 | "prettier.tabWidth": 2,
253 | "prettier.useTabs": true,
254 | "prettier.arrowParens": "always",
255 | "auto-close-tag.activationOnLanguage": [
256 | "xml",
257 | "php",
258 | "blade",
259 | "ejs",
260 | "jinja",
261 | "javascript",
262 | "javascriptreact",
263 | "typescript",
264 | "typescriptreact",
265 | "plaintext",
266 | "markdown",
267 | "vue",
268 | "liquid",
269 | "erb",
270 | "lang-cfml",
271 | "cfml",
272 | "HTML (EEx)",
273 | "HTML (Eex)",
274 | "plist"
275 | ],
276 | "eslint.trace.server": "off",
277 | "sync.forceUpload": true,
278 | "bracket-pair-colorizer-2.activeScopeCSS": [
279 | "borderStyle : solid",
280 | "borderWidth : 1px",
281 | "borderColor : {color}",
282 | "opacity: 0.5"
283 | ]
284 | }
285 |
286 | ```
287 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "@types/jest": "^26.0.15",
10 | "@types/node": "^12.0.0",
11 | "@types/react": "^17.0.0",
12 | "@types/react-dom": "^17.0.0",
13 | "react": "^17.0.2",
14 | "react-dom": "^17.0.2",
15 | "react-scripts": "4.0.3",
16 | "rumble-charts": "^3.1.2",
17 | "sass": "^1.32.8",
18 | "typescript": "^4.1.2",
19 | "web-vitals": "^1.0.1"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": [
29 | "react-app",
30 | "react-app/jest"
31 | ]
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-creators-program/dashboard-dinamico-reutilizavel-solid-react-typescript/952b022ab6cbaae8b42219e5c601769e6a3411cf/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-creators-program/dashboard-dinamico-reutilizavel-solid-react-typescript/952b022ab6cbaae8b42219e5c601769e6a3411cf/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-creators-program/dashboard-dinamico-reutilizavel-solid-react-typescript/952b022ab6cbaae8b42219e5c601769e6a3411cf/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/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 {Dashboard} from 'src/components/Dashboard/Dashboard';
3 | import {widgets} from 'src/mocks/dashboard.data';
4 |
5 | import './styles/main.scss';
6 |
7 | const App = () => {
8 | return (
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | export default App;
16 |
--------------------------------------------------------------------------------
/src/components/Dashboard-initial/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {content} from 'src/components/Dashboard-initial/dashboard.data';
3 | import {Widget} from 'src/components/Dashboard-initial/Widget';
4 |
5 | import './dashboard.scss';
6 |
7 | export const Dashboard = () => {
8 | return (
9 |
10 |
17 |
24 |
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/Dashboard-initial/Widget.tsx:
--------------------------------------------------------------------------------
1 | import React, {ReactNode} from 'react';
2 | // @ts-ignore
3 | import {Chart, Layer, Bars, Ticks} from 'rumble-charts';
4 | import {Content} from 'src/components/Dashboard-initial/dashboard.model';
5 |
6 | export interface WidgetProps {
7 | data: Content;
8 | type: 'COMPARISON' | 'METRIC' | 'FUNNEL';
9 | position: 'FULL' | 'HALF' | 'QUARTER';
10 | template: 'LIST' | 'PIE CHART' | 'BAR CHART' | 'BLOCK';
11 | className?: string;
12 | }
13 |
14 | export const Widget = ({data, type, position, template, className}: WidgetProps) => {
15 | const showContent = (): ReactNode => {
16 | if (template === 'LIST') {
17 | return (
18 |
19 | {data.items?.map((item: any) => (
20 | - {`${item.title} - ${item.value}`}
21 | ))}
22 |
23 | );
24 | }
25 | if (template === 'BLOCK') {
26 | return (
27 | <>
28 | {data.value}
29 | {`${data.prev} - ${data.current}`}
30 | >
31 | );
32 | }
33 | if (template === 'BAR CHART') {
34 | return (
35 |
36 |
37 |
38 | label}
45 | />
46 |
47 |
48 |
49 |
{data.current}
50 |
51 | );
52 | }
53 | return Template incompatível
;
54 | };
55 |
56 | return (
57 |
58 |
{data.title}
59 | {showContent()}
60 |
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/src/components/Dashboard-initial/dashboard.data.ts:
--------------------------------------------------------------------------------
1 | import { DataFormat } from "src/components/Dashboard-initial/dashboard.model";
2 |
3 | export const content: DataFormat = {
4 | widgets: [
5 | {
6 | title: "Numero de Pedidos",
7 | value: 35698,
8 | prev: "2019",
9 | current: "2020",
10 | },
11 | {
12 | title: "Lista de Pedidos",
13 | current: "abr/2021",
14 | items: [
15 | {
16 | title: "Pedro",
17 | value: "R$ 32,50",
18 | },
19 | {
20 | title: "Rafael",
21 | value: "R$ 54,95",
22 | },
23 | {
24 | title: "Ingrid",
25 | value: "R$ 12,00",
26 | },
27 | ],
28 | },
29 | {
30 | title: "Valor Total de Pedidos",
31 | current: "jan-mar de 2021",
32 | items: [
33 | {
34 | name: "Jan",
35 | data: [15000],
36 | },
37 | {
38 | name: "Fev",
39 | data: [12000],
40 | },
41 | {
42 | name: "Mar",
43 | data: [25000],
44 | },
45 | ],
46 | },
47 | ],
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/Dashboard-initial/dashboard.model.ts:
--------------------------------------------------------------------------------
1 | export interface List {
2 | title: string;
3 | value: string;
4 | }
5 |
6 | export type BarChart = {
7 | name: string;
8 | data: [number, number] | number[];
9 | };
10 |
11 | export interface Content {
12 | title: string;
13 | value?: number;
14 | prev?: string;
15 | current?: string;
16 | items?: List[] | BarChart[];
17 | }
18 |
19 | export interface DataFormat {
20 | widgets: Content[];
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Dashboard-initial/dashboard.scss:
--------------------------------------------------------------------------------
1 | .dashboard {
2 | display: flex;
3 | justify-items: center;
4 | flex-wrap: wrap;
5 |
6 | &__widget {
7 | margin: .5rem;
8 | border-radius: 3px;
9 | padding: 1rem;
10 |
11 | &--full {
12 | flex-basis: 100%;
13 | background-color: #76D7C4;
14 | }
15 |
16 | &--half {
17 | flex-basis: 43%;
18 | background-color: #85C1E9;
19 | }
20 |
21 | &--quarter {
22 | flex-basis: 20%;
23 | background-color: #D2B4DE;
24 | }
25 |
26 | .widget__title {
27 | margin-top: 0;
28 | }
29 |
30 | .widget__value {
31 | font-size: 2rem;
32 | font-weight: 700;
33 | }
34 |
35 | .widget__info {
36 | font-size: .8rem;
37 | font-weight: 300;
38 | font-style: italic;
39 | opacity: .75;
40 | }
41 |
42 | .widget__chart {
43 | font-size: .8rem;
44 |
45 | .chart__bar {
46 | border-radius: 15px;
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Widgets} from 'src/components/Dashboard/dashboard.model';
3 | import {Widget} from 'src/components/Dashboard/Widget';
4 |
5 | import './styles/main.scss';
6 |
7 | export interface DashboardProps {
8 | widgets: Widgets[];
9 | title: string;
10 | }
11 |
12 | export const Dashboard = ({widgets, title}: DashboardProps) => {
13 | return (
14 |
15 |
{title}
16 | {widgets?.map((widget: Widgets) => (
17 |
18 | ))}
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Widget.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {templateComponents} from 'src/components/Dashboard/dashboard.config';
3 | import {Widgets} from 'src/components/Dashboard/dashboard.model';
4 |
5 | export interface WidgetProps {
6 | content: Widgets;
7 | className?: string;
8 | }
9 |
10 | export const Widget = ({content, className}: WidgetProps) => {
11 | const WidgetComponent = templateComponents[content.config.template];
12 | const customClass = className && `${className} `;
13 | const position = content.config.position.toLowerCase();
14 |
15 | return (
16 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/Dashboard/dashboard.config.ts:
--------------------------------------------------------------------------------
1 | import { WidgetTemplateConfig } from "src/components/Dashboard/dashboard.model";
2 |
3 | import { BarChartTemplate } from "src/components/Dashboard/templates/BarChart";
4 | import { BlockTemplate } from "src/components/Dashboard/templates/Block";
5 | import { ListTemplate } from "src/components/Dashboard/templates/List";
6 |
7 | export type WidgetType = "COMPARISON" | "METRIC" | "FUNNEL";
8 | export type WidgetPosition = "FULL" | "HALF" | "QUARTER";
9 | export type WidgetTemplate = "LIST" | "PIE_CHART" | "BAR_CHART" | "BLOCK";
10 |
11 | export const templateComponents: WidgetTemplateConfig = {
12 | BLOCK: BlockTemplate,
13 | LIST: ListTemplate,
14 | BAR_CHART: BarChartTemplate,
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/Dashboard/dashboard.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | WidgetPosition,
3 | WidgetTemplate,
4 | WidgetType,
5 | } from "src/components/Dashboard/dashboard.config";
6 |
7 | export interface List {
8 | title: string;
9 | value: string;
10 | }
11 |
12 | export type BarChart = {
13 | name: string;
14 | data: [number, number] | number[];
15 | };
16 |
17 | export interface Content {
18 | title: string;
19 | value?: number;
20 | prev?: string;
21 | current?: string;
22 | items?: List[] | BarChart[];
23 | }
24 |
25 | export interface Configuration {
26 | type: WidgetType;
27 | position: WidgetPosition;
28 | template: WidgetTemplate;
29 | }
30 |
31 | export interface Widgets {
32 | config: Configuration;
33 | data: Content;
34 | }
35 |
36 | export interface WidgetComponentProps {
37 | content: Widgets;
38 | }
39 |
40 | export interface WidgetTemplateConfig {
41 | [template: string]: React.FC;
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Dashboard/styles/layouts/_dashboard.scss:
--------------------------------------------------------------------------------
1 | .dashboard {
2 | display: flex;
3 | justify-items: center;
4 | flex-wrap: wrap;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Dashboard/styles/layouts/_widget.scss:
--------------------------------------------------------------------------------
1 | .widget {
2 | margin: .5rem;
3 | border-radius: 3px;
4 | padding: 1rem;
5 |
6 | .widget__title {
7 | margin-top: 0;
8 | }
9 |
10 | .widget__value {
11 | font-size: 2rem;
12 | font-weight: 700;
13 | }
14 |
15 | .widget__info {
16 | font-size: .8rem;
17 | font-weight: 300;
18 | font-style: italic;
19 | opacity: .75;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Dashboard/styles/main.scss:
--------------------------------------------------------------------------------
1 | // LAYOUTS -------------------------------------
2 | @import './layouts/dashboard';
3 | @import './layouts/widget';
4 |
5 | // POSITIONS -------------------------------------
6 | @import './position/full';
7 | @import './position/half';
8 | @import './position/quarter';
9 |
10 | // TEMPLATES -------------------------------------
11 | @import './templates/list';
12 | @import './templates/block';
13 | @import './templates/bar-chart';
14 |
--------------------------------------------------------------------------------
/src/components/Dashboard/styles/position/_full.scss:
--------------------------------------------------------------------------------
1 | .widget--full {
2 | flex-basis: 100%;
3 | background-color: #76D7C4;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Dashboard/styles/position/_half.scss:
--------------------------------------------------------------------------------
1 | .widget--half {
2 | // remove margin and padding
3 | flex-basis: calc(50% - 3rem);
4 | background-color: #85C1E9;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Dashboard/styles/position/_quarter.scss:
--------------------------------------------------------------------------------
1 | .widget--quarter {
2 | // remove margin and padding
3 | flex-basis: calc(25% - 3rem);
4 | background-color: #D2B4DE;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Dashboard/styles/templates/_bar-chart.scss:
--------------------------------------------------------------------------------
1 | .widget__chart {
2 | font-size: .8rem;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | flex-wrap: wrap;
7 |
8 | .widget__title {
9 | font-size: 1.5rem
10 | }
11 |
12 | .chart__bar {
13 | border-radius: 15px;
14 | }
15 |
16 | .widget__info {
17 | flex-basis: 100%;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Dashboard/styles/templates/_block.scss:
--------------------------------------------------------------------------------
1 | .widget__block {
2 |
3 | .widget__title,
4 | .widget__value,
5 | .widget__info {
6 | text-align: center;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/Dashboard/styles/templates/_list.scss:
--------------------------------------------------------------------------------
1 | .widget__list {
2 | .widget__ul {
3 | padding-left: 0;
4 | }
5 |
6 | li.widget__li {
7 | list-style: none;
8 | padding: .5rem;
9 | border-bottom: 1px solid #00000030;
10 | display: flex;
11 | justify-content: space-between;
12 |
13 | &:last-child {
14 | border-bottom: none;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Dashboard/templates/BarChart.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {WidgetComponentProps} from 'src/components/Dashboard/dashboard.model';
3 | // @ts-ignore
4 | import {Chart, Layer, Bars, Ticks} from 'rumble-charts';
5 |
6 | export const BarChartTemplate = ({content}: WidgetComponentProps) => {
7 | const title = content.data.title;
8 | const items = content.data.items;
9 | const currentParam = content.data.current;
10 |
11 | return (
12 |
13 |
{title}
14 |
15 |
16 | label}
23 | />
24 |
25 |
26 |
27 |
{currentParam}
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/Dashboard/templates/Block.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {WidgetComponentProps} from 'src/components/Dashboard/dashboard.model';
3 |
4 | export const BlockTemplate = ({content}: WidgetComponentProps) => {
5 | const prev = content.data.prev;
6 | const currentParam = content.data.current;
7 | const title = content.data.title;
8 | const value = content.data.value;
9 |
10 | const handleInfo = () => (prev ? `${prev} - ${currentParam}` : currentParam);
11 |
12 | return (
13 |
14 |
{title}
15 |
{value}
16 |
{handleInfo()}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/Dashboard/templates/List.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {WidgetComponentProps} from 'src/components/Dashboard/dashboard.model';
3 |
4 | export const ListTemplate = ({content}: WidgetComponentProps) => {
5 | const title = content.data.title;
6 | const itemsList = content.data.items!;
7 |
8 | return (
9 |
10 |
{title}
11 |
12 | {itemsList.map((item: any) => (
13 | -
14 | {item.title}
15 | {item.value}
16 |
17 | ))}
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/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/mocks/dashboard.data.ts:
--------------------------------------------------------------------------------
1 | import { Widgets } from "src/components/Dashboard/dashboard.model";
2 |
3 | export const widgets: Widgets[] = [
4 | {
5 | config: {
6 | type: "COMPARISON",
7 | position: "FULL",
8 | template: "BAR_CHART",
9 | },
10 | data: {
11 | title: "Valor Total de Pedidos",
12 | current: "jan-mar de 2021",
13 | items: [
14 | {
15 | name: "Jan",
16 | data: [15000],
17 | },
18 | {
19 | name: "Fev",
20 | data: [12000],
21 | },
22 | {
23 | name: "Mar",
24 | data: [25000],
25 | },
26 | ],
27 | },
28 | },
29 | {
30 | config: {
31 | type: "METRIC",
32 | position: "HALF",
33 | template: "LIST",
34 | },
35 | data: {
36 | title: "Lista de Pedidos",
37 | current: "abr/2021",
38 | items: [
39 | {
40 | title: "Pedro",
41 | value: "R$ 32,50",
42 | },
43 | {
44 | title: "Rafael",
45 | value: "R$ 54,95",
46 | },
47 | {
48 | title: "Ingrid",
49 | value: "R$ 12,00",
50 | },
51 | ],
52 | },
53 | },
54 | {
55 | config: {
56 | type: "COMPARISON",
57 | position: "QUARTER",
58 | template: "BLOCK",
59 | },
60 | data: {
61 | title: "Numero de Pedidos",
62 | value: 35698,
63 | prev: "2019",
64 | current: "2020",
65 | },
66 | },
67 | ];
68 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/styles/main.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap');
2 |
3 | * {
4 | font-family: 'Open Sans', sans-serif;
5 | color: #1C2833;
6 | }
7 |
8 | body {
9 | width: 100%;
10 | margin: 0 auto;
11 | }
12 |
13 | h1 {
14 | margin: .5rem;
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "typeRoots": [
4 | "node_modules/@types",
5 | "src/types"
6 | ],
7 | "target": "es5",
8 | "lib": [
9 | "dom",
10 | "dom.iterable",
11 | "esnext"
12 | ],
13 | "allowJs": true,
14 | "skipLibCheck": true,
15 | "esModuleInterop": true,
16 | "allowSyntheticDefaultImports": true,
17 | "strict": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "module": "esnext",
20 | "moduleResolution": "node",
21 | "resolveJsonModule": true,
22 | "isolatedModules": true,
23 | "noEmit": true,
24 | "jsx": "react-jsx",
25 | "baseUrl": "./",
26 | "noFallthroughCasesInSwitch": true
27 | },
28 | "include": [
29 | "src"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------