├── .babelrc ├── .gitignore ├── LICENSE ├── PATENTS ├── README.md ├── config.js ├── js ├── app.js ├── auth │ └── Auth.js ├── components │ ├── App │ │ ├── App.js │ │ ├── Description.js │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── Hero.js │ │ ├── Login.js │ │ └── Register.js │ ├── GraphiQL │ │ └── GraphiQL.js │ └── HackerNewsClone │ │ ├── HackerNewsItem.js │ │ ├── HackerNewsItems.js │ │ ├── Header.js │ │ ├── Home.js │ │ └── Logout.js ├── mutations │ ├── LoginMutation.js │ └── RegisterMutation.js └── routes │ └── HomeRoute.js ├── package.json ├── plugins └── babelRelayPlugin.js ├── public └── index.html ├── schema.json ├── scripts └── updateSchema.js └── server.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "passPerPreset": true, 3 | "presets": [ 4 | "react", 5 | "es2015", 6 | "stage-0" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | data/schema.graphql 5 | .idea/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For Relay Starter Kit software 4 | 5 | Copyright (c) 2013-2015, Facebook, Inc. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without modification, 9 | are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name Facebook nor the names of its contributors may be used to 19 | endorse or promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 26 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights Version 2 2 | 3 | "Software" means the Relay Starter Kit software distributed by Facebook, Inc. 4 | 5 | Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software 6 | ("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable 7 | (subject to the termination provision below) license under any Necessary 8 | Claims, to make, have made, use, sell, offer to sell, import, and otherwise 9 | transfer the Software. For avoidance of doubt, no license is granted under 10 | Facebook's rights in any patent claims that are infringed by (i) modifications 11 | to the Software made by you or any third party or (ii) the Software in 12 | combination with any software or other technology. 13 | 14 | The license granted hereunder will terminate, automatically and without notice, 15 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate 16 | directly or indirectly, or take a direct financial interest in, any Patent 17 | Assertion: (i) against Facebook or any of its subsidiaries or corporate 18 | affiliates, (ii) against any party if such Patent Assertion arises in whole or 19 | in part from any software, technology, product or service of Facebook or any of 20 | its subsidiaries or corporate affiliates, or (iii) against any party relating 21 | to the Software. Notwithstanding the foregoing, if Facebook or any of its 22 | subsidiaries or corporate affiliates files a lawsuit alleging patent 23 | infringement against you in the first instance, and you respond by filing a 24 | patent infringement counterclaim in that lawsuit against that party that is 25 | unrelated to the Software, the license granted hereunder will not terminate 26 | under section (i) of this paragraph due to such counterclaim. 27 | 28 | A "Necessary Claim" is a claim of a patent owned by Facebook that is 29 | necessarily infringed by the Software standing alone. 30 | 31 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, 32 | or contributory infringement or inducement to infringe any patent, including a 33 | cross-claim or counterclaim. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scaphold.io's HackerNews Tutorial 2 | 3 | Fork this boilerplate code to get started with a HackerNews clone built with React-Relay. 4 | 5 | Quickstart: 6 | 7 | 1) Go to Scaphold.io (https://scaphold.io). 8 | 9 | 2) Create an account and dataset. 10 | 11 | 3) Change the URL in the API manager (config.js) in the boilerplate to point to your unique Scaphold.io API URL. 12 | 13 | 4) Run with: npm start 14 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Modify the config Scaphold URL to point to your specific app. 3 | * Find the URL at the top of the page on Scaphold.io once you've created an app. 4 | * Yup. It's that easy. 5 | */ 6 | 7 | var config = { 8 | scapholdUrl: "https://us-west-2.api.scaphold.io/graphql/hacker-news-tutorial" 9 | } 10 | 11 | module.exports = config; -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import Relay from 'react-relay'; 6 | import { Router, Route, IndexRoute, applyRouterMiddleware, browserHistory, routes, hashHistory } from 'react-router'; 7 | import useRelay from 'react-router-relay'; 8 | import config from './../config'; 9 | 10 | import App from './components/App/App'; 11 | import Home from './components/HackerNewsClone/Home'; 12 | import GraphiQLModule from './components/GraphiQL/GraphiQL'; 13 | import { HomeQueries, prepareHomeParams } from './routes/HomeRoute'; 14 | 15 | var options = {}; 16 | if (localStorage.scapholdAuthToken) { 17 | options.headers = { 18 | Authorization: 'Bearer ' + localStorage.scapholdAuthToken 19 | } 20 | } 21 | 22 | Relay.injectNetworkLayer( 23 | new Relay.DefaultNetworkLayer(config.scapholdUrl, options) 24 | ); 25 | 26 | ReactDOM.render( 27 | 33 | 34 | 36 | 37 | , 38 | document.getElementById('root') 39 | ); 40 | -------------------------------------------------------------------------------- /js/auth/Auth.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | import RegisterMutation from './../mutations/RegisterMutation'; 3 | import LoginMutation from './../mutations/LoginMutation'; 4 | 5 | export function register(username, password) { 6 | return new Promise((resolve, reject) => { 7 | Relay.Store.commitUpdate(new RegisterMutation({ 8 | input: { 9 | username: username, 10 | password: password 11 | }, 12 | user: null 13 | }), { 14 | onSuccess: (data) => { 15 | resolve(login(username, password)); 16 | }, 17 | onFailure: (transaction) => { 18 | reject(transaction.getError().message); 19 | } 20 | }); 21 | }) 22 | } 23 | 24 | export function login(username, password) { 25 | return new Promise((resolve, reject) => { 26 | Relay.Store.commitUpdate(new LoginMutation({ 27 | input: { 28 | username: username, 29 | password: password 30 | }, 31 | user: null 32 | }), { 33 | onSuccess: (data) => { 34 | resolve(data); 35 | }, 36 | onFailure: (transaction) => { 37 | reject(transaction.getError().message); 38 | } 39 | }); 40 | }) 41 | } -------------------------------------------------------------------------------- /js/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import {Row, Col, Button, Jumbotron} from 'react-bootstrap'; 4 | import {hashHistory} from 'react-router'; 5 | import Header from './Header'; 6 | import Hero from './Hero'; 7 | import Description from './Description'; 8 | import Footer from './Footer'; 9 | 10 | class App extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | render() { 16 | if (localStorage.scapholdAuthToken) { 17 | hashHistory.push('/home'); 18 | } 19 | 20 | return ( 21 |
22 |
23 | 24 | 25 |
27 | ); 28 | } 29 | } 30 | 31 | export default Relay.createContainer(App, { 32 | fragments: { 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /js/components/App/Description.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import {Row, Col} from 'react-bootstrap'; 4 | 5 | class Description extends React.Component { 6 | render() { 7 | return ( 8 | 9 | 10 |

React.js Boilerplate

11 |

This React.js boilerplate helps developers create modern, performant, and clean web apps with the help of Scaphold.io.

12 | 13 |

React-Relay

14 |

Leverage the simplicity and power of Relay and GraphQL to manage your application's data store.

15 | 16 | 17 | 18 |

React-Bootstrap

19 |

Smoothe and creative components to fit the way you want your apps to be experienced.

20 | 21 |

Webpack

22 |

Webpack is a module bundler that helps you serve your application in any environment with hot reloading.

23 | 24 |
25 | ); 26 | } 27 | } 28 | 29 | export default Relay.createContainer(Description, { 30 | fragments: { 31 | } 32 | }); 33 | 34 | const styles = { 35 | marketing: { 36 | margin: '40px 0', 37 | p: { 38 | marginTop: 28 39 | }, 40 | h4: { 41 | marginTop: 28 42 | } 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /js/components/App/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import {Row, Col} from 'react-bootstrap'; 4 | import FontAwesome from 'react-fontawesome'; 5 | 6 | class Footer extends React.Component { 7 | render() { 8 | return ( 9 | 10 | 11 |

Made with from the Scaphold team

12 | 13 |
14 | ); 15 | } 16 | } 17 | 18 | export default Relay.createContainer(Footer, { 19 | fragments: { 20 | } 21 | }); 22 | 23 | const styles = { 24 | footer: { 25 | textAlign: 'center', 26 | paddingTop: 19, 27 | color: '#777', 28 | borderTop: '1px, solid, #e5e5e5' 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /js/components/App/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import {hashHistory} from 'react-router'; 4 | import {Navbar, Nav, NavItem, NavDropdown, MenuItem} from 'react-bootstrap'; 5 | import Login from './Login'; 6 | import Register from './Register'; 7 | 8 | class Header extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.goToGraphiQL = this.goToGraphiQL.bind(this); 12 | this.goHome = this.goHome.bind(this); 13 | } 14 | 15 | goToGraphiQL() { 16 | hashHistory.push('/graphiql'); 17 | } 18 | 19 | goHome() { 20 | hashHistory.push('/'); 21 | } 22 | 23 | render() { 24 | return ( 25 | 26 | 27 | 28 | Scaphold 29 | 30 | 31 | 37 | 38 | ); 39 | } 40 | } 41 | 42 | export default Relay.createContainer(Header, { 43 | fragments: { 44 | } 45 | }); 46 | 47 | const styles = { 48 | navbar: { 49 | marginBottom: 0 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /js/components/App/Hero.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import {Row, Col, Button, Jumbotron} from 'react-bootstrap'; 4 | import FontAwesome from 'react-fontawesome'; 5 | 6 | class Hero extends React.Component { 7 | render() { 8 | return ( 9 | 10 | 11 | 12 |

Welcome!

13 |

Here you'll find Scaphold.io's HackerNews Clone Tutorial :)

14 |

15 |

Join our Slack community!

16 |
17 | 18 |
19 | ); 20 | } 21 | } 22 | 23 | export default Relay.createContainer(Hero, { 24 | fragments: { 25 | } 26 | }); 27 | 28 | const styles = { 29 | jumbotron: { 30 | marginTop: 20, 31 | borderRadius: 10, 32 | textAlign: 'center' 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /js/components/App/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import { hashHistory } from 'react-router'; 4 | import { Button, Modal, OverlayTrigger, NavItem, Form, FormControl, FormGroup, Row, Col, ControlLabel} from 'react-bootstrap'; 5 | import * as Auth from './../../auth/Auth'; 6 | import config from './../../../config'; 7 | 8 | class Login extends React.Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | showModal: false, 14 | loginEmail: undefined, 15 | loginPassword: undefined, 16 | errors: undefined 17 | }; 18 | this.close = this.close.bind(this); 19 | this.open = this.open.bind(this); 20 | this._handleLoginEmailChange = this._handleLoginEmailChange.bind(this); 21 | this._handleLoginPasswordChange = this._handleLoginPasswordChange.bind(this); 22 | this.loginUser = this.loginUser.bind(this); 23 | } 24 | 25 | close() { 26 | this.setState({ showModal: false }); 27 | } 28 | 29 | open() { 30 | this.setState({ showModal: true }); 31 | } 32 | 33 | _handleLoginEmailChange(e) { 34 | this.state.loginEmail = e.target.value; 35 | } 36 | _handleLoginPasswordChange(e) { 37 | this.state.loginPassword = e.target.value; 38 | } 39 | 40 | loginUser() { 41 | Auth.login(this.state.loginEmail, this.state.loginPassword) 42 | .then((result) => { 43 | localStorage.scapholdAuthToken = result.loginUser.token; 44 | localStorage.userId = result.loginUser.id; 45 | localStorage.email = this.state.loginEmail; 46 | hashHistory.push(`/home`); 47 | }).catch((error) => { 48 | var e; 49 | if (error) { 50 | e = error.split('[ERROR]')[1]; 51 | } 52 | this.setState({errors: "Error: " + e}); 53 | }); 54 | } 55 | 56 | render() { 57 | return ( 58 | 59 | Login 60 | 61 | 62 | 63 | Login Here! 64 | 65 | 66 |
67 | 68 | 69 | Email 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Password 79 | 80 | 81 | 82 | 83 | 84 |
85 |
{this.state.errors}
86 |
87 | 88 | 89 | 90 | 91 |
92 |
93 | ); 94 | } 95 | } 96 | 97 | export default Relay.createContainer(Login, { 98 | fragments: { 99 | } 100 | }); 101 | 102 | const styles = { 103 | errors: { 104 | textAlign: 'center', 105 | color: 'red' 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /js/components/App/Register.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import { hashHistory } from 'react-router'; 4 | import { Button, Modal, OverlayTrigger, NavItem, Form, FormControl, FormGroup, Row, Col, ControlLabel} from 'react-bootstrap'; 5 | import * as Auth from './../../auth/Auth'; 6 | import config from './../../../config'; 7 | 8 | class Register extends React.Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | showModal: false, 14 | registerEmail: undefined, 15 | registerPassword: undefined, 16 | errors: undefined 17 | }; 18 | this.close = this.close.bind(this); 19 | this.open = this.open.bind(this); 20 | this._handleRegisterEmailChange = this._handleRegisterEmailChange.bind(this); 21 | this._handleRegisterPasswordChange = this._handleRegisterPasswordChange.bind(this); 22 | this.registerUser = this.registerUser.bind(this); 23 | } 24 | 25 | close() { 26 | this.setState({ showModal: false }); 27 | } 28 | 29 | open() { 30 | this.setState({ showModal: true }); 31 | } 32 | 33 | registerUser() { 34 | Auth.register(this.state.registerEmail, this.state.registerPassword) 35 | .then((result) => { 36 | close(); 37 | localStorage.scapholdAuthToken = result.loginUser.token; 38 | localStorage.userId = result.loginUser.id; 39 | localStorage.email = this.state.registerEmail; 40 | hashHistory.push(`/home`); 41 | }).catch((error) => { 42 | this.setState({errors: "Error: " + error}); 43 | }); 44 | } 45 | 46 | _handleRegisterEmailChange(e) { 47 | this.state.registerEmail = e.target.value; 48 | } 49 | 50 | _handleRegisterPasswordChange(e) { 51 | this.state.registerPassword = e.target.value; 52 | } 53 | 54 | render() { 55 | return ( 56 | 57 | Register 58 | 59 | 60 | 61 | Register Here! 62 | 63 | 64 |
65 | 66 | 67 | 68 | Email 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Password 78 | 79 | 80 | 81 | 82 | 83 | 84 |
85 |
{this.state.errors}
86 |
87 | 88 | 89 | 90 | 91 |
92 |
93 | ); 94 | } 95 | } 96 | 97 | export default Relay.createContainer(Register, { 98 | fragments: { 99 | } 100 | }); 101 | 102 | const styles = { 103 | errors: { 104 | textAlign: 'center', 105 | color: 'red' 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /js/components/GraphiQL/GraphiQL.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import GraphiQL from 'graphiql'; 4 | import fetch from 'isomorphic-fetch'; 5 | import config from './../../../config'; 6 | import Header from './../App/Header'; 7 | import LoggedInHeader from './../HackerNewsClone/Header'; 8 | 9 | function graphQLFetcher(graphQLParams) { 10 | return fetch(config.scapholdUrl, { 11 | method: 'post', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | 'Authorization': 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3NjYXBob2xkLmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw1NzI4ZGE0ZmZkMzU5ZmZiNzMxYzYwMjUiLCJhdWQiOiJKdGdmeVpJUTJwSmo5ckk4RTllNjE3aFFjazBSbnhBbiIsImV4cCI6MTQ2MjYyMTYwNCwiaWF0IjoxNDYyNTM1MjA0fQ.v85wIK3KmFqFu6DVlvnTZ4PNaTIGuJ3OYXG2ZNJiC4s' 15 | }, 16 | body: JSON.stringify(graphQLParams), 17 | }).then(response => response.json()); 18 | } 19 | 20 | class GraphiQLModule extends React.Component { 21 | render() { 22 | var header; 23 | if (!localStorage.scapholdAuthToken) { 24 | header =
; 25 | } 26 | else { 27 | header = ; 28 | } 29 | 30 | return ( 31 | 32 | {header} 33 | 34 | 35 | ) 36 | } 37 | } 38 | 39 | export default Relay.createContainer(GraphiQLModule, { 40 | // initialVariables: { 41 | // input: null 42 | // }, 43 | fragments: { 44 | // user: (variables) => { 45 | // return Relay.QL` 46 | // fragment on UserQuerySet { 47 | // get (id: $input) { 48 | // id, 49 | // credentials { 50 | // basic { 51 | // email 52 | // } 53 | // }, 54 | // createdAt 55 | // } 56 | // } 57 | // ` 58 | // } 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /js/components/HackerNewsClone/HackerNewsItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | 4 | class HackerNewsItem extends React.Component { 5 | render() { 6 | let item = this.props.hnItem.node; 7 | let time = new Date(item.createdAt); 8 | time = time.toString(); 9 | let mailto; 10 | let email; 11 | if (item.author && item.author.username) { 12 | mailto = "mailto:" + item.author.username; 13 | email = item.author.username; 14 | } 15 | 16 | return ( 17 |
18 |

{this.props.num + 1}. {item.title}

19 |

{item.score} points by {email}

20 |
at {time}
21 |
22 |
23 | ); 24 | } 25 | } 26 | 27 | export default Relay.createContainer(HackerNewsItem, { 28 | fragments: { 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /js/components/HackerNewsClone/HackerNewsItems.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import HackerNewsItem from './HackerNewsItem'; 4 | import {Row, Col} from 'react-bootstrap'; 5 | 6 | class HackerNewsItems extends React.Component { 7 | render() { 8 | let items = this.props.allHackerNewsItems.edges.map( 9 | (hnItem, idx) => 10 |
11 | 12 |
13 | ); 14 | 15 | return ( 16 | 17 | 18 | {items} 19 | 20 | 21 | ); 22 | } 23 | } 24 | 25 | export default Relay.createContainer(HackerNewsItems, { 26 | fragments: { 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /js/components/HackerNewsClone/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import {hashHistory} from 'react-router'; 4 | import {Navbar, Nav, NavItem, NavDropdown, MenuItem} from 'react-bootstrap'; 5 | import Logout from './Logout'; 6 | 7 | class Header extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.goToGraphiQL = this.goToGraphiQL.bind(this); 11 | this.goHome = this.goHome.bind(this); 12 | } 13 | 14 | goToGraphiQL() { 15 | hashHistory.push('/graphiql'); 16 | } 17 | 18 | goHome() { 19 | hashHistory.push('/'); 20 | } 21 | 22 | render() { 23 | var loggedInUser = localStorage.email; 24 | 25 | return ( 26 | 27 | 28 | 29 | Hacker News Clone 30 | 31 | 32 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default Relay.createContainer(Header, { 44 | fragments: { 45 | } 46 | }); 47 | 48 | const styles = { 49 | navbar: { 50 | marginBottom: 0 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /js/components/HackerNewsClone/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import {Button} from 'react-bootstrap'; 4 | import {hashHistory} from 'react-router'; 5 | import Header from './Header'; 6 | import HackerNewsItems from './HackerNewsItems'; 7 | 8 | class Home extends React.Component { 9 | render() { 10 | if (!localStorage.scapholdAuthToken) { 11 | hashHistory.push('/'); 12 | } 13 | 14 | return ( 15 |
16 |
17 |
    18 | 19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | export default Relay.createContainer(Home, { 26 | initialVariables: { 27 | input: null, 28 | orderBy: null 29 | }, 30 | fragments: { 31 | allHackerNewsItems: (variables) => { 32 | return Relay.QL ` 33 | fragment on Viewer { 34 | allHackerNewsItems (first: 10, orderBy: $orderBy) { 35 | edges { 36 | node { 37 | id, 38 | createdAt, 39 | modifiedAt, 40 | title, 41 | score, 42 | url, 43 | author { 44 | id, 45 | username 46 | } 47 | } 48 | } 49 | } 50 | } 51 | ` 52 | } 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /js/components/HackerNewsClone/Logout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import { hashHistory } from 'react-router'; 4 | import { NavItem } from 'react-bootstrap'; 5 | 6 | class Logout extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.logoutUser = this.logoutUser.bind(this); 11 | } 12 | 13 | logoutUser() { 14 | localStorage.clear(); 15 | hashHistory.push('/'); 16 | } 17 | 18 | render() { 19 | return ( 20 | Logout 21 | ); 22 | } 23 | } 24 | 25 | export default Relay.createContainer(Logout, { 26 | fragments: { 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /js/mutations/LoginMutation.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | export default class LoginMutation extends Relay.Mutation { 4 | static initialVariables = { 5 | input: null 6 | }; 7 | 8 | getMutation() { 9 | return Relay.QL` 10 | mutation { 11 | loginUser 12 | } 13 | `; 14 | } 15 | 16 | getVariables() { 17 | return { 18 | username: this.props.input.username, 19 | password: this.props.input.password 20 | }; 21 | } 22 | 23 | getFatQuery() { 24 | return Relay.QL` 25 | fragment on LoginUserPayload { 26 | token 27 | user { 28 | id 29 | } 30 | } 31 | ` 32 | } 33 | 34 | getConfigs() { 35 | return [{ 36 | type: 'REQUIRED_CHILDREN', 37 | children: [Relay.QL ` 38 | fragment on LoginUserPayload { 39 | token 40 | user { 41 | id 42 | } 43 | } 44 | `] 45 | }] 46 | } 47 | 48 | getOptimisticResponse() { 49 | return { 50 | loginUser: this.props.loginUser 51 | } 52 | } 53 | 54 | static fragments = { 55 | user: () => Relay.QL` 56 | fragment on LoginUserPayload { 57 | token 58 | user { 59 | id 60 | } 61 | } 62 | `, 63 | }; 64 | } -------------------------------------------------------------------------------- /js/mutations/RegisterMutation.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | export default class RegisterMutation extends Relay.Mutation { 4 | static initialVariables = { 5 | input: null 6 | }; 7 | 8 | getMutation() { 9 | return Relay.QL` 10 | mutation { 11 | createUser 12 | } 13 | `; 14 | } 15 | 16 | getVariables() { 17 | return { 18 | username: this.props.input.username, 19 | password: this.props.input.password 20 | }; 21 | } 22 | 23 | getFatQuery() { 24 | return Relay.QL` 25 | fragment on CreateUserPayload { 26 | changedUser { 27 | username, 28 | createdAt, 29 | modifiedAt 30 | } 31 | } 32 | ` 33 | } 34 | 35 | getConfigs() { 36 | return [{ 37 | type: 'REQUIRED_CHILDREN', 38 | children: [Relay.QL ` 39 | fragment on CreateUserPayload { 40 | changedUser { 41 | username, 42 | createdAt, 43 | modifiedAt 44 | } 45 | } 46 | `] 47 | }] 48 | } 49 | 50 | // getOptimisticResponse() { 51 | // return { 52 | // changedUser: { 53 | // username: this.props.username 54 | // } 55 | // } 56 | // } 57 | 58 | static fragments = { 59 | user: () => Relay.QL` 60 | fragment on CreateUserPayload { 61 | changedUser { 62 | username, 63 | createdAt, 64 | modifiedAt 65 | } 66 | } 67 | `, 68 | }; 69 | } -------------------------------------------------------------------------------- /js/routes/HomeRoute.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | import config from './../../config'; 3 | 4 | export const HomeQueries = { 5 | allHackerNewsItems: (Component, variables) => { 6 | return Relay.QL ` 7 | query { 8 | viewer { 9 | ${Component.getFragment('allHackerNewsItems', {orderBy: variables.orderBy})} 10 | } 11 | } 12 | ` 13 | } 14 | } 15 | 16 | export function prepareHomeParams(params, {}) { 17 | return { 18 | ...params, 19 | orderBy: {field: "createdAt", direction: "ASC" } 20 | }; 21 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hacker-news-tutorial", 3 | "private": true, 4 | "description": "", 5 | "repository": "facebook/relay-starter-kit", 6 | "version": "0.1.0", 7 | "scripts": { 8 | "start": "babel-node ./server.js", 9 | "update-schema": "babel-node ./scripts/updateSchema.js" 10 | }, 11 | "dependencies": { 12 | "babel-core": "^6.7.7", 13 | "babel-loader": "6.2.4", 14 | "babel-polyfill": "6.7.4", 15 | "babel-preset-es2015": "6.6.0", 16 | "babel-preset-react": "6.5.0", 17 | "babel-preset-stage-0": "6.5.0", 18 | "babel-relay-plugin": "^0.10.0", 19 | "classnames": "2.2.4", 20 | "express": "^4.13.4", 21 | "graphiql": "^0.10.0", 22 | "graphql": "^0.8.2", 23 | "graphql-relay": "^0.4.2", 24 | "isomorphic-fetch": "^2.2.1", 25 | "react": "^15.1.0", 26 | "react-bootstrap": "^0.29.3", 27 | "react-dom": "^15.1.0", 28 | "react-fontawesome": "^1.1.0", 29 | "react-relay": "^0.10.0", 30 | "react-router": "^2.4.0", 31 | "react-router-relay": "^0.13.2", 32 | "sync-request": "^2.0.1", 33 | "webpack": "1.13.0", 34 | "webpack-dev-server": "1.14.1" 35 | }, 36 | "devDependencies": { 37 | "babel-cli": "6.7.7" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /plugins/babelRelayPlugin.js: -------------------------------------------------------------------------------- 1 | import getBabelRelayPlugin from 'babel-relay-plugin'; 2 | 3 | const schema = require('../schema.json'); 4 | 5 | export default getBabelRelayPlugin(schema.data); 6 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React-Relay Starter Kit for Scaphold.io 10 | 11 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /scripts/updateSchema.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env babel-node --optional es7.asyncFunctions 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import { introspectionQuery } from 'graphql/utilities' 6 | import request from 'sync-request' 7 | import config from '../config' 8 | 9 | // Save JSON of full schema introspection for Babel Relay Plugin to use 10 | (async () => { 11 | var result = await request('POST', config.scapholdUrl, { qs: { query: introspectionQuery } }); 12 | if (result.errors) { 13 | console.error( 14 | 'ERROR introspecting schema: ', 15 | JSON.stringify(result.errors, null, 2) 16 | ); 17 | } else { 18 | fs.writeFileSync( 19 | path.join(__dirname, '../schema.json'), 20 | result.body 21 | ); 22 | } 23 | })(); 24 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import WebpackDevServer from 'webpack-dev-server'; 4 | import express from 'express'; 5 | import config from './config'; 6 | 7 | const APP_PORT = 3001; 8 | 9 | // Serve the Relay app 10 | var compiler = webpack({ 11 | entry: path.resolve(__dirname, 'js', 'app.js'), 12 | module: { 13 | loaders: [ 14 | { 15 | exclude: /node_modules/, 16 | loader: 'babel', 17 | test: /\.js$/, 18 | query: { 19 | plugins: [path.join(__dirname, 'plugins/babelRelayPlugin')] 20 | } 21 | } 22 | ] 23 | }, 24 | output: {filename: 'app.js', path: '/'} 25 | }); 26 | 27 | var app = new WebpackDevServer(compiler, { 28 | contentBase: '/public/', 29 | publicPath: '/js/', 30 | proxy: { '/graphql': config.scapholdUrl }, 31 | stats: {colors: true} 32 | }); 33 | // Serve static resources 34 | app.use('/', express.static(path.resolve(__dirname, 'public'))); 35 | app.listen(APP_PORT, () => { 36 | console.log(`App is now running on http://localhost:${APP_PORT}`); 37 | }); 38 | --------------------------------------------------------------------------------