├── .babelrc ├── .editorconfig ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .stylelintrc ├── Api └── weather.js ├── LICENSE ├── README.md ├── build ├── 0.83133707.js └── bundle.c0781dff.js ├── components ├── Button │ ├── Readme.md │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.js.snap │ │ └── index.js │ ├── index.js │ └── style.css ├── ButtonDropdown │ ├── Readme.md │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.js.snap │ │ └── index.js │ ├── index.js │ └── style.css ├── GithubCard │ ├── Followers │ │ ├── index.css │ │ └── index.js │ ├── Info │ │ ├── index.css │ │ └── index.js │ ├── Readme.md │ ├── index.js │ └── style.css ├── Loading │ ├── Readme.md │ ├── index.js │ └── style.css ├── Weather │ ├── Readme.md │ ├── index.js │ └── style.css └── css │ ├── typo.css │ └── variables.css ├── config └── fileTransformer.js ├── index.html ├── package.json ├── postcss.config.js ├── styleguide.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "node": "current", 7 | "browsers": ["last 3 versions"] 8 | } 9 | }], 10 | "react" 11 | ], 12 | "plugins": [ 13 | "transform-class-properties", 14 | "transform-object-rest-spread" 15 | ], 16 | "env": { 17 | "test": { 18 | "plugins": ["transform-es2015-modules-commonjs"] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true, 6 | "jest": true 7 | }, 8 | "parser": "babel-eslint", 9 | "plugins": [ 10 | "react", 11 | "flowtype" 12 | ], 13 | "extends": [ 14 | "airbnb", 15 | "plugin:flowtype/recommended" 16 | ], 17 | "rules": { 18 | "comma-dangle": [ 19 | "error", 20 | "only-multiline" 21 | ], 22 | "react/jsx-filename-extension": "off", 23 | "import/no-extraneous-dependencies": "off", 24 | "prefer-arrow-callback": "off", 25 | "import/no-named-as-default": "off", 26 | "no-restricted-syntax": "off", 27 | "jsx-a11y/no-static-element-interactions": "off", 28 | "max-len": [ 29 | "error", 30 | 100, 31 | { 32 | "ignoreStrings": true, 33 | "ignoreComments": true, 34 | "ignoreUrls": true, 35 | "ignoreTrailingComments": true 36 | } 37 | ], 38 | "react/sort-comp": [2, { 39 | "order": [ 40 | "static-methods", 41 | "lifecycle", 42 | "everything-else", 43 | "render" 44 | ], 45 | "groups": { 46 | "lifecycle": [ 47 | "displayName", 48 | "propTypes", 49 | "props", 50 | "contextTypes", 51 | "childContextTypes", 52 | "mixins", 53 | "statics", 54 | "defaultProps", 55 | "constructor", 56 | "getDefaultProps", 57 | "getInitialState", 58 | "state", 59 | "getChildContext", 60 | "componentWillMount", 61 | "componentDidMount", 62 | "componentWillReceiveProps", 63 | "shouldComponentUpdate", 64 | "componentWillUpdate", 65 | "componentDidUpdate", 66 | "componentWillUnmount" 67 | ] 68 | } 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/stylelint/.* 3 | .*/node_modules/jss/.* 4 | .*/node_modules/findup/.* 5 | 6 | [include] 7 | 8 | [libs] 9 | flow-typed 10 | 11 | [options] 12 | module.use_strict=true 13 | module.ignore_non_literal_requires=true 14 | suppress_comment= \\(.\\|\n\\)*\\$FlowBug 15 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe 16 | module.name_mapper='.*\(.\)' -> '/flow/cssModules.js' 17 | module.name_mapper='.*\(.graphql\)' -> '/flow/graphql.js' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/nodejs,osx,node,intellij,vim,webstorm,linux,windows 3 | 4 | ### OSX ### 5 | *.DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | .tmp 9 | 10 | # Icon must end with two \r 11 | Icon 12 | # Thumbnails 13 | ._* 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | 30 | ### Node ### 31 | # Logs 32 | logs 33 | *.log 34 | npm-debug.log* 35 | 36 | # Runtime data 37 | pids 38 | *.pid 39 | *.seed 40 | *.pid.lock 41 | 42 | # Directory for instrumented libs generated by jscoverage/JSCover 43 | lib-cov 44 | 45 | # Coverage directory used by tools like istanbul 46 | coverage 47 | 48 | # nyc test coverage 49 | .nyc_output 50 | 51 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 52 | .grunt 53 | 54 | # node-waf configuration 55 | .lock-wscript 56 | 57 | # Compiled binary addons (http://nodejs.org/api/addons.html) 58 | build/Release 59 | 60 | # Dependency directories 61 | node_modules 62 | jspm_packages 63 | 64 | # Optional npm cache directory 65 | .npm 66 | 67 | # Optional eslint cache 68 | .eslintcache 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | 80 | 81 | ### Intellij ### 82 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 83 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 84 | 85 | # User-specific stuff: 86 | .idea/workspace.xml 87 | .idea/tasks.xml 88 | 89 | # Sensitive or high-churn files: 90 | .idea/dataSources/ 91 | .idea/dataSources.ids 92 | .idea/dataSources.xml 93 | .idea/dataSources.local.xml 94 | .idea/sqlDataSources.xml 95 | .idea/dynamic.xml 96 | .idea/uiDesigner.xml 97 | 98 | # Gradle: 99 | .idea/gradle.xml 100 | .idea/libraries 101 | 102 | # Mongo Explorer plugin: 103 | .idea/mongoSettings.xml 104 | 105 | ## File-based project format: 106 | *.iws 107 | 108 | ## Plugin-specific files: 109 | 110 | # IntelliJ 111 | /out/ 112 | 113 | # mpeltonen/sbt-idea plugin 114 | .idea_modules/ 115 | 116 | # JIRA plugin 117 | atlassian-ide-plugin.xml 118 | 119 | # Crashlytics plugin (for Android Studio and IntelliJ) 120 | com_crashlytics_export_strings.xml 121 | crashlytics.properties 122 | crashlytics-build.properties 123 | fabric.properties 124 | 125 | ### Intellij Patch ### 126 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 127 | 128 | # *.iml 129 | # modules.xml 130 | # .idea/misc.xml 131 | # *.ipr 132 | 133 | 134 | ### Vim ### 135 | # swap 136 | [._]*.s[a-v][a-z] 137 | [._]*.sw[a-p] 138 | [._]s[a-v][a-z] 139 | [._]sw[a-p] 140 | # session 141 | Session.vim 142 | # temporary 143 | .netrwhist 144 | *~ 145 | # auto-generated tag files 146 | tags 147 | 148 | 149 | ### WebStorm ### 150 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 151 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 152 | 153 | # User-specific stuff: 154 | 155 | # Sensitive or high-churn files: 156 | 157 | # Gradle: 158 | 159 | # Mongo Explorer plugin: 160 | 161 | ## File-based project format: 162 | 163 | ## Plugin-specific files: 164 | 165 | # IntelliJ 166 | 167 | # mpeltonen/sbt-idea plugin 168 | 169 | # JIRA plugin 170 | 171 | # Crashlytics plugin (for Android Studio and IntelliJ) 172 | 173 | ### WebStorm Patch ### 174 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 175 | 176 | # *.iml 177 | # modules.xml 178 | # .idea/misc.xml 179 | # *.ipr 180 | 181 | 182 | ### Linux ### 183 | 184 | # temporary files which can be created if a process still has a handle open of a deleted file 185 | .fuse_hidden* 186 | 187 | # KDE directory preferences 188 | .directory 189 | 190 | # Linux trash folder which might appear on any partition or disk 191 | .Trash-* 192 | 193 | # .nfs files are created when an open file is removed but is still being accessed 194 | .nfs* 195 | 196 | 197 | ### Windows ### 198 | # Windows thumbnail cache files 199 | Thumbs.db 200 | ehthumbs.db 201 | ehthumbs_vista.db 202 | 203 | # Folder config file 204 | Desktop.ini 205 | 206 | # Recycle Bin used on file shares 207 | $RECYCLE.BIN/ 208 | 209 | # Windows Installer files 210 | *.cab 211 | *.msi 212 | *.msm 213 | *.msp 214 | 215 | # Windows shortcuts 216 | *.lnk 217 | 218 | # End of https://www.gitignore.io/api/nodejs,osx,node,intellij,vim,webstorm,linux,windows 219 | reports 220 | dist 221 | coverage 222 | .tmp 223 | styleguide 224 | .idea 225 | .vscode 226 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "declaration-no-important": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Api/weather.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const base = 'https://api.wunderground.com/api/6972e27d6b748a1f'; 4 | const geoLookup = `${base}/geolookup/q/autoip.json`; 5 | 6 | 7 | const getWeatherUrl = (lat, long) => `${base}/forecast/geolookup/conditions/q/${lat},${long}.json`; 8 | 9 | const getLocation = () => 10 | axios.get(geoLookup) 11 | .then(rsp => rsp.data.location) 12 | .then(location => ({ lat: location.lat, lon: location.lon })); 13 | 14 | 15 | const getData = () => 16 | getLocation() 17 | .then(location => axios.get(getWeatherUrl(location.lat, location.lon)) 18 | .then(rsp => rsp.data.current_observation)); 19 | 20 | 21 | export default getData; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sara Vieira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 30.30 2 | 30 React Components in 30ish days 3 | 4 | I decided to challenge myself and create 30 react components in about 30 days. I can't commit it to be 30 days but I will try my best. 5 | 6 | The Components to be added are here: 7 | http://flask.io/nHLj2 8 | 9 | ## Note about the videos 10 | 11 | The videos are not to be seen as tutorials, they will not be edited in the majority of cases and I will Fuck up in all of them probably since they will start from scratch and the components will be built interest on the video. 12 | It's mainly to get ideas how to fix common issues and for you to have this experience with me so we can be awesome together 13 | There may not be a lot of sound in some of them since I live with 3 people who already think I am insane and I don't perpetuate that :p 14 | 15 | ## Stack 16 | 17 | 18 | | CSS | JavaScript | Tools | 19 | |-----------------------------------------------------------|--------------------------------------------|-------------------------------------| 20 | | [PostCSS](http://postcss.org/) | [Babel](https://babeljs.io/) | [Webpack](https://webpack.js.org/) | 21 | | [CSSnext](http://cssnext.io/) | [Flowtype](https://flowtype.org/) | [Yarn](https://yarnpkg.com/) | 22 | | [css-modules](https://github.com/css-modules/css-modules) | [React](https://facebook.github.io/react/) | [React Styleguidist](https://github.com/styleguidist/react-styleguidist) | 23 | | [stylelint](https://stylelint.io/) | [ESlint](http://eslint.org/) | | 24 | | | [Jest](https://facebook.github.io/jest/) | | | 25 | 26 | ## Commands 27 | 28 | - yarn start -> serve the react styleguidist 29 | - yarn test -> run eslint, flow and unit tests 30 | -------------------------------------------------------------------------------- /components/Button/Readme.md: -------------------------------------------------------------------------------- 1 | The Video: 2 | 3 | 4 | Examples: 5 | Console.log Example 6 | ```example 7 |
8 | 11 | 14 | 17 |
18 | ``` 19 | 20 | className Example 21 | 22 | ```example 23 |
24 | 27 | 30 | 33 |
34 | ``` 35 | 36 | Disabled Example 37 | 38 | ```example 39 |
40 | 43 | 46 | 49 |
50 | ``` 51 | -------------------------------------------------------------------------------- /components/Button/__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Button renders the text inside it 1`] = ` 4 | 10 | 11 | `; 12 | 13 | exports[`Button renders value 1`] = ` 14 | 23 | 24 | `; 25 | 26 | exports[`adds types 1`] = ` 27 | 37 | 38 | `; 39 | 40 | exports[`adds types 2`] = ` 41 | 51 | 52 | `; 53 | 54 | exports[`adds types 3`] = ` 55 | 65 | 66 | `; 67 | 68 | exports[`calls on click function 1`] = ` 69 | 81 | 82 | `; 83 | -------------------------------------------------------------------------------- /components/Button/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line 2 | import tjson from 'enzyme-to-json'; 3 | import { mount } from 'enzyme'; 4 | import Button from '../index'; 5 | 6 | test('Button renders the text inside it', () => { 7 | const button = mount( 8 | 9 | ); 10 | expect(button.hasClass('primary')).toEqual(true); 11 | expect(button.text()).toBe('Test'); 12 | const tree = tjson(button); 13 | expect(tree).toMatchSnapshot(); 14 | }); 15 | 16 | test('Button renders value', () => { 17 | const button = mount( 18 | 19 | ); 20 | expect(button.hasClass('primary')).toEqual(true); 21 | expect(button.text()).toBe('Test'); 22 | expect(button.find('button').props().value).toBe('30'); 23 | const tree = tjson(button); 24 | expect(tree).toMatchSnapshot(); 25 | }); 26 | 27 | test('adds types', () => { 28 | const button = mount( 29 | 32 | ); 33 | expect(button.hasClass('secondary')).toEqual(true); 34 | expect(button.find('p').html()).toBe('

Work

'); 35 | const tree = tjson(button); 36 | expect(tree).toMatchSnapshot(); 37 | }); 38 | 39 | test('adds types', () => { 40 | const button = mount( 41 | 44 | ); 45 | expect(button.hasClass('ghost')).toEqual(true); 46 | expect(button.find('p').html()).toBe('

Ghost

'); 47 | const tree = tjson(button); 48 | expect(tree).toMatchSnapshot(); 49 | }); 50 | 51 | test('adds types', () => { 52 | const button = mount( 53 | 56 | ); 57 | expect(button.hasClass('test')).toEqual(true); 58 | expect(button.find('p').html()).toBe('

Ghost

'); 59 | const tree = tjson(button); 60 | expect(tree).toMatchSnapshot(); 61 | }); 62 | 63 | test('calls on click function', () => { 64 | const test = jest.fn(); 65 | const button = mount( 66 | 69 | ); 70 | button.simulate('click'); 71 | expect(test).toHaveBeenCalled(); 72 | const tree = tjson(button); 73 | expect(tree).toMatchSnapshot(); 74 | }); 75 | -------------------------------------------------------------------------------- /components/Button/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import classnames from 'classnames'; 4 | import style from './style.css'; 5 | 6 | type ButtonType = 'primary' | 'secondary' | 'ghost' 7 | 8 | type Props = { 9 | children: string | number | Element, 10 | disabled: boolean, 11 | className: string, 12 | onClick: Function, 13 | type: ButtonType, 14 | value: string 15 | } 16 | 17 | function Button(props: Props) { 18 | return ( 19 | 33 | ); 34 | } 35 | 36 | export default Button; 37 | -------------------------------------------------------------------------------- /components/Button/style.css: -------------------------------------------------------------------------------- 1 | @import '../css/variables.css'; 2 | 3 | .button { 4 | outline: none; 5 | border: none; 6 | border-radius: 3px; 7 | width: 150px; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | padding: 16px 0; 12 | font-size: 14px; 13 | font-weight: 900; 14 | text-transform: uppercase; 15 | cursor: pointer; 16 | transition: background-color 200ms ease; 17 | 18 | &[disabled] { 19 | cursor: default; 20 | } 21 | 22 | &.primary { 23 | background-color: var(--dark-sky-blue-three); 24 | color: var(--white-two); 25 | 26 | &[disabled] { 27 | opacity: 0.24; 28 | } 29 | 30 | &:hover { 31 | background-color: color(var(--dark-sky-blue-three) l(+10%)); 32 | } 33 | } 34 | 35 | &.secondary { 36 | background-color: var(--dark-grey-blue); 37 | color: var(--white-two); 38 | 39 | &[disabled] { 40 | color: rgba(133, 133, 133, 0.4); 41 | } 42 | 43 | &:hover { 44 | background-color: color(var(--dark-grey-blue) l(+10%)); 45 | } 46 | } 47 | 48 | &.ghost { 49 | background-color: var(--white); 50 | border: solid 1px var(--white-four); 51 | color: var(--dark-grey-blue-two); 52 | 53 | &[disabled] { 54 | color: var(--dark-grey-blue-two); 55 | opacity: 0.24; 56 | } 57 | 58 | &:hover { 59 | background-color: color(var(--white) b(+10%)); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /components/ButtonDropdown/Readme.md: -------------------------------------------------------------------------------- 1 | Video: 2 | 3 | 4 | Example: 5 | ```example 6 | 7 |
    8 |
  • Button
  • 9 |
  • Button Dropwndown
  • 10 |
  • Button Dropdown
  • 11 |
  • Checkbox
  • 12 |
  • Weather Widget
  • 13 |
  • Camera Component
  • 14 |
15 |
16 | ``` 17 | -------------------------------------------------------------------------------- /components/ButtonDropdown/__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ButtonDropdown renders children on click 1`] = ` 4 | 5 |
8 | 16 | 22 |
25 | Test 26 |
27 |
28 |
29 | `; 30 | 31 | exports[`ButtonDropdown renders children on click 2`] = ` 32 | 33 |
36 | 44 | 50 |
53 |

54 | Test 55 |

56 |
57 |
58 |
59 | `; 60 | 61 | exports[`ButtonDropdown renders the text inside it 1`] = ` 62 | 63 |
66 | 74 | 80 |
81 |
82 | `; 83 | -------------------------------------------------------------------------------- /components/ButtonDropdown/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line 2 | import tjson from 'enzyme-to-json'; 3 | import { mount } from 'enzyme'; 4 | import ButtonDropdown from '../index'; 5 | 6 | test('ButtonDropdown renders the text inside it', () => { 7 | const buttonDropdown = mount( 8 | Test 9 | ); 10 | expect(buttonDropdown.hasClass('ButtonDropdownWrapper')).toEqual(true); 11 | expect(buttonDropdown.text()).toBe('▼'); 12 | const tree = tjson(buttonDropdown); 13 | expect(tree).toMatchSnapshot(); 14 | }); 15 | 16 | test('ButtonDropdown renders children on click', () => { 17 | const buttonDropdown = mount( 18 | Test 19 | ); 20 | buttonDropdown.find('.primary').simulate('click'); 21 | expect(buttonDropdown.hasClass('ButtonDropdownWrapper')).toEqual(true); 22 | expect(buttonDropdown.text()).toBe('▲Test'); 23 | const tree = tjson(buttonDropdown); 24 | expect(tree).toMatchSnapshot(); 25 | }); 26 | 27 | test('ButtonDropdown renders children on click', () => { 28 | const buttonDropdown = mount( 29 | 30 |

Test

31 |
32 | ); 33 | buttonDropdown.find('.primary').simulate('click'); 34 | expect(buttonDropdown.hasClass('ButtonDropdownWrapper')).toEqual(true); 35 | expect(buttonDropdown.find('p').html()).toBe('

Test

'); 36 | const tree = tjson(buttonDropdown); 37 | expect(tree).toMatchSnapshot(); 38 | }); 39 | -------------------------------------------------------------------------------- /components/ButtonDropdown/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import Button from '../Button'; 4 | import style from './style.css'; 5 | 6 | type Props = { 7 | buttonContent: string, 8 | children: Element 9 | } 10 | 11 | type State = { 12 | opened: boolean 13 | } 14 | 15 | class ButtonDropdown extends Component { 16 | props: Props; 17 | 18 | constructor(props: Props) { 19 | super(props); 20 | this.state = { 21 | opened: false 22 | }; 23 | this.handleDrowpdown = this.handleDrowpdown.bind(this); 24 | } 25 | 26 | state: State; 27 | handleDrowpdown: () => void; 28 | 29 | handleDrowpdown() { 30 | this.setState({ 31 | opened: !this.state.opened 32 | }); 33 | } 34 | 35 | 36 | render() { 37 | return ( 38 |
39 | 44 | 50 | { this.state.opened && 51 |
{this.props.children}
52 | } 53 |
54 | ); 55 | } 56 | } 57 | 58 | export default ButtonDropdown; 59 | -------------------------------------------------------------------------------- /components/ButtonDropdown/style.css: -------------------------------------------------------------------------------- 1 | @import '../css/variables.css'; 2 | 3 | .ButtonDropdownWrapper { 4 | position: relative; 5 | display: inline-flex; 6 | color: var(--white-two); 7 | align-items: center; 8 | border-radius: 3px; 9 | 10 | & .ButtonDropdownIcon { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | padding: 16px 18px; 15 | border-left: solid 1px var(--white-two); 16 | cursor: pointer; 17 | outline: none; 18 | border: none; 19 | background-color: var(--dark-sky-blue-three); 20 | color: var(--white-two); 21 | margin-left: -2px; 22 | min-height: 49px; 23 | border-radius: 0 3px 3px 0; 24 | 25 | &:hover { 26 | background-color: color(var(--dark-sky-blue-three) l(+10%)); 27 | } 28 | } 29 | 30 | & .dropdown { 31 | position: absolute; 32 | top: 100%; 33 | border-radius: 3px; 34 | background-color: var(--white); 35 | width: 100%; 36 | padding: 10px; 37 | color: var(--dark-sky-blue-three); 38 | 39 | & a { 40 | color: var(--dark-sky-blue-three); 41 | text-decoration: none; 42 | 43 | &:hover { 44 | color: color(var(--dark-sky-blue-three) b(+10%)); 45 | } 46 | } 47 | 48 | & ul { 49 | list-style: none; 50 | padding: 0; 51 | margin: 0; 52 | 53 | & li:not(:last-child) { 54 | padding-bottom: 10px; 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /components/GithubCard/Followers/index.css: -------------------------------------------------------------------------------- 1 | .followers { 2 | & span { 3 | margin-right: 4px; 4 | } 5 | 6 | & small:first-of-type { 7 | margin-right: 8px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /components/GithubCard/Followers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import FontAwesome from 'react-fontawesome'; 5 | import style from './index.css'; 6 | 7 | type Props = { 8 | followers: number, 9 | following: number, 10 | followersCallback: Function, 11 | followingCallback: Function, 12 | } 13 | 14 | const Followers = (props: Props) => ( 15 |
16 | {(props.followers !== 0 || props.following !== 0) && 17 |
18 | 19 | Following: 20 | 21 | {props.following} 22 | 23 | 24 | Followers: 25 | 26 | {props.followers} 27 | 28 | 29 |
30 | } 31 |
32 | ); 33 | 34 | export default Followers; 35 | -------------------------------------------------------------------------------- /components/GithubCard/Info/index.css: -------------------------------------------------------------------------------- 1 | .info { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | 6 | & small:not(:last-child) { 7 | margin-right: 8px; 8 | } 9 | 10 | & strong { 11 | margin-right: 2px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /components/GithubCard/Info/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import style from './index.css'; 5 | 6 | type Props = { 7 | company: string, 8 | email: string, 9 | } 10 | 11 | const Info = (props: Props) => ( 12 |
13 | {(props.company || props.email) && 14 |
15 | {props.company && 16 | 17 | Comapany: 18 | {props.company} 19 | 20 | } 21 | {props.email && 22 | 23 | Email: 24 | {props.email} 25 | 26 | } 27 |
28 | } 29 |
30 | ); 31 | 32 | export default Info; 33 | -------------------------------------------------------------------------------- /components/GithubCard/Readme.md: -------------------------------------------------------------------------------- 1 | Logged in User: 2 | ```example 3 | 4 | ``` 5 | 6 | Any User: 7 | ```example 8 | 9 | ``` 10 | 11 | Github: 12 | ```example 13 | 14 | ``` 15 | 16 | No Token for other users: 17 | ```example 18 | 19 | ``` 20 | 21 | No Token your user: 22 | ```example 23 | 24 | ``` 25 | -------------------------------------------------------------------------------- /components/GithubCard/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import FontAwesome from 'react-fontawesome'; 4 | import GitHub from 'github-api'; 5 | import Info from './Info'; 6 | import Followers from './Followers'; 7 | import Loading from '../Loading'; 8 | import style from './style.css'; 9 | 10 | type Props = { 11 | /** 12 | * If no string is passed the logged in user will be shown 13 | * @type {Object} 14 | */ 15 | user?: ?string, 16 | token?: ?string 17 | } 18 | 19 | type State = { 20 | loading: boolean, 21 | profile: Object 22 | } 23 | 24 | class GitHubCard extends Component { 25 | static defaultProps = { 26 | user: null, 27 | token: null 28 | } 29 | props: Props; 30 | constructor(props: Props) { 31 | super(props); 32 | this.state = { 33 | user: null, 34 | loading: true, 35 | profile: {} 36 | }; 37 | this.createUrl = this.createUrl.bind(this); 38 | } 39 | 40 | state: State; 41 | 42 | componentWillMount() { 43 | const gh = new GitHub({ 44 | token: this.props.token 45 | }); 46 | const user = gh.getUser(this.props.user); 47 | user.getProfile((err, profile) => 48 | this.setState({ 49 | loading: false, 50 | profile: { 51 | login: profile.login, 52 | avatar_url: profile.avatar_url, 53 | company: profile.company, 54 | email: profile.email, 55 | repos: profile.public_repos, 56 | url: profile.html_url, 57 | followers: profile.followers, 58 | following: profile.following 59 | } 60 | })); 61 | } 62 | 63 | createUrl: (a: string) => void; 64 | 65 | createUrl(tab: string) { 66 | return `${this.state.profile.url}?tab=${tab}`; 67 | } 68 | 69 | 70 | render() { 71 | return ( 72 |
73 | { !this.props.user && !this.props.token ? 74 |
You need a token
: 75 |
76 | {this.state.loading ? : 77 |
78 |

@{this.state.profile.login}

79 | {this.state.profile.login} 80 | 81 | 85 | this.createUrl('followers')} 88 | following={this.state.profile.following} 89 | followingCallback={() => this.createUrl('following')} 90 | /> 91 |
92 | } 93 |
94 | } 95 |
96 | ); 97 | } 98 | } 99 | 100 | export default GitHubCard; 101 | -------------------------------------------------------------------------------- /components/GithubCard/style.css: -------------------------------------------------------------------------------- 1 | @import "../css/variables.css"; 2 | @import "../css/typo.css"; 3 | @import 'https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css'; 4 | 5 | .card { 6 | background: var(--white-three); 7 | min-width: 250px; 8 | max-width: 350px; 9 | padding: 14px 0; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | flex-direction: column; 14 | border-radius: 3px; 15 | box-shadow: var(--subtle-shadow); 16 | 17 | & img { 18 | max-width: 150px; 19 | box-shadow: var(--subtle-shadow); 20 | border-radius: 2px; 21 | margin-bottom: 8px; 22 | } 23 | 24 | & .repos { 25 | margin-left: 4px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /components/Loading/Readme.md: -------------------------------------------------------------------------------- 1 | ```example 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /components/Loading/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import style from './style.css'; 4 | 5 | const Loading = () => ( 6 |
7 |
8 |
9 |
10 |
11 |
12 | ); 13 | 14 | export default Loading; 15 | -------------------------------------------------------------------------------- /components/Loading/style.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | 6 | & .loading-bar { 7 | display: inline-block; 8 | width: 4px; 9 | height: 18px; 10 | border-radius: 4px; 11 | animation: loading 1s ease-in-out infinite; 12 | 13 | &:nth-child(1) { 14 | background-color: #3498db; 15 | animation-delay: 0; 16 | } 17 | 18 | &:nth-child(2) { 19 | background-color: #c0392b; 20 | animation-delay: 0.09s; 21 | } 22 | 23 | &:nth-child(3) { 24 | background-color: #f1c40f; 25 | animation-delay: 0.18s; 26 | } 27 | 28 | &:nth-child(4) { 29 | background-color: #27ae60; 30 | animation-delay: 0.27s; 31 | } 32 | } 33 | } 34 | 35 | @keyframes loading { 36 | 0% { 37 | transform: scale(1); 38 | } 39 | 40 | 20% { 41 | transform: scale(1, 2.2); 42 | } 43 | 44 | 40% { 45 | transform: scale(1); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /components/Weather/Readme.md: -------------------------------------------------------------------------------- 1 | No Props example (defaults to Celsius): 2 | 3 | ```example 4 | 5 | ``` 6 | 7 | Farenheight: 8 | 9 | ```example 10 | 11 | ``` 12 | 13 | Celsius: 14 | 15 | ```example 16 | 17 | ``` 18 | -------------------------------------------------------------------------------- /components/Weather/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import Loading from '../Loading'; 4 | import style from './style.css'; 5 | import getData from '../../Api/weather'; 6 | 7 | type Unit = 'F' | 'C' | 'f' | 'c'; 8 | 9 | type Props = { 10 | /** 11 | * 'F' | 'C' | 'f' | 'c' 12 | */ 13 | unit?: Unit 14 | } 15 | 16 | type State = { 17 | location: string, 18 | temp_c: number, 19 | temp_f: number, 20 | icon_url: string, 21 | weather: string, 22 | loading: boolean, 23 | error: boolean 24 | } 25 | 26 | class Weather extends Component { 27 | 28 | static defaultProps = { 29 | unit: 'C' 30 | } 31 | 32 | props: Props; 33 | 34 | constructor(props: Props) { 35 | super(props); 36 | this.state = { 37 | location: '', 38 | temp_c: 0, 39 | temp_f: 0, 40 | icon_url: '', 41 | weather: '', 42 | loading: true, 43 | error: false 44 | }; 45 | } 46 | 47 | state: State; 48 | 49 | componentWillMount() { 50 | getData() 51 | .then(weather => 52 | this.setState({ 53 | loading: false, 54 | location: weather.display_location.full, 55 | temp_c: weather.temp_c, 56 | temp_f: weather.temp_f, 57 | icon_url: weather.icon_url, 58 | weather: weather.weather, 59 | }) 60 | ) 61 | .catch(() => 62 | this.setState({ 63 | loading: false, 64 | error: true 65 | }) 66 | ); 67 | } 68 | 69 | render() { 70 | return ( 71 |
72 | {this.state.loading ? : 73 |
74 | {!this.state.error ? 75 |
76 |

{this.state.weather}

77 | {this.state.weather} 78 |

79 | It's { 80 | (this.props.unit || 'c').toLowerCase() === 'f' ? 81 | `${this.state.temp_f}º F` : 82 | `${this.state.temp_c}º C` 83 | } 84 |

85 | You are currently on {this.state.location} 86 |
87 | : 88 |
89 |

😞

90 | There has been a problem getting the weather 91 |
92 | } 93 |
94 | } 95 |
96 | ); 97 | } 98 | } 99 | 100 | export default Weather; 101 | -------------------------------------------------------------------------------- /components/Weather/style.css: -------------------------------------------------------------------------------- 1 | @import '../css/variables.css'; 2 | @import '../css/typo.css'; 3 | 4 | .weather { 5 | background: var(--white-three); 6 | width: 250px; 7 | padding: 14px 0; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | flex-direction: column; 12 | border-radius: 3px; 13 | box-shadow: var(--subtle-shadow); 14 | } 15 | 16 | .error { 17 | padding: 0 14px; 18 | text-align: center; 19 | } 20 | -------------------------------------------------------------------------------- /components/css/typo.css: -------------------------------------------------------------------------------- 1 | @import 'variables.css'; 2 | 3 | h1 { 4 | font-size: 22px; 5 | line-height: 28px; 6 | margin: 14px 0; 7 | color: var(--black-two); 8 | } 9 | 10 | p { 11 | font-size: 14px; 12 | color: var(--black-four); 13 | margin: 8px 0; 14 | } 15 | 16 | small { 17 | font-size: 12px; 18 | } 19 | 20 | a, 21 | a:visited { 22 | text-decoration: none; 23 | font-size: 14px; 24 | color: var(--dark-sky-blue-two); 25 | transition: all 200ms ease; 26 | 27 | &:hover { 28 | color: var(--dark-sky-blue-three); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /components/css/variables.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Lato'); 2 | 3 | :root { 4 | --dark-sky-blue: #31b3ef; 5 | --azure: #15b4f1; 6 | --warm-grey: #858585; 7 | --greyish: #b0b0b0; 8 | --dark-sky-blue-two: #2aa7dc; 9 | --golden-yellow: #f8c61c; 10 | --steel: #7c8495; 11 | --greenish-teal: #48d2a0; 12 | --dark-sky-blue-three: #358ed7; 13 | --dark-grey-blue: #34495e; 14 | --dusk-blue: #2a648e; 15 | --white-two: #f9f4f4; 16 | --black: #222; 17 | --watermelon: #f54b5e; 18 | --denim-blue: #3a5695; 19 | --dark-sky-blue-four: #55acee; 20 | --pale-red: #e05139; 21 | --dark-grey-blue-two: #34495e; 22 | --white: #fff; 23 | --dark-sky-blue-five: #4a90e2; 24 | --dark-sky-blue-three-two: #358ed7; 25 | --white-three: #fbfbfb; 26 | --white-four: #ddd; 27 | --white-five: #dedede; 28 | --charcoal-grey: #43484d; 29 | --warm-grey-two: #858585; 30 | --dark-sky-blue-four-two: #55acee; 31 | --charcoal-grey-two: #31363d; 32 | --greyish-two: #b0b0b0; 33 | --steel-two: #7c8495; 34 | --white-six: #eee; 35 | --greenish-teal-two: #48d2a0; 36 | --warm-grey-29: rgba(133, 133, 133, 0.29); 37 | --watermelon-two: #f54b5e; 38 | --black-two: #222; 39 | --steel-three: #7d8997; 40 | --faded-blue: #729ec2; 41 | --warm-grey-three: #979797; 42 | --golden-yellow-two: #f8c51c; 43 | --strawberry: #fc1b28; 44 | --slate: #465c72; 45 | --tree-green: #186e1a; 46 | --black-three: #0b0b09; 47 | --banana-yellow: #fffa4a; 48 | --silver: #dde1e4; 49 | --gunmetal: #444a59; 50 | --denim-blue-two: #3a5695; 51 | --charcoal-grey-three: #454553; 52 | --white-seven: #d8d5d5; 53 | --pale-red-two: #e05139; 54 | --warm-grey-four: #777; 55 | --greyish-brown: #444; 56 | --faded-pink: #efbdc4; 57 | --greenish-teal-three: #37cc71; 58 | --black-four: #1a1a1a; 59 | --metallic-blue: #55708a; 60 | --pale-turquoise: #92f9e2; 61 | --blue-purple: #7400ff; 62 | --dark-sky-blue-two-two: #2aa7dc; 63 | --greyish-48: rgba(176, 176, 176, 0.48); 64 | --greenish-teal-four: #2ecc71; 65 | --white-eight: #d8d8d8; 66 | --cool-grey: #b2bac4; 67 | --dusk-blue-two: #2a648e; 68 | --big-shadow: 0 12px 22px 0 rgba(0, 0, 0, 0.24); 69 | --subtle-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.09); 70 | --inner-shadow: inset 4px -14px 11px 0 rgba(0, 0, 0, 0.02); 71 | } 72 | 73 | * { 74 | font-family: 'Lato', sans-serif; 75 | box-sizing: border-box; 76 | } 77 | -------------------------------------------------------------------------------- /config/fileTransformer.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 30 30 Style Guide 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "30-30", 3 | "version": "0.0.1", 4 | "description": "30 React Components in 30ish days", 5 | "main": "index.js", 6 | "repository": "git@github.com:SaraVieira/30-30.git", 7 | "author": "hey@iamsaravieira.com", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "styleguidist server", 11 | "test": "run-s flow eslint jest", 12 | "build": "styleguidist build", 13 | "jest": "jest", 14 | "jest:update": "jest -u", 15 | "eslint": "eslint ./components", 16 | "eslint:fix": "eslint ./components --fix", 17 | "flow": "flow; test $? -eq 0 -o $? -eq 2", 18 | "publish-please": "publish-please", 19 | "prepublish": "publish-please guard", 20 | "serve": "http-server ./styleguide" 21 | }, 22 | "devDependencies": { 23 | "babel-core": "^6.24.1", 24 | "babel-eslint": "^7.2.3", 25 | "babel-loader": "^7.0.0", 26 | "babel-preset-env": "^1.4.0", 27 | "babel-preset-react": "^6.24.1", 28 | "css-loader": "^0.28.0", 29 | "enzyme": "^2.8.2", 30 | "enzyme-to-json": "^1.5.1", 31 | "eslint-config-airbnb": "^14.1.0", 32 | "eslint-plugin-flowtype": "^2.32.1", 33 | "eslint-plugin-import": "^2.2.0", 34 | "eslint-plugin-jsx-a11y": "^4.0.0", 35 | "eslint-plugin-react": "^6.10.3", 36 | "flow": "^0.2.3", 37 | "flow-typed": "^2.1.1", 38 | "identity-obj-proxy": "^3.0.0", 39 | "jest": "^19.0.2", 40 | "jss": "^7.1.0", 41 | "npm-run-all": "^4.0.2", 42 | "postcss-loader": "^1.3.3", 43 | "publish-please": "^2.3.1", 44 | "react": "^15.5.4", 45 | "react-addons-test-utils": "^15.5.1", 46 | "react-dom": "^15.5.4", 47 | "react-styleguidist": "^5.1.1", 48 | "react-svg-loader": "^1.1.1", 49 | "style-loader": "^0.16.1", 50 | "stylelint": "^7.10.1", 51 | "stylelint-config-standard": "^16.0.0", 52 | "webpack": "^2.4.1" 53 | }, 54 | "dependencies": { 55 | "axios": "^0.16.1", 56 | "babel-plugin-transform-class-properties": "^6.24.1", 57 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 58 | "eslint": "^3.19.0", 59 | "github-api": "^3.0.0", 60 | "postcss-cssnext": "^2.10.0", 61 | "postcss-smart-import": "^0.6.12", 62 | "react-fontawesome": "^1.6.1", 63 | "classnames": "^2.2.5" 64 | }, 65 | "browserslist": [ 66 | "last 3 versions", 67 | "last 4 iOS versions" 68 | ], 69 | "jest": { 70 | "bail": true, 71 | "verbose": true, 72 | "moduleNameMapper": { 73 | "\\.(svg|jpg|jpeg|png)$": "/config/fileTransformer.js", 74 | "\\.(css)$": "identity-obj-proxy" 75 | }, 76 | "collectCoverage": true, 77 | "coverageDirectory": "coverage", 78 | "cacheDirectory": ".tmp/jest" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | module.exports = { 4 | plugins: [ 5 | require('postcss-smart-import'), 6 | require('postcss-cssnext')() 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /styleguide.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const dir = path.join(__dirname, 'components'); 4 | 5 | module.exports = { 6 | components: 'components/*/*.js', 7 | styleguideDir: './', 8 | webpackConfig: { 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.css/, 13 | include: dir, 14 | use: [ 15 | 'style-loader', 16 | { 17 | loader: 'css-loader', 18 | options: { 19 | modules: true, 20 | localIdentName: '[local]__[name]--[hash:base64:5]', 21 | camelCase: true, 22 | minimize: true 23 | } 24 | }, 25 | 'postcss-loader' 26 | ] 27 | }, 28 | { 29 | test: /(\.jsx?)$/, 30 | include: [dir].concat(path.join(__dirname, 'Icons')).concat(path.join(__dirname, 'Colors')), 31 | loader: 'babel-loader' 32 | }, 33 | { 34 | test: /\.svg$/, 35 | include: path.join(__dirname, 'Icons'), 36 | loader: 'file-loader?name=[name].[ext]' 37 | }, 38 | { 39 | test: /\.svg$/, 40 | include: [dir].concat(path.join(__dirname, 'assets')), 41 | exclude: path.join(__dirname, 'Icons'), 42 | loader: 'react-svg-loader', 43 | }, 44 | { 45 | test: /\.(png|jpg)$/, 46 | loaders: 'url-loader?limit=10000', 47 | include: path.join(__dirname, 'assets/images'), 48 | } 49 | ] 50 | } 51 | } 52 | }; 53 | --------------------------------------------------------------------------------