├── .gitignore ├── src ├── dispatchers │ └── dispatcher.es6 ├── lib │ ├── actions.es6 │ ├── api.es6 │ └── store.es6 ├── components │ ├── markdown.jsx │ ├── button.jsx │ ├── issues-list.jsx │ ├── dropdown-item.jsx │ ├── repo-info.jsx │ ├── notification.jsx │ ├── input.jsx │ ├── dropdown.jsx │ ├── issue-item.jsx │ ├── pagination.jsx │ └── search-form.jsx ├── utils │ └── fetch.es6 ├── stores │ ├── user-store.es6 │ ├── issues-store.es6 │ ├── repos-store.es6 │ └── pagination-store.es6 ├── constants │ └── app-constants.es6 ├── actions │ └── app-actions.es6 ├── api │ └── github-api.es6 └── app.jsx ├── index.html ├── .editorconfig ├── webpack.config.js ├── package.json ├── webpack-prod.config.js ├── README.md └── styl └── main.styl /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | fonts 3 | node_modules 4 | -------------------------------------------------------------------------------- /src/dispatchers/dispatcher.es6: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from 'flux'; 2 | 3 | export default new Dispatcher(); 4 | -------------------------------------------------------------------------------- /src/lib/actions.es6: -------------------------------------------------------------------------------- 1 | import Dispatcher from '../dispatchers/dispatcher'; 2 | 3 | let Actions = { 4 | 5 | _dispatch (action) { 6 | 7 | return new Promise((resolve, reject) => { 8 | 9 | action.promise = { resolve, reject }; 10 | 11 | Dispatcher.dispatch(action); 12 | }); 13 | } 14 | }; 15 | 16 | export default Actions; 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GitHub Issues 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/markdown.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import marked from 'marked'; 4 | 5 | let Markdown = React.createClass({ 6 | 7 | getDefaultProps() { 8 | 9 | return { 10 | 11 | md: '' 12 | }; 13 | }, 14 | 15 | render() { 16 | 17 | let html = { __html:marked(this.props.md) }; 18 | 19 | return
; 20 | } 21 | }); 22 | 23 | export default Markdown; 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /src/utils/fetch.es6: -------------------------------------------------------------------------------- 1 | let fetch = function (url) { 2 | 3 | return new Promise(function (resolve, reject) { 4 | 5 | let xhr = new XMLHttpRequest(); 6 | 7 | xhr.open('GET', url); 8 | xhr.responseType = 'json'; 9 | xhr.onload = () => { 10 | 11 | if (xhr.status === 200) { 12 | 13 | resolve(xhr.response); 14 | } else { 15 | 16 | reject(xhr.status); 17 | } 18 | }; 19 | 20 | xhr.send(); 21 | }); 22 | }; 23 | 24 | export default fetch; 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | watch: true, 4 | entry: [ 5 | __dirname + '/src/app.jsx' 6 | ], 7 | module: { 8 | loaders: [ 9 | { test: /\.(jsx|es6)$/, exclude: /node_modules/, loaders: ['6to5-loader?optional=coreAliasing'] }, 10 | { test: /\.styl$/, loader: 'style-loader!css-loader!stylus-loader?paths=node_modules/' } 11 | ] 12 | }, 13 | output: { 14 | path: __dirname + '/dist', 15 | filename: 'app.js' 16 | }, 17 | resolve: { 18 | extensions: ['', '.js', '.jsx', '.es6', '.styl'] 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | 3 | let Button = React.createClass({ 4 | 5 | mixins: [React.addons.PureRenderMixin], 6 | 7 | getDefaultProps() { 8 | 9 | return { 10 | 11 | type: 'button', 12 | className: '' 13 | }; 14 | }, 15 | 16 | render() { 17 | 18 | return ( 19 | 20 | 23 | ); 24 | } 25 | 26 | }); 27 | 28 | export default Button; 29 | -------------------------------------------------------------------------------- /src/stores/user-store.es6: -------------------------------------------------------------------------------- 1 | import Store from '../lib/store'; 2 | import Constants from '../constants/app-constants'; 3 | 4 | let UserStore = new Store({ 5 | 6 | _state: { 7 | 8 | userName: '', 9 | repoName: '' 10 | } 11 | }); 12 | 13 | UserStore.bindAction(Constants.UPDATE_USER, action => { 14 | 15 | let id = UserStore.registerAction({ 16 | 17 | [Constants.UPDATE_USER_SUCCESS]: action.promise.resolve, 18 | [Constants.UPDATE_USER_ERROR]: action.promise.reject 19 | }); 20 | 21 | let nextState = action.payload.userData; 22 | 23 | UserStore.update(nextState, Constants.UPDATE_USER_SUCCESS, id); 24 | }); 25 | 26 | export default UserStore; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-issues", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/app.jsx", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "6to5-loader": "^2.0.0", 13 | "core-js": "^0.4.5", 14 | "css-loader": "^0.9.1", 15 | "eventemitter2": "^0.4.14", 16 | "file-loader": "^0.8.1", 17 | "flux": "^2.0.1", 18 | "marked": "^0.3.3", 19 | "react": "^0.12.2", 20 | "style-loader": "^0.8.3", 21 | "stylus": "^0.49.3", 22 | "stylus-loader": "^0.5.0", 23 | "webpack": "^1.4.15" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/issues-list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import IssueItem from '../components/issue-item'; 4 | import Pagination from '../components/pagination'; 5 | 6 | let IssuesList = React.createClass({ 7 | 8 | getDefaultProps() { 9 | 10 | return { 11 | 12 | issues: [], 13 | pagination: {} 14 | }; 15 | }, 16 | 17 | render() { 18 | 19 | let issuesList = this.props.issues.map((issue, index) => { 20 | 21 | return ; 22 | }); 23 | 24 | return ( 25 | 26 |
27 | 28 |
{issuesList}
29 |
30 | ); 31 | } 32 | 33 | }); 34 | 35 | export default IssuesList; 36 | -------------------------------------------------------------------------------- /src/lib/api.es6: -------------------------------------------------------------------------------- 1 | import fetch from '../utils/fetch'; 2 | 3 | import Dispatcher from '../dispatchers/dispatcher'; 4 | 5 | let API = { 6 | 7 | _fetch (id, url, { SUCCESS, ERROR }) { 8 | 9 | return new Promise((resolve, reject) => { 10 | 11 | fetch(url).then((response) => { 12 | 13 | this._dispatch({ 14 | 15 | id, 16 | actionType: SUCCESS, 17 | payload: { response } 18 | }); 19 | 20 | resolve(id); 21 | }) 22 | .catch((error) => { 23 | 24 | this._dispatch({ 25 | 26 | id, 27 | actionType: ERROR, 28 | payload: { error } 29 | }); 30 | 31 | reject(id); 32 | }); 33 | }); 34 | }, 35 | 36 | _dispatch (action) { 37 | 38 | Dispatcher.dispatch(action); 39 | } 40 | }; 41 | 42 | export default API; 43 | -------------------------------------------------------------------------------- /src/constants/app-constants.es6: -------------------------------------------------------------------------------- 1 | const Constants = { 2 | 3 | API_ROOT: 'https://api.github.com/', 4 | 5 | CHANGE_EVENT: Symbol('change'), 6 | 7 | FETCH_ISSUES: Symbol('fetch-issues'), 8 | FETCH_ISSUES_SUCCESS: Symbol('fetch-issues-success'), 9 | FETCH_ISSUES_ERROR: Symbol('fetch-issues-error'), 10 | 11 | FETCH_REPOS: Symbol('fetch-repos'), 12 | FETCH_REPOS_SUCCESS: Symbol('fetch-repos-success'), 13 | FETCH_REPOS_ERROR: Symbol('fetch-repos-error'), 14 | 15 | UPDATE_USER: Symbol('update-user'), 16 | UPDATE_USER_SUCCESS: Symbol('update-user-success'), 17 | UPDATE_USER_ERROR: Symbol('update-user-error'), 18 | 19 | PAGINATE: Symbol('paginate'), 20 | PAGINATE_SUCCESS: Symbol('paginate-success'), 21 | PAGINATE_ERROR: Symbol('paginate-error'), 22 | 23 | SET_PAGE_SIZE: Symbol('set-page-size'), 24 | SET_PAGE_SIZE_SUCCESS: Symbol('set-page-size-success'), 25 | SET_PAGE_SIZE_ERROR: Symbol('set-page-size-error'), 26 | 27 | SET_REPO: Symbol('set-repo') 28 | }; 29 | 30 | export default Constants; 31 | -------------------------------------------------------------------------------- /src/stores/issues-store.es6: -------------------------------------------------------------------------------- 1 | import GitHubAPI from '../api/github-api'; 2 | 3 | import Store from '../lib/store'; 4 | import Constants from '../constants/app-constants'; 5 | 6 | let IssuesStore = new Store({ 7 | 8 | _state: { 9 | 10 | issues: [] 11 | } 12 | }); 13 | 14 | IssuesStore.bindAction(Constants.FETCH_ISSUES, action => { 15 | 16 | let id = IssuesStore.registerAction({ 17 | 18 | [Constants.FETCH_ISSUES_SUCCESS]: action.promise.resolve, 19 | [Constants.FETCH_ISSUES_ERROR]: action.promise.reject 20 | }); 21 | 22 | GitHubAPI.fetchIssues(action.payload, id); 23 | }); 24 | 25 | IssuesStore.bindAction(Constants.FETCH_ISSUES_SUCCESS, action => { 26 | 27 | let nextState = { issues: action.payload.response }; 28 | 29 | IssuesStore.update(nextState, Constants.FETCH_ISSUES_SUCCESS, action.id); 30 | }); 31 | 32 | IssuesStore.bindAction(Constants.PAGINATE_SUCCESS, action => { 33 | 34 | let nextState = { issues: action.payload.response }; 35 | 36 | IssuesStore.update(nextState); 37 | }); 38 | 39 | export default IssuesStore; 40 | -------------------------------------------------------------------------------- /webpack-prod.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | 5 | watch: true, 6 | entry: [ 7 | __dirname + '/src/app.jsx' 8 | ], 9 | module: { 10 | loaders: [ 11 | { test: /\.(jsx|es6)$/, exclude: /node_modules/, loaders: ['6to5-loader?optional=coreAliasing'] }, 12 | { test: /\.styl$/, loader: 'style-loader!css-loader!stylus-loader?paths=node_modules/' } 13 | ], 14 | noParse: [__dirname + '/node_modules/react/dist/react-with-addons.min.js'] 15 | }, 16 | output: { 17 | path: __dirname + '/dist', 18 | filename: 'app.prod.js' 19 | }, 20 | resolve: { 21 | extensions: ['', '.js', '.jsx', '.es6', '.styl'], 22 | alias: { 23 | 'react$': __dirname + '/node_modules/react/dist/react-with-addons.min.js', 24 | 'react/addons$': __dirname + '/node_modules/react/dist/react-with-addons.min.js' 25 | } 26 | }, 27 | plugins: [ 28 | new webpack.optimize.DedupePlugin(), 29 | new webpack.optimize.UglifyJsPlugin() 30 | ] 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/dropdown-item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | 3 | let DropdownItem = React.createClass({ 4 | 5 | mixins: [React.addons.PureRenderMixin], 6 | 7 | getDefaultProps() { 8 | 9 | return { 10 | 11 | className: 'item', 12 | focus: false, 13 | index: 0 14 | }; 15 | }, 16 | 17 | componentDidUpdate() { 18 | 19 | if (this.props.focus) { this.getDOMNode().focus(); } 20 | }, 21 | 22 | _onKeyDown (event) { 23 | 24 | event.preventDefault(); 25 | 26 | if (event.keyCode === 13 || event.type === 'touchstart') { 27 | 28 | return this.props.onItemSelect(this.props.index); 29 | } 30 | 31 | this.props.onKeyDown(event); 32 | }, 33 | 34 | render() { 35 | 36 | return ( 37 | 38 |
  • {this.props.children}
  • 42 | ); 43 | } 44 | 45 | }); 46 | 47 | export default DropdownItem; 48 | -------------------------------------------------------------------------------- /src/components/repo-info.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | 3 | let RepoInfo = React.createClass({ 4 | 5 | mixins: [React.addons.PureRenderMixin], 6 | 7 | getDefaultProps() { 8 | 9 | return { 10 | 11 | repo: {} 12 | }; 13 | }, 14 | 15 | render() { 16 | 17 | let repo = this.props.repo; 18 | 19 | let component = ( 20 | 21 |
    22 | 23 |

    {repo.name}

    24 |

    {repo.description}

    25 | 26 |
    27 | {repo.language} 28 | {repo.stargazers_count} 29 | {repo.forks_count} 30 |
    31 | 32 |
    33 | ); 34 | 35 | component = !Object.keys(repo).length ? null : component; 36 | 37 | return component; 38 | } 39 | }); 40 | 41 | export default RepoInfo; 42 | -------------------------------------------------------------------------------- /src/stores/repos-store.es6: -------------------------------------------------------------------------------- 1 | import GitHubAPI from '../api/github-api'; 2 | 3 | import Store from '../lib/store'; 4 | import Constants from '../constants/app-constants'; 5 | 6 | let ReposStore = new Store({ 7 | 8 | _state: { 9 | 10 | repos: [], 11 | selectedRepo: {} 12 | } 13 | }); 14 | 15 | ReposStore.bindAction(Constants.FETCH_REPOS, action => { 16 | 17 | let id = ReposStore.registerAction({ 18 | 19 | [Constants.FETCH_REPOS_SUCCESS]: action.promise.resolve, 20 | [Constants.FETCH_REPOS_ERROR]: action.promise.reject 21 | }); 22 | 23 | GitHubAPI.fetchRepos(action.payload.userName, id); 24 | }); 25 | 26 | ReposStore.bindAction(Constants.FETCH_REPOS_SUCCESS, action => { 27 | 28 | let nextState = { repos: action.payload.response }; 29 | 30 | ReposStore.update(nextState, Constants.FETCH_REPOS_SUCCESS, action.id); 31 | }); 32 | 33 | ReposStore.bindAction(Constants.SET_REPO, action => { 34 | 35 | let selectedRepo = ReposStore.getState().repos 36 | .filter(repo => action.payload.repoName === repo.name); 37 | 38 | let nextState = selectedRepo.length === 1 ? 39 | { selectedRepo: selectedRepo[0] } : 40 | {}; 41 | 42 | ReposStore.update(nextState); 43 | }); 44 | 45 | export default ReposStore; 46 | -------------------------------------------------------------------------------- /src/stores/pagination-store.es6: -------------------------------------------------------------------------------- 1 | import GitHubAPI from '../api/github-api'; 2 | 3 | import Store from '../lib/store'; 4 | import Constants from '../constants/app-constants'; 5 | 6 | let PaginationStore = new Store({ 7 | 8 | _state: { 9 | 10 | currPage: 1, 11 | perPage: 10, 12 | disabled: true 13 | } 14 | }); 15 | 16 | PaginationStore.bindAction(Constants.PAGINATE, action => { 17 | 18 | let id = PaginationStore.registerAction({ 19 | 20 | [Constants.PAGINATE_SUCCESS]: action.promise.resolve, 21 | [Constants.PAGINATE_ERROR]: action.promise.reject 22 | }); 23 | 24 | GitHubAPI.paginate(action.payload, id) 25 | .then(() => { 26 | 27 | let nextState = action.payload.paginationData; 28 | 29 | PaginationStore.update(nextState, Constants.PAGINATE_SUCCESS, id); 30 | }); 31 | }); 32 | 33 | PaginationStore.bindAction(Constants.SET_PAGE_SIZE, action => { 34 | 35 | let id = PaginationStore.registerAction({ 36 | 37 | [Constants.SET_PAGE_SIZE_SUCCESS]: action.promise.resolve, 38 | [Constants.SET_PAGE_SIZE_ERROR]: action.promise.reject 39 | }); 40 | 41 | let nextState = action.payload; 42 | 43 | PaginationStore.update(nextState, Constants.SET_PAGE_SIZE_SUCCESS, id); 44 | }); 45 | 46 | export default PaginationStore; 47 | -------------------------------------------------------------------------------- /src/components/notification.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Dispatcher from '../dispatchers/dispatcher'; 4 | import Constants from '../constants/app-constants'; 5 | 6 | let Notification = React.createClass({ 7 | 8 | getInitialState() { 9 | 10 | return { 11 | 12 | show: false, 13 | message: '' 14 | }; 15 | }, 16 | 17 | componentWillReceiveProps ({ status }) { 18 | 19 | if (status) { 20 | 21 | let message = ''; 22 | 23 | switch (status) { 24 | 25 | case Constants.FETCH_REPOS_SUCCESS: 26 | 27 | message = 'Repos fetched successfully!'; 28 | break; 29 | 30 | case Constants.FETCH_ISSUES_SUCCESS: 31 | 32 | message = 'Issues fetched successfully!'; 33 | break; 34 | } 35 | 36 | this.setState({ show: true, message }, () => { 37 | 38 | setTimeout(() => this.setState({ show: false }), 2000); 39 | }); 40 | } 41 | }, 42 | 43 | render() { 44 | 45 | let notify = this.state.show ? 46 |
    {this.state.message}
    : null; 47 | 48 | return
    {notify}
    ; 49 | } 50 | 51 | }); 52 | 53 | export default Notification; 54 | -------------------------------------------------------------------------------- /src/actions/app-actions.es6: -------------------------------------------------------------------------------- 1 | import Constants from '../constants/app-constants'; 2 | 3 | import Actions from '../lib/actions'; 4 | 5 | let AppActions = { 6 | 7 | fetchRepos (userName) { 8 | 9 | return this._dispatch({ 10 | 11 | actionType: Constants.FETCH_REPOS, 12 | payload: { userName } 13 | }); 14 | }, 15 | 16 | updateUser (userData) { 17 | 18 | return this._dispatch({ 19 | 20 | actionType: Constants.UPDATE_USER, 21 | payload: { userData } 22 | }); 23 | }, 24 | 25 | submitForm (userData, paginationData) { 26 | 27 | return this._dispatch({ 28 | 29 | actionType: Constants.FETCH_ISSUES, 30 | payload: { userData, paginationData } 31 | }); 32 | }, 33 | 34 | paginate (userData, paginationData) { 35 | 36 | return this._dispatch({ 37 | 38 | actionType: Constants.PAGINATE, 39 | payload: { userData, paginationData } 40 | }); 41 | }, 42 | 43 | setPageSize (perPage) { 44 | 45 | return this._dispatch({ 46 | 47 | actionType: Constants.SET_PAGE_SIZE, 48 | payload: { perPage } 49 | }); 50 | }, 51 | 52 | setRepo (repoName) { 53 | 54 | return this._dispatch({ 55 | 56 | actionType: Constants.SET_REPO, 57 | payload: { repoName } 58 | }); 59 | } 60 | }; 61 | 62 | Object.assign(AppActions, Actions); 63 | 64 | export default AppActions; 65 | -------------------------------------------------------------------------------- /src/components/input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | let Input = React.createClass({ 4 | 5 | getDefaultProps() { 6 | 7 | return { 8 | 9 | wrapperClassName: 'field', 10 | labelClassName: 'label', 11 | type: 'text', 12 | min: '', 13 | value: '', 14 | placeholder: '', 15 | required: false, 16 | disabled: false 17 | }; 18 | }, 19 | 20 | getValue() { 21 | 22 | return this.refs.input.getDOMNode().value; 23 | }, 24 | 25 | render() { 26 | 27 | return ( 28 | 29 |
    30 | 31 | 32 | 33 | 44 | 45 | {this.props.children} 46 | 47 |
    48 | ); 49 | } 50 | 51 | }); 52 | 53 | export default Input; 54 | -------------------------------------------------------------------------------- /src/api/github-api.es6: -------------------------------------------------------------------------------- 1 | import API from '../lib/api'; 2 | 3 | import Constants from '../constants/app-constants'; 4 | 5 | let GitHubAPI = { 6 | 7 | fetchIssues (payload, id) { 8 | 9 | let url = this._getIssuesURL(payload); 10 | 11 | return this._fetch.call(this, id, url, { 12 | 13 | SUCCESS: Constants.FETCH_ISSUES_SUCCESS, 14 | ERROR: Constants.FETCH_ISSUES_ERROR 15 | }); 16 | }, 17 | 18 | paginate (payload, id) { 19 | 20 | let url = this._getIssuesURL(payload); 21 | 22 | return this._fetch.call(this, id, url, { 23 | 24 | SUCCESS: Constants.PAGINATE_SUCCESS, 25 | ERROR: Constants.PAGINATE_ERROR 26 | }); 27 | }, 28 | 29 | fetchRepos (userName, id) { 30 | 31 | let url = Constants.API_ROOT + 'users/' + userName + '/repos'; 32 | url += '?per_page=9999'; 33 | 34 | return this._fetch.call(this, id, url, { 35 | 36 | SUCCESS: Constants.FETCH_REPOS_SUCCESS, 37 | ERROR: Constants.FETCH_REPOS_ERROR 38 | }); 39 | }, 40 | 41 | _getIssuesURL ({ userData, paginationData }) { 42 | 43 | let { userName, repoName } = userData, 44 | { currPage, perPage } = paginationData; 45 | 46 | let url = Constants.API_ROOT + 'repos/' + userName + '/' + repoName + '/issues'; 47 | url += '?page=' + currPage + '&per_page=' + perPage; 48 | 49 | return url; 50 | } 51 | }; 52 | 53 | Object.assign(GitHubAPI, API); 54 | 55 | export default GitHubAPI; 56 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ReposStore from './stores/repos-store'; 4 | import IssuesStore from './stores/issues-store'; 5 | 6 | import SearchForm from './components/search-form'; 7 | import IssuesList from './components/issues-list'; 8 | import Notification from './components/notification'; 9 | import RepoInfo from './components/repo-info'; 10 | 11 | require('../styl/main.styl'); 12 | 13 | React.initializeTouchEvents(true); 14 | 15 | let App = React.createClass({ 16 | 17 | getInitialState() { 18 | 19 | return { 20 | 21 | status: '' 22 | }; 23 | }, 24 | 25 | componentDidMount() { 26 | 27 | ReposStore.addChangeListener(this._onChange); 28 | IssuesStore.addChangeListener(this._onChange); 29 | }, 30 | 31 | componentWillUnmount() { 32 | 33 | ReposStore.removeChangeListener(this._onChange); 34 | IssuesStore.removeChangeListener(this._onChange); 35 | }, 36 | 37 | _onChange (status) { 38 | 39 | this.setState({ status }); 40 | }, 41 | 42 | render() { 43 | 44 | let repo = ReposStore.getState().selectedRepo; 45 | let issues = IssuesStore.getState().issues; 46 | 47 | return ( 48 | 49 |
    50 | 51 |
    52 | 53 | 54 |
    55 | 56 | 57 | 58 | 59 |
    60 | ); 61 | } 62 | 63 | }); 64 | 65 | React.render(, document.body); 66 | -------------------------------------------------------------------------------- /src/lib/store.es6: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter2'; 2 | 3 | import Dispatcher from '../dispatchers/dispatcher'; 4 | import Constants from '../constants/app-constants'; 5 | 6 | class Store extends EventEmitter { 7 | 8 | constructor (defs) { 9 | 10 | this._state = {}; 11 | this._status = null; 12 | this._actions = {}; 13 | this._callbacks = {}; 14 | 15 | this.dispatchToken = Dispatcher.register(this.handleAction.bind(this)); 16 | 17 | Object.assign(this, defs); 18 | } 19 | 20 | update (nextState, type, id) { 21 | 22 | Object.assign(this._state, nextState); 23 | 24 | if (id && type) { 25 | 26 | this._actions[id][type](); 27 | delete this._actions[id]; 28 | } 29 | 30 | this._status = type; 31 | 32 | this.emitChange(); 33 | } 34 | 35 | getState() { 36 | 37 | return this._state; 38 | } 39 | 40 | emitChange() { 41 | 42 | this.emit(Constants.CHANGE_EVENT, this._status); 43 | } 44 | 45 | addChangeListener (callback) { 46 | 47 | this.on(Constants.CHANGE_EVENT, callback); 48 | } 49 | 50 | removeChangeListener (callback) { 51 | 52 | this.removeListener(Constants.CHANGE_EVENT, callback); 53 | } 54 | 55 | registerAction (promise) { 56 | 57 | let id = Symbol(); 58 | 59 | this._actions[id] = promise; 60 | 61 | return id; 62 | } 63 | 64 | bindAction (type, callback) { 65 | 66 | this._callbacks[type] = this._callbacks[type] || []; 67 | 68 | this._callbacks[type].push(callback); 69 | } 70 | 71 | handleAction (action) { 72 | 73 | let callbacks = this._callbacks[action.actionType]; 74 | 75 | callbacks && callbacks.map(callback => callback(action)); 76 | } 77 | }; 78 | 79 | export default Store; 80 | -------------------------------------------------------------------------------- /src/components/dropdown.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import DropdownItem from './dropdown-item'; 4 | 5 | let Dropdown = React.createClass({ 6 | 7 | getDefaultProps() { 8 | 9 | return { 10 | 11 | className: '', 12 | repos: [], 13 | enter: false 14 | }; 15 | }, 16 | 17 | getInitialState() { 18 | 19 | return { 20 | 21 | activeItem: 0 22 | }; 23 | }, 24 | 25 | componentWillReceiveProps (nextProps) { 26 | 27 | if (nextProps.enter) { 28 | 29 | this.setState({ activeItem: 1 }); 30 | } 31 | }, 32 | 33 | _onItemKeyDown (event) { 34 | 35 | event.preventDefault(); 36 | 37 | switch (event.keyCode) { 38 | 39 | case 40: 40 | 41 | this.state.activeItem < this.props.repos.length && 42 | this.setState({ activeItem: this.state.activeItem + 1 }); 43 | 44 | break; 45 | 46 | case 38: 47 | 48 | this.state.activeItem && 49 | this.setState({ activeItem: this.state.activeItem - 1 }, () => { 50 | 51 | !this.state.activeItem && this.props.out(); 52 | }); 53 | 54 | break; 55 | } 56 | }, 57 | 58 | render() { 59 | 60 | let list = this.props.repos.map((repo, index) => { 61 | 62 | return {repo}; 67 | }); 68 | 69 | return ( 70 | 71 |
    72 | 73 |
      {list}
    74 | 75 |
    76 | ); 77 | } 78 | 79 | }); 80 | 81 | export default Dropdown; 82 | -------------------------------------------------------------------------------- /src/components/issue-item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Markdown from './markdown'; 4 | 5 | let IssueItem = React.createClass({ 6 | 7 | _dictionary: { 8 | 9 | months: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'] 10 | }, 11 | 12 | getDefaultProps() { 13 | 14 | return { 15 | 16 | issue: {} 17 | }; 18 | }, 19 | 20 | getInitialState() { 21 | 22 | return { 23 | 24 | showIssue: false 25 | }; 26 | }, 27 | 28 | componentWillReceiveProps (nextProps) { 29 | 30 | nextProps.issue !== this.props.issue && this.state.showIssue && 31 | this.setState({ showIssue: false }); 32 | }, 33 | 34 | _getDate (timestamp) { 35 | 36 | let date = new Date(timestamp); 37 | let prettyDate = ''; 38 | 39 | prettyDate += this._dictionary.months[date.getMonth()]; 40 | prettyDate += ' ' + date.getDate(); 41 | prettyDate += ', '; 42 | prettyDate += date.getFullYear(); 43 | prettyDate += ', ' + date.toLocaleTimeString(); 44 | 45 | return prettyDate; 46 | }, 47 | 48 | _toggle (event) { 49 | 50 | if (event.type === 'click' || event.keyCode === 13) { 51 | 52 | event.preventDefault(); 53 | 54 | this.setState({ showIssue: !this.state.showIssue }); 55 | } 56 | }, 57 | 58 | render() { 59 | 60 | let date = this._getDate(this.props.issue.created_at); 61 | 62 | let issue =
    ; 63 | 64 | return ( 65 | 66 |
    67 | 68 |
    69 | 70 | 75 | 76 |
    {this.props.issue.title}
    82 | 83 |
    84 | 85 |
    86 | 87 | {this.state.showIssue ? issue : null} 88 | 89 |
    90 | ); 91 | } 92 | 93 | }); 94 | 95 | export default IssueItem; 96 | -------------------------------------------------------------------------------- /src/components/pagination.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | 3 | import AppActions from '../actions/app-actions'; 4 | 5 | import IssuesStore from '../stores/issues-store'; 6 | import PaginationStore from '../stores/pagination-store'; 7 | import UserStore from '../stores/user-store'; 8 | import ReposStore from '../stores/repos-store'; 9 | 10 | import Input from './input'; 11 | import Button from './button'; 12 | 13 | let Pagination = React.createClass({ 14 | 15 | mixins: [React.addons.PureRenderMixin], 16 | 17 | getDefaultProps() { 18 | 19 | return { 20 | 21 | minPage: 1, 22 | minPerPage: 1 23 | } 24 | }, 25 | 26 | getInitialState() { 27 | 28 | return PaginationStore.getState(); 29 | }, 30 | 31 | componentDidMount() { 32 | 33 | IssuesStore.addChangeListener(this._onUpdate); 34 | }, 35 | 36 | componentWillUnmount() { 37 | 38 | IssuesStore.removeChangeListener(this._onUpdate); 39 | }, 40 | 41 | _onUpdate() { 42 | 43 | this.state.disabled && this.setState({ disabled: false }); 44 | }, 45 | 46 | _onChange() { 47 | 48 | this._updatePage({ 49 | 50 | currPage: Number(this.refs.currPage.getValue()), 51 | perPage: Number(this.refs.perPage.getValue()) 52 | }); 53 | }, 54 | 55 | _incPage() { 56 | 57 | if (this.state.disabled) { return; } 58 | 59 | this._updatePage({ currPage: this.state.currPage + 1 }); 60 | }, 61 | 62 | _decPage() { 63 | 64 | if (this.state.disabled || this.state.currPage === this.props.minPage) { return; } 65 | 66 | this._updatePage({ currPage: this.state.currPage - 1 }); 67 | }, 68 | 69 | _updatePage (nextState) { 70 | 71 | this.setState(nextState, () => { 72 | 73 | AppActions.setPageSize(this.state.perPage) 74 | .then(() => { 75 | 76 | if (Object.keys(ReposStore.getState().selectedRepo).length) { 77 | 78 | AppActions.paginate(UserStore.getState(), this.state); 79 | } 80 | }); 81 | }); 82 | }, 83 | 84 | render() { 85 | 86 | return ( 87 | 88 |
    89 | 90 | 96 | 97 | 98 | 99 | 106 | 107 | 108 | 109 |
    110 | ); 111 | } 112 | 113 | }); 114 | 115 | export default Pagination; 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Issues 2 | Sample React application built with Flux for learning purpose 3 | 4 | `npm install -g webpack && npm install && webpack` 5 | 6 | ## Flux Action 7 | 8 | [`src/lib/actions.es6`](https://github.com/roman01la/github-issues/blob/master/src/lib/actions.es6) 9 | 10 | `Actions` module is a proto object with a single method `._dispatch`. Application actions should be defined within an object extended by proto object. 11 | 12 | ``` 13 | let ItemsActions = {}; 14 | 15 | Object.assign(ItemsActions, Actions); 16 | ``` 17 | 18 | Action producer can be notified about the status of the particular action. `._dispatch` returns a promise which can be optionally obtained by receiver store and handled accordingly. 19 | 20 | ``` 21 | ItemsActions.fetch = url => { 22 | 23 | return this._dispatch({ 24 | 25 | actionType: Constants.FETCH_ITEMS, 26 | payload: { url } 27 | }); 28 | }; 29 | 30 | /* Show loader here */ 31 | 32 | ItemsActions.fetch(url) 33 | .then(/* Hide loader */) 34 | .catch(/* Show error msg */); 35 | ``` 36 | 37 | ## Flux Store 38 | 39 | [`src/lib/store.es6`](https://github.com/roman01la/github-issues/blob/master/src/lib/store.es6) 40 | 41 | `Store` module is a class which handles data, actions, updates and notifications. 42 | 43 | `_state` prop is where all the data is stored within the store, it's optional when creating a new store, but defining store's structure still can help. 44 | 45 | ``` 46 | let ItemsStore = new Store({ 47 | 48 | _state: { 49 | 50 | items: [] 51 | } 52 | }); 53 | ``` 54 | 55 | Listen for changes from within of the component and use `.getState()` method to retrieve recent data from the store. 56 | ``` 57 | componentDidMount() { 58 | 59 | ItemsStore.addChangeListener(this._onChange); 60 | }, 61 | 62 | componentWillUnmount() { 63 | 64 | ItemsStore.removeChangeListener(this._onChange); 65 | }, 66 | 67 | _onChange() { 68 | 69 | this.setState(ItemsStore.getState()); 70 | } 71 | ``` 72 | 73 | Use `.bindAction` method to listen for particular actions and do things like calling API inside of the callback fn, which accepts dispatched action. 74 | 75 | ``` 76 | ItemsStore.bindAction(Constants.FETCH_ITEMS, action => { 77 | 78 | API.fetchItems(action.payload); 79 | }); 80 | ``` 81 | To notify back action producer you should register incoming action inside of a callback fn with `.registerAction` method, which accepts an object where keys are action type constants for handling success and error, and values are injected into the action promise's `resolve` and `reject` fns. This will return a unique `id`, which then should be passed as an argument to an `API` module. 82 | 83 | ``` 84 | ItemsStore.bindAction(Constants.FETCH_ITEMS, action => { 85 | 86 | let id = ItemsStore.registerAction({ 87 | 88 | [Constants.FETCH_ITEMS_SUCCESS]: action.promise.resolve, 89 | [Constants.FETCH_ITEMS_ERROR]: action.promise.reject 90 | }); 91 | 92 | API.fetchItems(action.payload, id); 93 | }); 94 | ``` 95 | 96 | When done, `API` module will dispatch an action with API response and injected `id`, if provided. Store can update itself when listening for `API` actions, automatically emit `CHANGE_EVENT` event and optionally notify action producer. 97 | 98 | ``` 99 | ItemsStore.bindAction(Constants.FETCH_ITEMS_SUCCESS, action => { 100 | 101 | let nextState = { items: action.payload.response }; 102 | 103 | /* Update store */ 104 | ItemsStore.update(nextState); 105 | 106 | /* OR */ 107 | 108 | /* Update store and notify action producer */ 109 | ItemsStore.update(nextState, Constants.FETCH_ITEMS_SUCCESS, action.id); 110 | }); 111 | ``` 112 | 113 | Additionally `CHANGE_EVENT` will be emitted along with provided store's `status`, and so components which are listening for changes can distinguish between succeed or failed changes and respond accordingly. 114 | 115 | ``` 116 | ItemsStore.update(nextState, Constants.FETCH_ITEMS_SUCCESS); 117 | 118 | _onChange (status) { 119 | 120 | if (status === Constants.FETCH_ITEMS_SUCCESS) { 121 | 122 | this.setState(ItemsStore.getState()); 123 | } 124 | } 125 | ``` 126 | 127 | Doing everything explicitly might be confusing and looks like an overhead, but it's still optional functionality and you might don't want to use it everywhere. 128 | 129 | ## Flux API 130 | 131 | [`sr/lib/api.es6`](https://github.com/roman01la/github-issues/blob/master/src/lib/api.es6) 132 | 133 | `API` module is a proto object. Application API calls should be defined within an object extended by proto object. 134 | 135 | ``` 136 | let ItemsAPI = {}; 137 | 138 | Object.assign(ItemsAPI, Actions); 139 | ``` 140 | 141 | `._fetch` method accepts `id`, `url` and action type constants for dispatching success or error actions. 142 | 143 | ``` 144 | ItemsAPI.fetchItems = (payload, id) { 145 | 146 | return this._fetch.call(this, id, payload.url, { 147 | 148 | SUCCESS: Constants.FETCH_ITEMS_SUCCESS, 149 | ERROR: Constants.FETCH_ITEMS_ERROR 150 | }); 151 | }; 152 | ``` 153 | 154 | `._fetch` returns a promise which can be used to immediately update store. 155 | 156 | ``` 157 | ItemsStore.update(nextState) 158 | .then(/* ... */) 159 | .catch(/* ... */); 160 | ``` 161 | 162 | ## Flux Dispatcher 163 | [Facebook's Flux `Dispatcher`](https://github.com/facebook/flux/blob/master/src/Dispatcher.js) 164 | -------------------------------------------------------------------------------- /styl/main.styl: -------------------------------------------------------------------------------- 1 | /* Normalize */ 2 | 3 | html { 4 | box-sizing border-box 5 | } 6 | html * { 7 | box-sizing inherit 8 | } 9 | body { 10 | margin 0 11 | font normal 16px sans-serif 12 | } 13 | 14 | /* Global */ 15 | 16 | input { 17 | padding 4px 18 | height 26px 19 | border-radius 2px 20 | border 1px solid #ccc 21 | } 22 | label { 23 | font-size .8rem 24 | } 25 | 26 | button { 27 | display block 28 | padding 13px 29 | border none 30 | border-radius 2px 31 | background #479BE2 32 | font-size 1rem 33 | color #fafafa 34 | line-height 0 35 | margin 0 36 | } 37 | a { 38 | color #479BE2 39 | text-decoration none 40 | 41 | &:hover { 42 | text-decoration underline 43 | } 44 | } 45 | h3 { 46 | font-weight normal 47 | margin .5rem 0 48 | } 49 | pre { 50 | background #eee 51 | padding 10px 52 | border-radius 2px 53 | } 54 | code { 55 | background #eee 56 | padding 0 4px 57 | border-radius 2px 58 | } 59 | pre code { 60 | word-wrap break-word 61 | padding 0 62 | } 63 | 64 | /* Application */ 65 | 66 | .app { 67 | padding 10px 68 | margin 0 auto 69 | max-width 768px 70 | } 71 | 72 | /* Top section */ 73 | 74 | .top { 75 | display flex 76 | 77 | .search-form, 78 | .repo-info { 79 | flex 1 80 | } 81 | .stats { 82 | margin 20px 0 0 83 | 84 | span { 85 | margin 0 10px 86 | 87 | &:first-child { 88 | margin 0 10px 0 0 89 | } 90 | &:last-child { 91 | margin 0 0 0 10px 92 | } 93 | } 94 | .fa::before { 95 | margin 0 5px 0 0 96 | } 97 | } 98 | } 99 | 100 | .repo-name { 101 | margin-bottom 0 102 | } 103 | 104 | /* Search form */ 105 | 106 | .search-form { 107 | max-width 135px 108 | margin 0 20px 0 0 109 | } 110 | 111 | .search-form .field { 112 | margin 10px 0 113 | position relative 114 | 115 | label, 116 | input { 117 | display block 118 | } 119 | 120 | .fa-spinner { 121 | position absolute 122 | bottom 5px 123 | right 10px 124 | transform rotate(0deg) 125 | animation spin 1000ms linear infinite 126 | } 127 | } 128 | 129 | @keyframes spin { 130 | to { 131 | transform rotate(360deg) 132 | } 133 | } 134 | 135 | /* Dropdown */ 136 | 137 | .dropdown { 138 | font-size .7rem 139 | position relative 140 | 141 | .list { 142 | margin 0 143 | padding 0 144 | list-style none 145 | position absolute 146 | top 5px 147 | background #ffffff 148 | } 149 | .item { 150 | padding 2px 151 | border-bottom 1px solid #242424 152 | 153 | &:focus { 154 | background #479BE2 155 | color #fafafa 156 | } 157 | } 158 | } 159 | 160 | /* Issues list */ 161 | 162 | .issue { 163 | font-size .8rem 164 | display flex 165 | display -webkit-flex 166 | flex-direction column 167 | -webkit-flex-direction column 168 | 169 | .issue-info { 170 | display flex 171 | display -webkit-flex 172 | justify-content space-between 173 | -webkit-justify-content space-between 174 | border-bottom 1px solid #242424 175 | padding 10px 0 5px 176 | } 177 | .title { 178 | align-self flex-start 179 | -webkit-align-self flex-start 180 | flex 1 181 | -webkit-flex 1 182 | margin 0 20px 183 | 184 | &:hover { 185 | cursor pointer 186 | } 187 | } 188 | .time { 189 | font-size .7rem 190 | } 191 | 192 | .issue-body { 193 | padding 10px 0 194 | line-height 1.2rem 195 | border-bottom 1px dashed #242424 196 | margin 0 0 20px 197 | 198 | img { 199 | width 100% 200 | } 201 | } 202 | } 203 | 204 | /* Pagination form */ 205 | 206 | .pagination { 207 | text-align center 208 | margin 30px 0 209 | 210 | .prev, 211 | .next, 212 | .field { 213 | display inline-block 214 | vertical-align middle 215 | } 216 | input[type="number"] { 217 | width 50px 218 | text-align center 219 | 220 | &:disabled { 221 | background #ccc 222 | } 223 | } 224 | .field { 225 | margin 0 20px 226 | 227 | label { 228 | margin 0 10px 0 0 229 | } 230 | } 231 | } 232 | 233 | /* Notification */ 234 | 235 | .notification { 236 | position fixed 237 | top 0 238 | right 0 239 | 240 | .message { 241 | background #fafafa 242 | color #242424 243 | border 1px solid #242424 244 | padding 10px 245 | } 246 | } 247 | 248 | @media only screen and (max-width: 440px) { 249 | 250 | input { 251 | height 30px 252 | } 253 | 254 | .top { 255 | flex-direction column 256 | } 257 | .search-form { 258 | margin 0 259 | max-width none 260 | 261 | input[type="text"] { 262 | width 100% 263 | } 264 | } 265 | .dropdown .item { 266 | padding 6px 2px 267 | } 268 | .pagination { 269 | text-align right 270 | 271 | .field:first-child { 272 | margin 0 20px 10px 273 | } 274 | } 275 | .btn.prev { 276 | margin 0 0 10px 277 | } 278 | .issue-info { 279 | flex-direction column 280 | -webkit-flex-direction column 281 | 282 | .num, 283 | .title, 284 | .time { 285 | flex 1 286 | -webkit-flex 1 287 | } 288 | .num { 289 | max-width 50px 290 | } 291 | .title { 292 | margin 6px 0 293 | padding 4px 0 294 | } 295 | } 296 | .notification { 297 | width 100% 298 | } 299 | } 300 | 301 | -------------------------------------------------------------------------------- /src/components/search-form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import _core from 'core-js/library'; 4 | 5 | import AppActions from '../actions/app-actions'; 6 | 7 | import ReposStore from '../stores/repos-store'; 8 | import UserStore from '../stores/user-store'; 9 | import PaginationStore from '../stores/pagination-store'; 10 | 11 | import Dropdown from './dropdown'; 12 | import Button from './button'; 13 | import Input from './input'; 14 | 15 | let SearchForm = React.createClass({ 16 | 17 | _prev: { 18 | 19 | repoName: '' 20 | }, 21 | 22 | getInitialState() { 23 | 24 | return { 25 | 26 | user: { 27 | 28 | userName: '', 29 | repoName: '' 30 | }, 31 | 32 | matchedRepos: [], 33 | showDropdown: false, 34 | enterDropdown: false, 35 | repoSelected: false, 36 | loading: false 37 | }; 38 | }, 39 | 40 | componentDidUpdate (prevProps, { user: { userName, repoName } }) { 41 | 42 | let user = this.state.user; 43 | 44 | if (userName !== user.userName || 45 | repoName !== user.repoName) { 46 | 47 | AppActions.updateUser(user); 48 | } 49 | }, 50 | 51 | _updateUser() { 52 | 53 | return new Promise((resolve, reject) => { 54 | 55 | this.setState({ user: { 56 | 57 | userName: this.refs.userName.getValue(), 58 | repoName: this.refs.repoName.getValue() 59 | } }, resolve); 60 | }); 61 | }, 62 | 63 | _matchRepos() { 64 | 65 | let repoName = this.state.user.repoName; 66 | 67 | return repoName ? 68 | 69 | ReposStore.getState().repos 70 | .map(repo => repo.name) 71 | .filter(name => _core.String.startsWith(name, repoName)) : 72 | 73 | []; 74 | }, 75 | 76 | _onNameChange() { 77 | 78 | this._updateUser(); 79 | }, 80 | 81 | _onRepoChange() { 82 | 83 | this._updateUser() 84 | .then(() => { 85 | 86 | let nextState = { matchedRepos: this._matchRepos() }; 87 | 88 | !this.state.showDropdown && 89 | (nextState.showDropdown = true); 90 | 91 | this.setState(nextState); 92 | }); 93 | }, 94 | 95 | _onRepoFocus() { 96 | 97 | let userName = this.state.user.userName; 98 | 99 | if (userName !== this._prev.repoName) { 100 | 101 | this.setState({ loading: true }); 102 | 103 | this._prev.repoName = userName; 104 | AppActions.fetchRepos(userName) 105 | .then(() => this.setState({ 106 | 107 | showDropdown: true, 108 | matchedRepos: this._matchRepos(), 109 | loading: false 110 | })); 111 | 112 | } else { 113 | 114 | !this.state.showDropdown && this.setState({ showDropdown: true }); 115 | } 116 | 117 | if (this.state.repoSelected && this.state.showDropdown) { 118 | 119 | this.setState({ 120 | 121 | showDropdown: false, 122 | repoSelected: false 123 | }); 124 | } 125 | }, 126 | 127 | _onRepoBlur() { 128 | 129 | !this.state.enterDropdown && this.setState({ showDropdown: false }); 130 | }, 131 | 132 | _onRepoKeyDown (event) { 133 | 134 | if (event.keyCode === 40) { 135 | 136 | event.preventDefault(); 137 | 138 | this.setState({ enterDropdown: true }); 139 | } 140 | }, 141 | 142 | _focusToRepoName() { 143 | 144 | this.setState({ enterDropdown: false}, () => { 145 | 146 | this.refs.repoName.refs.input.getDOMNode().focus(); 147 | }); 148 | }, 149 | 150 | _onItemSelect (index) { 151 | 152 | let repoName = this.state.matchedRepos[index]; 153 | 154 | this.refs.repoName.refs.input.getDOMNode().value = repoName; 155 | 156 | this._updateUser() 157 | .then(() => { 158 | 159 | this.setState({ repoSelected: true }, () => { 160 | 161 | this._focusToRepoName(); 162 | 163 | this._submitForm(); 164 | }); 165 | }); 166 | }, 167 | 168 | _onSubmit (event) { 169 | 170 | event.preventDefault(); 171 | 172 | this._submitForm(); 173 | }, 174 | 175 | _submitForm() { 176 | 177 | return AppActions.submitForm(UserStore.getState(), PaginationStore.getState()) 178 | .then(() => AppActions.setRepo(this.state.user.repoName)); 179 | }, 180 | 181 | render() { 182 | 183 | let loader = ; 184 | 185 | let dropdown = ( 186 | 187 | 192 | ); 193 | 194 | dropdown = this.state.matchedRepos.length && this.state.showDropdown ? 195 | dropdown : null; 196 | 197 | return ( 198 | 199 |
    200 | 201 | 206 | 207 | 215 | 216 | {this.state.loading ? loader : null} 217 | {dropdown} 218 | 219 | 220 | 221 | 222 | 223 |
    224 | ); 225 | } 226 | }); 227 | 228 | export default SearchForm; 229 | --------------------------------------------------------------------------------