├── .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 |
77 | {
78 | this.state.newArticleForm ?
79 |
:
80 |
81 | }
82 |
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 |
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 |
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 |
--------------------------------------------------------------------------------