├── .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 | 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 | 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 | --------------------------------------------------------------------------------