├── LICENSE ├── README.md ├── client ├── .gitignore ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.js │ ├── App.test.js │ ├── Header.js │ ├── HomePage.js │ ├── Layout.js │ ├── NewTweetPage.js │ ├── Notification.js │ ├── Tweet.js │ ├── TweetPage.js │ ├── currentUserQuery.js │ ├── fragments.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── notifications.js │ ├── registerServiceWorker.js │ └── withCurrentUser.js └── yarn.lock ├── makefile ├── schema.graphql └── server ├── .babelrc ├── .gitignore ├── package-lock.json ├── package.json └── src ├── base.js ├── context.js ├── data.js ├── index.js ├── resolvers.js ├── scalar └── Date.js ├── schema.js ├── schema.spec.js ├── stat ├── resolvers.js └── schema.js ├── tweet ├── resolvers.js ├── resolvers.spec.js └── schema.js └── user ├── resolvers.js ├── resolvers.spec.js └── schema.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 marmelab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 |
hackdayArchived Repository
5 | The code of this repository was written during a Hack Day by a Marmelab developer. It's part of the distributed R&D effort at Marmelab, where each developer spends 2 days a month for learning and experimentation.
6 | This code is not intended to be used in production, and is not maintained. 7 |
10 | 11 | # GraphQL-example 12 | 13 | An example code structure for a GraphQL-powered mobile app in full-stack JavaScript. Contains client and server code. Architecture and code explained in the [Dive into Graphql series](https://marmelab.com/blog/2017/09/03/dive-into-graphql.html). 14 | 15 | ## Installation 16 | 17 | ```sh 18 | npm install 19 | ``` 20 | 21 | ## Run the server 22 | 23 | ```sh 24 | npm start 25 | ``` 26 | 27 | ## Run tests 28 | 29 | ```sh 30 | npm test 31 | ``` 32 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "apollo-link": "^0.5.0", 7 | "date-fns": "^1.28.5", 8 | "material-ui": "^1.0.0-beta.8", 9 | "material-ui-icons": "^1.0.0-alpha.19", 10 | "react": "^15.6.1", 11 | "react-apollo": "^1.4.14", 12 | "react-dom": "^15.6.1", 13 | "react-redux": "^5.0.6", 14 | "react-router-dom": "^4.1.2", 15 | "react-scripts": "1.0.11", 16 | "redux": "^3.7.2" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/GraphQL-example/6f6b561e9da0d185e147e9bfdd92920748f486f5/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 3 | import { gql, graphql } from 'react-apollo'; 4 | import { userFragment } from './fragments'; 5 | 6 | import HomePage from './HomePage'; 7 | import Header from './Header'; 8 | import TweetPage from './TweetPage'; 9 | import NewTweetPage from './NewTweetPage'; 10 | 11 | const App = ({ data: { currentUser } }) => 12 | 13 |
14 |
15 | 16 | 17 | 18 | {/* This route must come before the one with a parameter to be matched */} 19 | } /> 20 | 21 | 22 |
23 |
; 24 | 25 | export const currentUserQuery = gql` 26 | query headerQuery { 27 | currentUser: User { 28 | ...UserFields 29 | } 30 | } 31 | 32 | ${userFragment} 33 | `; 34 | 35 | export default graphql(currentUserQuery)(App); 36 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import AppBar from 'material-ui/AppBar'; 4 | import Avatar from 'material-ui/Avatar'; 5 | import IconButton from 'material-ui/IconButton'; 6 | import ModeEditIcon from 'material-ui-icons/ModeEdit'; 7 | import Toolbar from 'material-ui/Toolbar'; 8 | import Typography from 'material-ui/Typography'; 9 | import { withStyles, createMuiTheme } from 'material-ui/styles'; 10 | import { Link } from 'react-router-dom'; 11 | 12 | const styleSheet = createMuiTheme(theme => ({ 13 | title: { 14 | margin: '0 auto', 15 | } 16 | })); 17 | 18 | const Header = ({ classes, currentUser }) => ( 19 | 20 | 21 | {currentUser && 22 | 23 | } 24 | 25 | Home 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | 34 | Header.propTypes = { 35 | classes: PropTypes.object.isRequired, 36 | currentUser: PropTypes.shape({ 37 | id: PropTypes.string.isRequired, 38 | username: PropTypes.string.isRequired, 39 | full_name: PropTypes.string.isRequired, 40 | avatar_url: PropTypes.string.isRequired, 41 | }), 42 | }; 43 | 44 | export default withStyles(styleSheet)(Header); 45 | -------------------------------------------------------------------------------- /client/src/HomePage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { compose, gql, graphql } from 'react-apollo'; 5 | import { LinearProgress } from 'material-ui/Progress'; 6 | import Button from 'material-ui/Button'; 7 | import { withStyles, createMuiTheme } from 'material-ui/styles'; 8 | 9 | import Notification from './Notification'; 10 | import Tweet from './Tweet'; 11 | import { tweetFragment, userFragment } from './fragments'; 12 | import { removeNotification as removeNotificationAction } from './notifications'; 13 | 14 | const styleSheet = createMuiTheme(theme => ({ 15 | button: { 16 | textAlign: 'center', 17 | width: '100%', 18 | } 19 | })); 20 | 21 | class HomePage extends Component { 22 | state = { skip: 5 }; 23 | 24 | handleClick = () => { 25 | this.props.loadMore(this.state.skip).then(() => { 26 | this.setState(({ skip }) => ({ skip: skip + 5 })); 27 | }); 28 | } 29 | 30 | handleNotificationClose = notificationId => { 31 | this.props.removeNotification(notificationId); 32 | } 33 | 34 | render() { 35 | const { classes, loading, notifications, tweets } = this.props; 36 | return ( 37 |
38 | {loading && } 39 | {!loading && tweets.map(tweet => 40 | 41 | )} 42 | 43 | 46 | 47 | {notifications.map(notification => 48 | 53 | )} 54 |
55 | ); 56 | } 57 | } 58 | 59 | HomePage.propTypes = { 60 | data: PropTypes.shape({ 61 | loading: PropTypes.bool.isRequired, 62 | tweets: PropTypes.arrayOf( 63 | PropTypes.shape({ 64 | id: PropTypes.string.isRequired, 65 | body: PropTypes.string.isRequired, 66 | date: PropTypes.string.isRequired, 67 | Author: PropTypes.shape({ 68 | id: PropTypes.string.isRequired, 69 | username: PropTypes.string.isRequired, 70 | full_name: PropTypes.string.isRequired, 71 | avatar_url: PropTypes.string.isRequired, 72 | }).isRequired, 73 | Stats: PropTypes.shape({ 74 | views: PropTypes.number.isRequired, 75 | likes: PropTypes.number.isRequired, 76 | retweets: PropTypes.number.isRequired, 77 | responses: PropTypes.number.isRequired, 78 | }).isRequired, 79 | }).isRequired, 80 | ), 81 | }), 82 | }; 83 | 84 | HomePage.defaultProps = { 85 | tweets: [], 86 | }; 87 | 88 | export const homePageQuery = gql` 89 | query homePageQuery($limit: Int!, $skip: Int!) { 90 | tweets: Tweets(limit:$limit, skip:$skip, sort_field:"date", sort_order:"desc") { 91 | ...TweetFields 92 | } 93 | } 94 | 95 | ${userFragment} 96 | ${tweetFragment} 97 | `; 98 | 99 | export const homePageQueryVariables = { limit: 5, skip: 0 }; 100 | 101 | const mapStateToProps = state => ({ notifications: state.notifications }); 102 | 103 | export default compose( 104 | connect(mapStateToProps, { removeNotification: removeNotificationAction }), 105 | graphql(homePageQuery, { 106 | options: { 107 | variables: homePageQueryVariables, 108 | }, 109 | props: ({ data: { loading, tweets, fetchMore } }) => ({ 110 | loading, 111 | tweets, 112 | loadMore: skip => 113 | fetchMore({ 114 | variables: { limit: 5, skip }, 115 | updateQuery: (previousResult, { fetchMoreResult }) => ({ 116 | ...previousResult, 117 | tweets: [ 118 | ...previousResult.tweets, 119 | ...fetchMoreResult.tweets, 120 | ], 121 | }), 122 | }) 123 | }), 124 | }), 125 | withStyles(styleSheet), 126 | )(HomePage); 127 | -------------------------------------------------------------------------------- /client/src/Layout.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/GraphQL-example/6f6b561e9da0d185e147e9bfdd92920748f486f5/client/src/Layout.js -------------------------------------------------------------------------------- /client/src/NewTweetPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withStyles, createMuiTheme } from 'material-ui/styles'; 4 | import Button from 'material-ui/Button'; 5 | import { LinearProgress } from 'material-ui/Progress'; 6 | import { compose, gql, graphql } from 'react-apollo'; 7 | import { withRouter } from 'react-router'; 8 | import { userFragment, tweetFragment } from './fragments'; 9 | import { homePageQuery, homePageQueryVariables } from './HomePage'; 10 | import { notify as notifyAction } from './notifications'; 11 | 12 | const styleSheet = createMuiTheme(theme => ({ 13 | container: { 14 | margin: '1rem', 15 | }, 16 | button: { 17 | marginTop: '1rem', 18 | marginBottom: '1rem', 19 | }, 20 | textarea: { 21 | width: '100%', 22 | resize: 'none', 23 | boxSizing: 'border-box', 24 | } 25 | })); 26 | 27 | class NewTweetPage extends Component { 28 | state = { body: '', saving: false }; 29 | 30 | handleChange = event => { 31 | this.setState({ body: event.target.value }); 32 | } 33 | 34 | handleSubmit = () => { 35 | this.setState({ saving: true }, () => { 36 | this.props.submit(this.state.body); 37 | // The submit promise will only resolve once the server has anwsered. 38 | // As we have an optimistic response setup, we don't wait for the promise 39 | // to be resolved and redirect the user to the list immediatly 40 | this.props.redirectToHome(); 41 | }); 42 | } 43 | 44 | render() { 45 | const { classes, saving } = this.props; 46 | const { body } = this.state; 47 | 48 | return ( 49 |
50 | 51 |