├── .babelrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── config.js ├── package.json ├── sass └── style.scss ├── server.js ├── serverRender.js ├── src ├── api.js ├── components │ ├── App.js │ ├── Article.js │ ├── ArticleList.js │ ├── ArticleRow.js │ ├── Author.js │ └── NewArticleForm.js └── index.js ├── views └── index.ejs └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "latest", "stage-2"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": 'babel-eslint', 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "extends": ["eslint:recommended", "plugin:react/recommended"], 10 | "parserOptions": { 11 | "ecmaFeatures": { 12 | "experimentalObjectRestSpread": true, 13 | "jsx": true 14 | }, 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ "react" ], 18 | "rules": { 19 | "react/prop-types": ["off"], 20 | "indent": ["error", 2], 21 | "linebreak-style": ["error","unix"], 22 | "quotes": ["error","single"], 23 | "semi": ["error","always"], 24 | "no-console": ["warn", { "allow": ["info", "error"] }] 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | public/bundle.js 39 | public/bundle.js.map 40 | public/style.css 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React Blog Example 2 | 3 | A blog example written in React 4 | 5 | ### Development 6 | 7 | ``` 8 | npm install 9 | npm run nodemon 10 | npm run webpack 11 | ``` 12 | 13 | Server will be running on [http://localhost:8080/](http://localhost:8080/) 14 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const env = process.env; 2 | 3 | export const nodeEnv = env.NODE_ENV || 'development'; 4 | 5 | export default { 6 | port: env.PORT || 8080, 7 | host: env.HOST || 'localhost', 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsComplete", 3 | "version": "1.0.0", 4 | "description": "jsComplete.com", 5 | "main": "index.js", 6 | "scripts": { 7 | "nodemon": "nodemon --exec babel-node server.js --ignore public/", 8 | "webpack": "webpack -wd" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/jscomplete/node-react-template.git" 13 | }, 14 | "author": "", 15 | "license": "GPL-3.0", 16 | "bugs": { 17 | "url": "https://github.com/jscomplete/node-react-template/issues" 18 | }, 19 | "homepage": "https://github.com/jscomplete/node-react-template#readme", 20 | "dependencies": { 21 | "axios": "^0.15.3", 22 | "body-parser": "^1.16.0", 23 | "ejs": "^2.5.5", 24 | "express": "^4.14.0", 25 | "json-loader": "^0.5.4", 26 | "node-sass-middleware": "^0.9.8", 27 | "react": "^15.4.2", 28 | "react-dom": "^15.4.2" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "^6.22.2", 32 | "babel-eslint": "^7.1.1", 33 | "babel-loader": "^6.2.10", 34 | "babel-preset-latest": "^6.22.0", 35 | "babel-preset-react": "^6.22.0", 36 | "babel-preset-stage-2": "^6.22.0", 37 | "eslint": "^3.14.1", 38 | "eslint-plugin-react": "^6.9.0", 39 | "nodemon": "^1.11.0", 40 | "webpack": "^1.14.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sass/style.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | #left { 6 | float: left; 7 | padding-left: 1em; 8 | padding-right: 1em; 9 | width: 30%; 10 | } 11 | 12 | #right { 13 | float: left; 14 | padding-left: 1em; 15 | border-left: 1px solid #bbb; 16 | width: 60%; 17 | min-height: 100px; 18 | } 19 | 20 | #header { 21 | background-color: #ddd; 22 | margin: 0; 23 | padding: 1em; 24 | margin-bottom: 1em; 25 | } 26 | 27 | .article-row { 28 | border-bottom: 1px dashed #ccc; 29 | margin-top: 1em; 30 | padding-bottom: 0.5em; 31 | } 32 | 33 | .article-date { 34 | font-size: 0.9em; 35 | color: #666; 36 | padding: 0.5em 0; 37 | } 38 | 39 | .article-body { 40 | padding: 1em; 41 | white-space: pre-line; 42 | color: #333; 43 | } 44 | 45 | #new-article { 46 | margin-top: 2em; 47 | } 48 | 49 | .link { 50 | cursor: pointer; 51 | } 52 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import config from './config'; 2 | import sassMiddleware from 'node-sass-middleware'; 3 | import path from 'path'; 4 | import express from 'express'; 5 | import bodyParser from 'body-parser'; 6 | 7 | const server = express(); 8 | server.use(bodyParser.json()); 9 | 10 | server.use(sassMiddleware({ 11 | src: path.join(__dirname, 'sass'), 12 | dest: path.join(__dirname, 'public') 13 | })); 14 | 15 | server.set('view engine', 'ejs'); 16 | 17 | server.get('/', (req, res) => { 18 | res.render('index'); 19 | }); 20 | 21 | server.use(express.static('public')); 22 | 23 | server.listen(config.port, config.host, () => { 24 | console.info('Express listening on port', config.port); 25 | }); 26 | -------------------------------------------------------------------------------- /serverRender.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | 4 | import App from './src/components/App'; 5 | 6 | import config from './config'; 7 | import axios from 'axios'; 8 | 9 | const getApiUrl = () => { 10 | return `${config.serverUrl}/api/items`; 11 | }; 12 | 13 | const getInitialData = (apiData) => { 14 | return apiData; 15 | }; 16 | 17 | const serverRender = () => 18 | axios.get(getApiUrl()) 19 | .then(resp => { 20 | const initialData = getInitialData(resp.data); 21 | return { 22 | initialMarkup: ReactDOMServer.renderToString( 23 | 24 | ), 25 | initialData 26 | }; 27 | }); 28 | 29 | export default serverRender; 30 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | const rawData = { 2 | articles: [ 3 | { 4 | id: 'build-blog-with-react', 5 | title: 'So you want to build a blog with React?', 6 | date: new Date().toString(), 7 | author: { 8 | firstName: 'Samer', 9 | lastName: 'Buna', 10 | website: 'https://twitter.com/samerbuna', 11 | }, 12 | body: ` 13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 14 | 15 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 16 | 17 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 18 | ` 19 | }, 20 | { 21 | id: 'graphql-is-legen-dary', 22 | title: 'GraphQL is legen--wait-for-it--dary', 23 | date: new Date().toString(), 24 | author: { 25 | firstName: 'Samer', 26 | lastName: 'Buna', 27 | website: 'https://twitter.com/samerbuna', 28 | }, 29 | body: ` 30 | incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 31 | 32 | incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 33 | 34 | incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 35 | 36 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 37 | 38 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 39 | ` 40 | }, 41 | { 42 | id: 'react-16-released', 43 | title: 'React 16.0 is released', 44 | date: new Date().toString(), 45 | author: { 46 | firstName: 'The React', 47 | lastName: 'Team', 48 | website: 'https://twitter.com/reactjs' 49 | }, 50 | body: ` 51 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 52 | 53 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 54 | 55 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 56 | ` 57 | } 58 | ] 59 | }; 60 | 61 | const deepCopy = (obj) => JSON.parse(JSON.stringify(obj)); 62 | 63 | let appData = { 64 | articles: [] 65 | }; 66 | 67 | export const getArticleList = () => { 68 | appData.articles = rawData.articles.reduce((acc, article) => { 69 | acc.push({ id: article.id, title: article.title, date: article.date }); 70 | return acc; 71 | }, []); 72 | 73 | return Promise.resolve(deepCopy(appData.articles)); 74 | }; 75 | 76 | export const getArticle = (articleId) => { 77 | appData.currentArticle = rawData.articles.find(article => article.id === articleId); 78 | return Promise.resolve(deepCopy(appData.currentArticle)); 79 | }; 80 | 81 | export const addArticle = (articleInfo) => { 82 | const newArticle = Object.assign({}, articleInfo, { 83 | id: articleInfo.title.toLowerCase().replace(/[^a-z]+/, '-'), 84 | date: new Date().toString(), 85 | }); 86 | rawData.articles.push(newArticle); 87 | appData.currentArticle = newArticle; 88 | return Promise.resolve(deepCopy(appData.currentArticle)); 89 | }; 90 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ArticleList from './ArticleList'; 4 | import Article from './Article'; 5 | import NewArticleForm from './NewArticleForm'; 6 | 7 | import * as api from '../api'; 8 | 9 | class App extends React.Component { 10 | 11 | state = { 12 | data: { 13 | articles: [], 14 | currentArticle: {} 15 | }, 16 | newArticleForm: false 17 | }; 18 | 19 | componentDidMount() { 20 | api.getArticleList().then(articleList => { 21 | this.setState((prevState) => ({ 22 | data: { 23 | ...prevState.data, 24 | articles: articleList, 25 | }, 26 | })); 27 | }); 28 | } 29 | 30 | setCurrentArticle = (articleId) => { 31 | api.getArticle(articleId).then(article => { 32 | this.setState((prevState) => ({ 33 | data: { 34 | ...prevState.data, 35 | currentArticle: article, 36 | }, 37 | newArticleForm: false, 38 | })); 39 | }); 40 | }; 41 | 42 | showNewArticleForm = (event) => { 43 | event.preventDefault(); 44 | this.setState({ newArticleForm: true }); 45 | } 46 | 47 | addArticle = (articleInput) => { 48 | api.addArticle(articleInput).then(newArticle => { 49 | this.setState((prevState) => ({ 50 | data: { 51 | articles: [...prevState.data.articles, newArticle], 52 | currentArticle: newArticle, 53 | }, 54 | newArticleForm: false, 55 | })); 56 | }); 57 | }; 58 | 59 | render() { 60 | return ( 61 |
62 | 63 | 64 |
65 |

Article List

66 | 70 | 71 | 74 |
75 | 76 | 83 | 84 |
85 | ); 86 | } 87 | } 88 | 89 | export default App; 90 | -------------------------------------------------------------------------------- /src/components/Article.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Author from './Author'; 4 | 5 | class Article extends React.Component { 6 | render() { 7 | if (!this.props.title) { 8 | return

Select an Article

; 9 | } 10 | return ( 11 |
12 |

{this.props.title}

13 |
14 | {this.props.date} 15 |
16 | 17 |
18 | {this.props.body} 19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | export default Article; 26 | -------------------------------------------------------------------------------- /src/components/ArticleList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ArticleRow from './ArticleRow'; 4 | 5 | class ArticleList extends React.Component { 6 | render() { 7 | const { articles, onArticleClick } = this.props; 8 | return ( 9 |
10 | {articles.map(article => 11 | 16 | )} 17 |
18 | ); 19 | } 20 | } 21 | 22 | export default ArticleList; 23 | -------------------------------------------------------------------------------- /src/components/ArticleRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class ArticleRow extends React.Component { 4 | handleClick = (event) => { 5 | event.preventDefault(); 6 | this.props.onClick(this.props.id); 7 | }; 8 | render() { 9 | return ( 10 |
11 |
{this.props.title}
12 |
{this.props.date}
13 |
14 | ); 15 | } 16 | } 17 | 18 | export default ArticleRow; 19 | -------------------------------------------------------------------------------- /src/components/Author.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Author extends React.Component { 4 | render() { 5 | return ( 6 |
7 | By: {this.props.firstName} {this.props.lastName} 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default Author; 14 | -------------------------------------------------------------------------------- /src/components/NewArticleForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class NewArticleForm extends React.Component { 4 | handleSubmit = (event) => { 5 | event.preventDefault(); 6 | this.props.addArticle({ 7 | title: this.titleInput.value, 8 | author: { 9 | firstName: this.authorFirstNameInput.value, 10 | lastName: this.authorLastNameInput.value, 11 | website: this.authorWebsiteInput.value, 12 | }, 13 | body: this.bodyInput.value, 14 | }); 15 | } 16 | render() { 17 | return ( 18 |
19 |

New Article

20 | 21 |
22 | this.titleInput = input} 24 | className="form-control" 25 | placeholder="Article Title" /> 26 |
27 | this.authorFirstNameInput = input} 29 | className="form-control" 30 | placeholder="Author First Name" /> 31 |
32 | this.authorLastNameInput = input} 34 | className="form-control" 35 | placeholder="Author Last Name" /> 36 |
37 | this.authorWebsiteInput = input} 39 | className="form-control" 40 | placeholder="Author Website" /> 41 |
42 | 47 |
48 | 51 |
52 |
53 | ); 54 | } 55 | } 56 | 57 | export default NewArticleForm; 58 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './components/App'; 5 | 6 | ReactDOM.render( 7 | , 8 | document.getElementById('root') 9 | ); 10 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Blog Example 8 | 9 | 10 | 11 | 12 |
Loading...
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | path: path.join(__dirname, 'public'), 7 | filename: 'bundle.js' 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.js$/, 13 | exclude: /node_modules/, 14 | loader: 'babel-loader' 15 | } 16 | ] 17 | } 18 | }; 19 | --------------------------------------------------------------------------------