├── .gitignore
├── .travis.yml
├── process
├── mockup.jpg
└── devlog.md
├── .babelrc
├── screenshots
├── avatars.png
├── first-ui.png
├── GitHunt-add.png
├── GitHunt-app.png
├── GitHunt-new.png
├── error-stack.png
├── github-api.png
├── default-mock.png
├── useful-error.png
├── GitHunt-GraphQL.png
├── first-mutation.png
├── github-oath-setup.png
└── github-oauth-keys.png
├── .eslintrc
├── ui
├── subscriptions.js
├── routes.js
├── style.css
├── Html.js
├── RepoInfo.js
├── client.js
├── NewEntry.js
├── server.js
├── Layout.js
├── Feed.js
└── CommentsPage.js
├── webpack.config.js
├── webpack.production.config.js
├── LICENSE
├── README.md
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dev.sqlite3
4 | .env
5 | .idea/
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
4 | - "5"
5 | - "4"
6 |
--------------------------------------------------------------------------------
/process/mockup.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/process/mockup.jpg
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "stage-2",
5 | "react"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/screenshots/avatars.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/avatars.png
--------------------------------------------------------------------------------
/screenshots/first-ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/first-ui.png
--------------------------------------------------------------------------------
/screenshots/GitHunt-add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/GitHunt-add.png
--------------------------------------------------------------------------------
/screenshots/GitHunt-app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/GitHunt-app.png
--------------------------------------------------------------------------------
/screenshots/GitHunt-new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/GitHunt-new.png
--------------------------------------------------------------------------------
/screenshots/error-stack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/error-stack.png
--------------------------------------------------------------------------------
/screenshots/github-api.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/github-api.png
--------------------------------------------------------------------------------
/screenshots/default-mock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/default-mock.png
--------------------------------------------------------------------------------
/screenshots/useful-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/useful-error.png
--------------------------------------------------------------------------------
/screenshots/GitHunt-GraphQL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/GitHunt-GraphQL.png
--------------------------------------------------------------------------------
/screenshots/first-mutation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/first-mutation.png
--------------------------------------------------------------------------------
/screenshots/github-oath-setup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/github-oath-setup.png
--------------------------------------------------------------------------------
/screenshots/github-oauth-keys.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/GitHunt-React/master/screenshots/github-oauth-keys.png
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "mocha": true,
5 | },
6 | "extends": "airbnb",
7 | "parser": "babel-eslint",
8 | "globals": {
9 | "ga": true
10 | },
11 | "rules": {
12 | "camelcase": 0
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ui/subscriptions.js:
--------------------------------------------------------------------------------
1 | import { print } from 'graphql-tag/printer';
2 |
3 | // quick way to add the subscribe and unsubscribe functions to the network interface
4 | export default function addGraphQLSubscriptions(networkInterface, wsClient) {
5 | return Object.assign(networkInterface, {
6 | subscribe(request, handler) {
7 | return wsClient.subscribe({
8 | query: print(request.query),
9 | variables: request.variables,
10 | }, handler);
11 | },
12 | unsubscribe(id: number) {
13 | wsClient.unsubscribe(id);
14 | },
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: './ui/client.js',
3 | output: {
4 | filename: "bundle.js",
5 | publicPath: '/'
6 | },
7 | module: {
8 | loaders: [{
9 | test: /\.css/,
10 | loader: 'style!css'
11 | }, {
12 | test: /\.js$/,
13 | loader: 'babel',
14 | // Exclude apollo client from the webpack config in case
15 | // we want to use npm link.
16 | exclude: /(node_modules)|(apollo-client)/
17 | }, {
18 | test: /\.json$/,
19 | loader: 'json'
20 | }]
21 | },
22 | devServer: {
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/ui/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import Feed from './Feed';
5 | import Layout from './Layout';
6 | import NewEntry from './NewEntry';
7 | import CommentsPage from './CommentsPage';
8 |
9 | export default (
10 |
14 |
17 |
21 |
25 |
29 |
30 | );
31 |
--------------------------------------------------------------------------------
/webpack.production.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin')
2 | const webpack = require('webpack');
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-source-map',
6 | entry: './ui/client.js',
7 | output: {
8 | path: 'api/dist/',
9 | publicPath: '/',
10 | filename: 'bundle.js'
11 | },
12 | module: {
13 | loaders: [{
14 | test: /\.css/,
15 | loader: 'style!css'
16 | }, {
17 | test: /\.js$/,
18 | loader: 'babel',
19 | exclude: /node_modules/
20 | }, {
21 | test: /\.json$/,
22 | loader: 'json'
23 | }]
24 | },
25 | plugins: [
26 | new webpack.DefinePlugin({
27 | 'process.env': {
28 | 'NODE_ENV': JSON.stringify('production')
29 | }
30 | }),
31 | new HtmlWebpackPlugin({
32 | template: 'ui/index.html'
33 | }),
34 | new webpack.optimize.CommonsChunkPlugin('common.js'),
35 | new webpack.optimize.DedupePlugin(),
36 | new webpack.optimize.UglifyJsPlugin(),
37 | new webpack.optimize.AggressiveMergingPlugin()
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Meteor Development Group
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 |
--------------------------------------------------------------------------------
/ui/style.css:
--------------------------------------------------------------------------------
1 | .media-vote {
2 | display: table-cell;
3 | vertical-align: top;
4 | padding-right: 10px;
5 | }
6 | .media-vote .btn-score {
7 | border: none;
8 | background-color: transparent;
9 | font-size: 1.3em;
10 | padding: 3px 12px;
11 | line-height: 0;
12 | color: rgba(91, 192, 222, 0.52);
13 | }
14 | .media-vote .btn-score:hover, .media-vote .btn-score.active {
15 | color: #5bc0de;
16 | box-shadow: none;
17 | }
18 | .media-vote .btn-score:active {
19 | color: #5BA2DE;
20 | box-shadow: none;
21 | }
22 | .media-vote .btn-score:focus {
23 | outline: none;
24 | }
25 | .media-vote .vote-score {
26 | text-align: center;
27 | font-size: 1.2em;
28 | }
29 |
30 | .invisible {
31 | visibility: hidden;
32 | }
33 |
34 | body {
35 | padding-bottom: 60px;
36 | }
37 |
38 | .comment-box {
39 | border-top:1px solid black;
40 | padding: 5px;
41 | }
42 |
43 | #footer {
44 | position: fixed;
45 | bottom: 0px;
46 | text-align: center;
47 | width: 100%;
48 | background-color: #f0f0f0;
49 | border-top: 1px solid #c4c4c4;
50 | padding-top: 10px;
51 | }
52 |
53 | #footer li {
54 | display: inline;
55 | margin: 40px 10px;
56 | color: #999;
57 | font-size: 13px;
58 | }
59 |
--------------------------------------------------------------------------------
/ui/Html.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | // XXX: production setup?
4 | const basePort = process.env.PORT || 3000;
5 | const scriptUrl = `http://localhost:${basePort + 20}/bundle.js`;
6 |
7 | function Html({ content, state }) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | GitHunt
15 |
16 |
17 |
18 |
24 |
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | Html.propTypes = {
35 | content: PropTypes.string.isRequired,
36 | state: PropTypes.object.isRequired,
37 | };
38 |
39 |
40 | export default Html;
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GitHunt React
2 |
3 | An example of a client-side app built with React and Apollo Client.
4 |
5 | [](http://www.apollostack.com/#slack)
6 | [](https://travis-ci.org/apollostack/GitHunt-React)
7 |
8 | Please submit a pull request if you see anything that can be improved!
9 |
10 | ## Running the app
11 |
12 | ### 0. This repository is only the React frontend. Run the [GitHunt API](https://github.com/apollostack/GitHunt-API) first. (This is temporary, until we have a permanently hosted demo server.)
13 |
14 | ### 1. Install Node/npm
15 |
16 | Make sure you have Node.js installed (the app has been tested with Node `4.4.5` and `5.3.0`)
17 |
18 | ### 2. Clone and install dependencies
19 |
20 | ```
21 | git clone https://github.com/apollostack/GitHunt-React.git
22 | cd GitHunt
23 | npm install
24 | ```
25 |
26 | ### 3. Run the app
27 |
28 | ```
29 | npm start
30 | ```
31 |
32 | - Open the client at http://localhost:3000
33 | - Click "Log in with GitHub" in the upper right corner
34 | - You'll be presented with the seed items in the app
35 |
36 | 
37 |
38 | #### Submit a Repo
39 | Click the green Submit button and add repo with the username/repo-name pattern.
40 |
41 | 
42 |
43 | #### New Item
44 | Review the new item, up vote it and visit the repo via the link.
45 | 
46 |
--------------------------------------------------------------------------------
/ui/RepoInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TimeAgo from 'react-timeago';
3 | import { emojify } from 'node-emoji';
4 |
5 | function RepoInfo({
6 | description,
7 | stargazers_count,
8 | open_issues_count,
9 | created_at,
10 | user_url,
11 | username,
12 | children,
13 | }) {
14 | return (
15 |
16 |
17 | {description && emojify(description)}
18 |
19 |
20 |
24 |
25 |
29 |
30 | {children}
31 |
32 | Submitted
33 |
36 | by
37 | {username}
38 |
39 |
40 | );
41 | }
42 |
43 | RepoInfo.propTypes = {
44 | description: React.PropTypes.string.isRequired,
45 | stargazers_count: React.PropTypes.number.isRequired,
46 | open_issues_count: React.PropTypes.number.isRequired,
47 | created_at: React.PropTypes.number.isRequired,
48 | user_url: React.PropTypes.string.isRequired,
49 | username: React.PropTypes.string.isRequired,
50 | children: React.PropTypes.node,
51 | };
52 |
53 | function InfoLabel({ label, value }) {
54 | return (
55 | {label}: {value}
56 | );
57 | }
58 |
59 | InfoLabel.propTypes = {
60 | label: React.PropTypes.string,
61 | value: React.PropTypes.number,
62 | };
63 |
64 | export default RepoInfo;
65 |
--------------------------------------------------------------------------------
/ui/client.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Router, browserHistory } from 'react-router';
4 | import ApolloClient, { createNetworkInterface, addTypename } from 'apollo-client';
5 | import { ApolloProvider } from 'react-apollo';
6 | const ReactGA = require('react-ga');
7 | // Polyfill fetch
8 | import 'isomorphic-fetch';
9 |
10 | import routes from './routes.js';
11 |
12 | import './style.css';
13 |
14 | import { Client } from 'subscriptions-transport-ws';
15 | import addGraphQLSubscriptions from './subscriptions';
16 |
17 | const wsClient = new Client('ws://localhost:8080');
18 |
19 | const networkInterface = createNetworkInterface({
20 | uri: '/graphql',
21 | opts: {
22 | credentials: 'same-origin',
23 | },
24 | transportBatching: true,
25 | });
26 |
27 | const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
28 | networkInterface,
29 | wsClient,
30 | );
31 |
32 | // Initialize Analytics
33 | ReactGA.initialize('UA-74643563-4');
34 |
35 | function logPageView() {
36 | ReactGA.set({ page: window.location.pathname });
37 | ReactGA.pageview(window.location.pathname);
38 | }
39 |
40 | const client = new ApolloClient({
41 | networkInterface: networkInterfaceWithSubscriptions,
42 | queryTransformer: addTypename,
43 | dataIdFromObject: (result) => {
44 | if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
45 | return result.__typename + result.id; // eslint-disable-line no-underscore-dangle
46 | }
47 | return null;
48 | },
49 | shouldBatch: true,
50 | initialState: window.__APOLLO_STATE__, // eslint-disable-line no-underscore-dangle
51 | ssrForceFetchDelay: 100,
52 | });
53 |
54 | render((
55 |
56 |
57 | {routes}
58 |
59 |
60 | ), document.getElementById('content'));
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "githunt",
3 | "version": "1.0.0",
4 | "description": "Example app for Apollo",
5 | "main": "index.js",
6 | "scripts": {
7 | "start-app-client": "webpack-dev-server -d --hot --inline --no-info --port 3020",
8 | "start-app-server": "nodemon ui/server.js --watch ui --exec babel-node",
9 | "start": "concurrently \"npm run start-app-client\" \"npm run start-app-server\"",
10 | "lint": "eslint ui",
11 | "build": "webpack -p --progress --config ./webpack.production.config.js"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/apollostack/GitHunt.git"
16 | },
17 | "author": "",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/apollostack/GitHunt/issues"
21 | },
22 | "homepage": "https://github.com/apollostack/GitHunt#readme",
23 | "devDependencies": {
24 | "babel-cli": "^6.8.0",
25 | "babel-core": "^6.8.0",
26 | "babel-eslint": "^6.1.0",
27 | "babel-loader": "^6.2.4",
28 | "babel-preset-es2015": "^6.6.0",
29 | "babel-preset-react": "^6.5.0",
30 | "babel-preset-stage-2": "^6.5.0",
31 | "babel-register": "^6.9.0",
32 | "concurrently": "^2.1.0",
33 | "css-loader": "^0.25.0",
34 | "eslint": "^3.4.0",
35 | "eslint-config-airbnb": "^11.0.0",
36 | "eslint-plugin-babel": "^3.3.0",
37 | "eslint-plugin-import": "^1.8.1",
38 | "eslint-plugin-jsx-a11y": "^2.2.1",
39 | "eslint-plugin-react": "^6.2.0",
40 | "html-webpack-plugin": "^2.22.0",
41 | "nodemon": "^1.9.2",
42 | "style-loader": "^0.13.1",
43 | "webpack": "^1.13.0",
44 | "webpack-dev-server": "^1.14.1"
45 | },
46 | "dependencies": {
47 | "apollo-client": "^0.4.14",
48 | "classnames": "^2.2.5",
49 | "express": "^4.14.0",
50 | "graphql-tag": "^0.1.7",
51 | "http-proxy-middleware": "^0.17.1",
52 | "isomorphic-fetch": "^2.2.1",
53 | "json-loader": "^0.5.4",
54 | "node-emoji": "^1.3.0",
55 | "react": "^15.1.0",
56 | "react-addons-update": "^15.2.1",
57 | "react-apollo": "^0.5.2",
58 | "react-dom": "^15.1.0",
59 | "react-ga": "^2.1.0",
60 | "react-router": "^2.4.1",
61 | "react-timeago": "^3.0.0",
62 | "redux": "^3.5.2",
63 | "subscriptions-transport-ws": "^0.1.2"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/ui/NewEntry.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { graphql } from 'react-apollo';
3 | import { browserHistory } from 'react-router';
4 | import gql from 'graphql-tag';
5 |
6 | class NewEntry extends React.Component {
7 | constructor() {
8 | super();
9 | this.state = {};
10 |
11 | this.submitForm = this.submitForm.bind(this);
12 | }
13 |
14 | submitForm(event) {
15 | event.preventDefault();
16 |
17 | const { submit } = this.props;
18 |
19 | const repoFullName = event.target.repoFullName.value;
20 |
21 | return submit(repoFullName).then((res) => {
22 | if (!res.errors) {
23 | browserHistory.push('/feed/new');
24 | } else {
25 | this.setState({ errors: res.errors });
26 | }
27 | });
28 | }
29 |
30 | render() {
31 | const { errors } = this.state;
32 | return (
33 |
34 |
Submit a repository
35 |
36 |
62 |
63 | );
64 | }
65 | }
66 |
67 | NewEntry.propTypes = {
68 | submit: React.PropTypes.func.isRequired,
69 | };
70 |
71 | const SUBMIT_RESPOSITORY_MUTATION = gql`
72 | mutation submitRepository($repoFullName: String!) {
73 | submitRepository(repoFullName: $repoFullName) {
74 | createdAt
75 | }
76 | }
77 | `;
78 |
79 | const NewEntryWithData = graphql(SUBMIT_RESPOSITORY_MUTATION, {
80 | props({ mutate }) {
81 | return {
82 | submit(repoFullName) {
83 | return mutate({ variables: { repoFullName } });
84 | },
85 | };
86 | },
87 | })(NewEntry);
88 |
89 | export default NewEntryWithData;
90 |
--------------------------------------------------------------------------------
/ui/server.js:
--------------------------------------------------------------------------------
1 | import Express from 'express';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom/server';
4 | import ApolloClient, { createNetworkInterface } from 'apollo-client';
5 | import { ApolloProvider } from 'react-apollo';
6 | import { getDataFromTree } from 'react-apollo/server';
7 | import { match, RouterContext } from 'react-router';
8 | import path from 'path';
9 | import 'isomorphic-fetch';
10 | import proxy from 'http-proxy-middleware';
11 |
12 | import routes from './routes';
13 | import Html from './Html';
14 |
15 | const basePort = process.env.PORT || 3000;
16 | const apiHost = `http://localhost:${basePort + 10}`;
17 | const apiUrl = `${apiHost}/graphql`;
18 |
19 | const app = new Express();
20 | app.use(Express.static(path.join(process.cwd(), 'static')));
21 |
22 | const apiProxy = proxy({ target: apiHost });
23 | app.use('/graphql', apiProxy);
24 | app.use('/graphiql', apiProxy);
25 | app.use('/login', apiProxy);
26 | app.use('/logout', apiProxy);
27 |
28 | app.use((req, res) => {
29 | match({ routes, location: req.originalUrl }, (error, redirectLocation, renderProps) => {
30 | if (redirectLocation) {
31 | res.redirect(redirectLocation.pathname + redirectLocation.search);
32 | } else if (error) {
33 | console.error('ROUTER ERROR:', error); // eslint-disable-line no-console
34 | res.status(500);
35 | } else if (renderProps) {
36 | const client = new ApolloClient({
37 | ssrMode: true,
38 | networkInterface: createNetworkInterface(apiUrl, {
39 | credentials: 'same-origin',
40 | // transfer request headers to networkInterface so that they're accessible to proxy server
41 | // Addresses this issue: https://github.com/matthew-andrews/isomorphic-fetch/issues/83
42 | headers: req.headers,
43 | }),
44 | });
45 |
46 | const component = (
47 |
48 |
49 |
50 | );
51 |
52 | getDataFromTree(component).then(context => {
53 | const content = ReactDOM.renderToString(component);
54 | res.status(200);
55 |
56 | const html =