├── .gitignore ├── .babelrc ├── README.md ├── src ├── app │ ├── entrypoints │ │ └── main.js │ ├── ducks │ │ ├── index.js │ │ └── searchResults.js │ ├── store │ │ └── configureStore.js │ ├── containers │ │ ├── root │ │ │ └── Root.js │ │ └── app │ │ │ └── App.js │ └── components │ │ ├── searchresult │ │ └── SearchResult.js │ │ ├── searchinput │ │ └── SearchInput.js │ │ └── searchresults │ │ └── SearchResults.js ├── index.html └── utils │ └── GITHUBAPIUtils.js ├── package.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dist/ 3 | html/ 4 | node_modules/ 5 | specification/ 6 | *.iml 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "optional": [ 4 | "runtime" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | reactjs-and-cutting-edge-stack 2 | 3 | Как запустить сервер для разработки: 4 | $ npm install 5 | $ npm run dev 6 | -------------------------------------------------------------------------------- /src/app/entrypoints/main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import Root from "containers/root/Root"; 4 | 5 | render(, document.getElementById("container")); 6 | -------------------------------------------------------------------------------- /src/app/ducks/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import searchResults from 'ducks/searchResults'; 3 | 4 | const rootReducer = combineReducers({ 5 | searchResults 6 | }); 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Github search 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux"; 2 | import rootReducer from "ducks"; 3 | import thunk from "redux-thunk"; 4 | import createLogger from "redux-logger"; 5 | 6 | const createStoreWithMiddlewares = applyMiddleware( 7 | thunk, 8 | createLogger() 9 | )(createStore); 10 | 11 | export default createStoreWithMiddlewares(rootReducer); 12 | -------------------------------------------------------------------------------- /src/app/containers/root/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Provider } from "react-redux"; 3 | import store from "store/configureStore"; 4 | import App from "containers/app/App"; 5 | 6 | export default class Root extends Component { 7 | render() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/components/searchresult/SearchResult.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from "react"; 2 | 3 | export default class SearchResult extends Component { 4 | 5 | static propTypes = { 6 | result: PropTypes.object.isRequired 7 | } 8 | 9 | render() { 10 | const { 11 | result: { 12 | owner: { login: authorName }, 13 | html_url: repositoryPath, 14 | full_name: repositoryName 15 | } 16 | } = this.props; 17 | 18 | return ( 19 |
  • 20 |

    {authorName}

    21 | {repositoryName} 22 |
  • 23 | ); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/components/searchinput/SearchInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from "react"; 2 | import emptyFunction from "fbjs/lib/emptyFunction"; 3 | 4 | export default class SearchInput extends Component { 5 | 6 | static propTypes = { 7 | onSearch: PropTypes.func 8 | } 9 | 10 | static defaultProps = { 11 | onSearch: emptyFunction 12 | } 13 | 14 | render() { 15 | return ( 16 |
    17 | this.input = c} 20 | /> 21 | 22 | 23 |
    24 | ); 25 | } 26 | 27 | onFormSubmit = (evt) => { 28 | evt.preventDefault(); 29 | this.props.onSearch(this.input.value) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/GITHUBAPIUtils.js: -------------------------------------------------------------------------------- 1 | import { fetch, Headers } from "whatwg-fetch"; 2 | 3 | export default { 4 | 5 | async search(term) { 6 | const headers = new Headers(); 7 | headers.append("Accept", "application/json"); 8 | headers.append("Content-Type", "application/json"); 9 | 10 | const query = JSON.stringify({ q: term }); 11 | 12 | let response = await fetch( 13 | "https://api.github.com/search/repositories?q=" + encodeURIComponent(term), 14 | { 15 | method: "GET", 16 | headers 17 | } 18 | ); 19 | 20 | if (response.status < 200 || response.status >= 300) { 21 | throw new Error(response.statusText); 22 | } 23 | 24 | response = await response.json(); 25 | 26 | return response.items; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/app/ducks/searchResults.js: -------------------------------------------------------------------------------- 1 | import { search as searchAPI } from "utils/GITHUBAPIUtils"; 2 | 3 | const SEARCH = "RCES/searchResults/SEARCH"; 4 | 5 | const initialState = { 6 | isLoading: false, 7 | results: null 8 | }; 9 | 10 | export default function reducer(state = initialState, action = {}) { 11 | const { type, payload } = action; 12 | 13 | switch(type) { 14 | case SEARCH: 15 | return Object.assign( 16 | {}, 17 | state, 18 | payload ? { 19 | results: payload, 20 | isLoading: false 21 | } 22 | : 23 | { 24 | results: [], 25 | isLoading: true 26 | } 27 | ); 28 | 29 | default: 30 | return state; 31 | } 32 | } 33 | 34 | export function search(query) { 35 | return async function(dispatch, getState) { 36 | dispatch({ 37 | type: SEARCH 38 | }); 39 | 40 | const results = await searchAPI(query); 41 | 42 | return dispatch({ 43 | type: SEARCH, 44 | payload: results 45 | }); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/app/components/searchresults/SearchResults.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from "react"; 2 | import shallowCompare from "react-addons-shallow-compare"; 3 | import SearchResult from "components/searchresult/SearchResult"; 4 | 5 | export default class SearchResults extends Component { 6 | 7 | static propTypes = { 8 | results: PropTypes.array 9 | } 10 | 11 | static defaultProps = { 12 | results: [] 13 | } 14 | 15 | shouldComponentUpdate = (nextProps, nextState) => shallowCompare(this, nextProps, nextState) 16 | 17 | render() { 18 | const { 19 | results 20 | } = this.props; 21 | 22 | if (!results) { 23 | return null; 24 | } else if (results.length > 0) { 25 | return ( 26 | 33 | ); 34 | } else { 35 | return ( 36 |

    There is no repositories with such name

    37 | ); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/app/containers/app/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from "react"; 2 | import { connect } from "react-redux"; 3 | import SearchInput from "components/searchinput/SearchInput"; 4 | import SearchResults from "components/searchresults/SearchResults"; 5 | import { search } from "ducks/searchResults"; 6 | 7 | class App extends Component { 8 | 9 | static propTypes = { 10 | results: PropTypes.array, 11 | isLoading: PropTypes.bool.isRequired 12 | } 13 | 14 | constructor(...args) { 15 | super(...args); 16 | 17 | this.onSearchInputSearch = this.onSearchInputSearch.bind(this); 18 | } 19 | 20 | render() { 21 | const { 22 | results, 23 | isLoading 24 | } = this.props; 25 | 26 | return ( 27 |
    28 | 29 | {isLoading ? 30 |

    Loading...

    31 | : 32 | 33 | } 34 |
    35 | ); 36 | } 37 | 38 | onSearchInputSearch(query) { 39 | this.props.dispatch(search(query)); 40 | } 41 | 42 | } 43 | 44 | const mapStateToProps = ({ searchResults: { results, isLoading } }) => ({ 45 | results, 46 | isLoading 47 | }); 48 | 49 | export default connect(mapStateToProps)(App); 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RCES", 3 | "version": "0.0.1", 4 | "description": "React & cutting edge stack hackathon", 5 | "scripts": { 6 | "clean": "rimraf ./dist", 7 | "prebuild:html": "npm run clean", 8 | "build:html": "mkdirp dist && cpy ./src/index.html ./dist/", 9 | "build:webpack": "webpack -p --progress --colors", 10 | "build": "npm run build:html && npm run build:webpack", 11 | "predev": "npm run build:html", 12 | "dev": "webpack-dev-server --hot --content-base ./dist/ --port 4000 --history-api-fallback --inline" 13 | }, 14 | "dependencies": { 15 | "classnames": "^2.1.0", 16 | "react": "^0.14.2", 17 | "react-addons-shallow-compare": "^0.14.2", 18 | "react-dom": "^0.14.2", 19 | "react-redux": "^4.0.0", 20 | "redux": "^3.0.4", 21 | "redux-logger": "^2.0.4", 22 | "redux-thunk": "^1.0.0", 23 | "whatwg-fetch": "^0.9.0" 24 | }, 25 | "devDependencies": { 26 | "autoprefixer-loader": "^3.1.0", 27 | "babel-core": "^5.2.17", 28 | "babel-loader": "^5.0.0", 29 | "babel-runtime": "^5.2.17", 30 | "cpy": "^3.4.1", 31 | "css-loader": "^0.19.0", 32 | "exports-loader": "^0.6.2", 33 | "file-loader": "^0.8.1", 34 | "mkdirp": "^0.5.1", 35 | "react-hot-loader": "^1.2.7", 36 | "rimraf": "^2.3.3", 37 | "style-loader": "^0.12.2", 38 | "stylus": "^0.52.0", 39 | "stylus-loader": "^1.1.1", 40 | "url-loader": "^0.5.5", 41 | "webpack": "^1.9.4", 42 | "webpack-dev-server": "^1.8.2", 43 | "yargs": "^3.8.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var yargs = require("yargs"); 3 | var path = require("path"); 4 | 5 | var argv = yargs 6 | .boolean("p").alias("p", "optimize-minimize") 7 | .boolean("h").alias("h", "hot") 8 | .argv; 9 | 10 | module.exports = { 11 | 12 | entry: { 13 | main: path.join(__dirname, "src", "app", "entrypoints", "main.js") 14 | }, 15 | 16 | output: { 17 | path: path.join(__dirname, "dist", "app"), 18 | filename: "[name].js", 19 | publicPath: "/app/" 20 | }, 21 | 22 | module: { 23 | loaders: [ 24 | { 25 | test: /fetch.js$/, 26 | loaders: ["exports?fetch=window.fetch.bind(window),Headers=window.Headers,Request=window.Request,Response=window.Response"] 27 | }, 28 | 29 | { 30 | test: /\.js$/, 31 | exclude: [/node_modules/], 32 | loaders: (function() { 33 | var loaders = []; 34 | 35 | if (argv.h) { 36 | loaders.push("react-hot"); 37 | } 38 | 39 | loaders.push("babel"); 40 | 41 | return loaders; 42 | })() 43 | }, 44 | 45 | { 46 | test: /\.styl$/, 47 | loaders: [ 48 | "style", 49 | "css", 50 | "autoprefixer?browsers=last 2 version", 51 | "stylus" 52 | ] 53 | }, 54 | 55 | { 56 | test: /\.css$/, 57 | loaders: [ 58 | "style", 59 | "css", 60 | "autoprefixer?browsers=last 2 version" 61 | ] 62 | }, 63 | 64 | { 65 | test: /\.(png|jpg|gif)$/, 66 | loaders: (function() { 67 | var loaders = []; 68 | 69 | loaders.push("url?limit=50000"); 70 | 71 | return loaders; 72 | })() 73 | }, 74 | 75 | { 76 | test: /\.(ttf|eot|woff|svg)$/, 77 | loaders: [ 78 | "file" 79 | ] 80 | } 81 | ] 82 | }, 83 | 84 | resolve: { 85 | extensions: ["", ".js"], 86 | root: [ 87 | path.join(__dirname, "src", "app") 88 | ], 89 | modulesDirectories: ["web_modules", "node_modules", "src"] 90 | }, 91 | 92 | cache: !argv.p, 93 | debug: !argv.p, 94 | devtool: !argv.p ? "source-map" : false, 95 | 96 | stats: { 97 | colors: true, 98 | reasons: !argv.p 99 | }, 100 | 101 | plugins: (function() { 102 | var plugins = []; 103 | 104 | plugins.push( 105 | new webpack.DefinePlugin({ 106 | "process.env.NODE_ENV": JSON.stringify(argv.p ? "production" : "development"), 107 | "__DEV__": !argv.p 108 | }) 109 | ); 110 | 111 | if (argv.p) { 112 | plugins.push(new webpack.optimize.DedupePlugin()); 113 | plugins.push(new webpack.optimize.UglifyJsPlugin({ 114 | compress: { 115 | warnings: false 116 | } 117 | })); 118 | plugins.push(new webpack.optimize.OccurenceOrderPlugin()); 119 | plugins.push(new webpack.optimize.AggressiveMergingPlugin()); 120 | } 121 | 122 | if (argv.h) { 123 | plugins.push(new webpack.NoErrorsPlugin()); 124 | } 125 | 126 | return plugins; 127 | })() 128 | }; 129 | --------------------------------------------------------------------------------