├── .babelrc ├── .gitignore ├── README.md ├── dist └── bundle.js ├── img └── loading.svg ├── package.json ├── scss ├── _settings.scss ├── components │ ├── _loading.scss │ └── index.scss ├── main.scss └── pages │ ├── _countries.scss │ ├── _country.scss │ └── index.scss ├── src ├── action │ ├── countries.js │ └── types.js ├── common │ ├── Loading.js │ └── index.js ├── components │ ├── Countries │ │ ├── CountriesItem.js │ │ └── index.js │ └── Country │ │ └── index.js ├── index.js ├── reducers │ ├── countries.js │ ├── country.js │ └── index.js ├── router │ ├── Routes.js │ └── index.js └── store.js ├── template.html ├── webpack.build.config.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env"], "react"], 3 | "plugins": [ 4 | "transform-object-rest-spread", 5 | "transform-decorators-legacy", 6 | ["transform-runtime", { 7 | "helpers": false, 8 | "polyfill": false, 9 | "regenerator": true, 10 | "moduleName": "babel-runtime" 11 | }] 12 | ] 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Server Side Rendering Boilerplate 2 | 3 | ## Pre requirements 4 | * [Node.js](https://nodejs.org/) 5 | 6 | ## Libraries docs 7 | * [React](https://reactjs.org/) 8 | * [Redux](https://redux.js.org/introduction) 9 | * [Webpack](https://webpack.js.org/) 10 | * [SASS](https://sass-lang.com/guide) 11 | * [Babel](https://babeljs.io) 12 | * [Express](http://expressjs.com/) 13 | 14 | ## Getting started 15 | ```bash 16 | $ git clone https://github.com/mirchenko/react-ssr-boilerplate.git 17 | $ cd 18 | $ yarn 19 | $ yarn start 20 | ``` 21 | 22 | Service available on `localhost:8080` -------------------------------------------------------------------------------- /img/loading.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ssr-boilerplate", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "webpack-dev-server", 8 | "build": "webpack --config webpack.build.config.js" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.18.0", 12 | "babel-cli": "^6.26.0", 13 | "babel-loader": "^7.1.4", 14 | "babel-preset-es2015": "^6.24.1", 15 | "babel-preset-react": "^6.24.1", 16 | "babel-preset-stage-0": "^6.24.1", 17 | "babel-preset-stage-1": "^6.24.1", 18 | "babel-preset-stage-2": "^6.24.1", 19 | "css-loader": "^0.28.11", 20 | "html-webpack-plugin": "^3.2.0", 21 | "node-sass": "^4.8.3", 22 | "react": "^16.3.1", 23 | "react-dom": "^16.3.1", 24 | "react-redux": "^5.0.7", 25 | "redux": "^3.7.2", 26 | "redux-thunk": "^2.2.0", 27 | "sass-loader": "^7.0.1", 28 | "style-loader": "^0.20.3" 29 | }, 30 | "devDependencies": { 31 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 32 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 33 | "babel-plugin-transform-runtime": "^6.23.0", 34 | "babel-preset-env": "^1.6.1", 35 | "babel-runtime": "^6.26.0", 36 | "react-router-dom": "^4.2.2", 37 | "webpack": "^4.32.2", 38 | "webpack-cli": "^3.3.2", 39 | "webpack-dev-server": "^3.4.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scss/_settings.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .container { 7 | width: 100%; 8 | max-width: 1000px; 9 | margin: 0 auto; 10 | padding: 0 16px; 11 | } -------------------------------------------------------------------------------- /scss/components/_loading.scss: -------------------------------------------------------------------------------- 1 | .loading-container { 2 | width: 200px; 3 | height: 200px; 4 | margin: 100px auto; 5 | 6 | img { 7 | width: 100%; 8 | } 9 | } -------------------------------------------------------------------------------- /scss/components/index.scss: -------------------------------------------------------------------------------- 1 | @import "loading"; -------------------------------------------------------------------------------- /scss/main.scss: -------------------------------------------------------------------------------- 1 | @import "settings"; 2 | @import "components/index"; 3 | @import "pages/index"; 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /scss/pages/_countries.scss: -------------------------------------------------------------------------------- 1 | .countries-container { 2 | width: 100%; 3 | display: flex; 4 | flex-direction: row; 5 | justify-content: flex-start; 6 | align-items: flex-start; 7 | flex-wrap: wrap; 8 | 9 | .countries-item { 10 | text-decoration: none; 11 | color: #224758; 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: flex-start; 15 | align-items: flex-start; 16 | margin: 24px; 17 | padding: 16px; 18 | box-sizing: border-box; 19 | max-width: 200px; 20 | flex: 0 0 200px; 21 | overflow: hidden; 22 | 23 | img { 24 | width: 80px; 25 | //height: 80px; 26 | } 27 | 28 | .countries-item-data { 29 | display: flex; 30 | flex-direction: column; 31 | justify-content: flex-start; 32 | align-items: flex-start; 33 | 34 | h4 { 35 | margin: 8px 0; 36 | font-size: 18px; 37 | line-height: 30px; 38 | } 39 | 40 | span { 41 | overflow: hidden; 42 | text-overflow: ellipsis; 43 | white-space: nowrap; 44 | font-size: 16px; 45 | line-height: 24px; 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /scss/pages/_country.scss: -------------------------------------------------------------------------------- 1 | .country-container { 2 | img { 3 | width: 100%; 4 | } 5 | 6 | .country-data { 7 | margin: 24px 0; 8 | 9 | > div { 10 | margin: 8px 0; 11 | > span { 12 | margin-right: 16px; 13 | } 14 | } 15 | } 16 | 17 | .languages { 18 | > span { 19 | margin-right: 16px; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /scss/pages/index.scss: -------------------------------------------------------------------------------- 1 | @import "countries"; 2 | @import "country"; -------------------------------------------------------------------------------- /src/action/countries.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { 4 | ROOT, 5 | REQUEST_COUNTRIES, 6 | RECEIVE_COUNTRIES, 7 | REQUEST_COUNTRY, 8 | RECEIVE_COUNTRY 9 | } from "./types"; 10 | 11 | 12 | export const fetchCountries = () => async dispatch => { 13 | try { 14 | dispatch({ type: REQUEST_COUNTRIES }); 15 | const res = await axios.get(`${ROOT}/all`); 16 | dispatch({ type: RECEIVE_COUNTRIES, payload: res.data }); 17 | } catch(e) { 18 | console.log(e); 19 | dispatch({ type: RECEIVE_COUNTRIES, payload: [] }); 20 | } 21 | }; 22 | 23 | 24 | export const fetchCountry = name => async dispatch => { 25 | try { 26 | dispatch({ type: REQUEST_COUNTRY }); 27 | const res = await axios.get(`${ROOT}/name/${name}`); 28 | dispatch({ type: RECEIVE_COUNTRY, payload: res.data[0] }); 29 | } catch(e) { 30 | console.log(e); 31 | dispatch({ type: RECEIVE_COUNTRY, payload: {} }); 32 | } 33 | }; -------------------------------------------------------------------------------- /src/action/types.js: -------------------------------------------------------------------------------- 1 | export const ROOT = 'https://restcountries.eu/rest/v2'; 2 | 3 | export const REQUEST_COUNTRIES = 'REQUEST_COUNTRIES'; 4 | export const RECEIVE_COUNTRIES = 'RECEIVE_COUNTRIES'; 5 | 6 | 7 | export const REQUEST_COUNTRY = 'REQUEST_COUNTRY'; 8 | export const RECEIVE_COUNTRY = 'RECEIVE_COUNTRY'; -------------------------------------------------------------------------------- /src/common/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Loading = () =>
4 |
5 | 6 |
7 |
; -------------------------------------------------------------------------------- /src/common/index.js: -------------------------------------------------------------------------------- 1 | export * from './Loading'; -------------------------------------------------------------------------------- /src/components/Countries/CountriesItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | export default ({ name, flag, capital, population }) => { 5 | return ( 6 | 7 | 8 |
9 |

{name}

10 | {capital} 11 | {population} pop. 12 |
13 |
14 | ); 15 | }; -------------------------------------------------------------------------------- /src/components/Countries/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchCountries } from "../../action/countries"; 4 | import { Loading } from '../../common'; 5 | import CountriesItem from './CountriesItem'; 6 | 7 | const m = ({ countries }) => ({ countries }); 8 | 9 | 10 | @connect(m, { fetchCountries }) 11 | export default class Countries extends Component { 12 | 13 | componentDidMount() { 14 | this.props.fetchCountries(); 15 | } 16 | 17 | 18 | render() { 19 | const { countries: { isFetching, data } } = this.props; 20 | 21 | if(isFetching) { 22 | return 23 | } 24 | 25 | return( 26 |
27 |
28 | {data.map((item, i) => )} 29 |
30 |
31 | ); 32 | } 33 | }; -------------------------------------------------------------------------------- /src/components/Country/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchCountry } from "../../action/countries"; 4 | import { Loading } from "../../common"; 5 | 6 | const m = ({ country }) => ({ country }); 7 | 8 | @connect(m, { fetchCountry }) 9 | export default class Country extends Component { 10 | 11 | componentDidMount() { 12 | this.props.fetchCountry(this.props.match.params.name); 13 | } 14 | 15 | render() { 16 | const { country: { isFetching, flag, name, nativeName, capital, region, population, languages } } = this.props; 17 | 18 | if(isFetching) { 19 | return ; 20 | } 21 | 22 | return( 23 |
24 |
25 | 26 |
27 |
28 | Name: 29 | {name} 30 |
31 |
32 | NativeName: 33 | {nativeName} 34 |
35 |
36 | Capital: 37 | {capital} 38 |
39 |
40 | Region: 41 | {region} 42 |
43 | 44 |
45 | Population: 46 | {population} 47 |
48 |
49 |
50 | 51 |
52 | {languages.map((item, i) => {item.name})} 53 |
54 | 55 |
56 |
57 | ); 58 | } 59 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hydrate } from 'react-dom'; 3 | import Router from './router'; 4 | import { Provider } from 'react-redux'; 5 | import store from './store'; 6 | import '../scss/main.scss'; 7 | 8 | hydrate( 9 | 10 | 11 | , 12 | document.querySelector('#app') 13 | ); -------------------------------------------------------------------------------- /src/reducers/countries.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_COUNTRIES, 3 | RECEIVE_COUNTRIES 4 | } from "../action/types"; 5 | 6 | const INITIAL_STATE = { 7 | data: [], 8 | isFetching: false, 9 | lastUpdate: Date.now() 10 | }; 11 | 12 | export default (state = INITIAL_STATE, action) => { 13 | switch (action.type) { 14 | case REQUEST_COUNTRIES: { 15 | return { ...state, isFetching: true }; 16 | } 17 | case RECEIVE_COUNTRIES: { 18 | return { ...state, isFetching: false, data: action.payload }; 19 | } 20 | default: 21 | return state; 22 | } 23 | }; -------------------------------------------------------------------------------- /src/reducers/country.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_COUNTRY, 3 | RECEIVE_COUNTRY 4 | } from "../action/types"; 5 | 6 | const INITIAL_STATE = { 7 | name: '', 8 | nativeName: '', 9 | flag: '', 10 | capital: '', 11 | region: '', 12 | population: '', 13 | languages: [], 14 | isFetching: false, 15 | lastUpdate: Date.now() 16 | }; 17 | 18 | export default(state = INITIAL_STATE, action) => { 19 | switch(action.type) { 20 | case REQUEST_COUNTRY: { 21 | return { ...state, isFetching: true }; 22 | } 23 | case RECEIVE_COUNTRY: { 24 | return { ...state, isFetching: false, ...action.payload }; 25 | } 26 | default: return state; 27 | } 28 | }; -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import Countries from "./countries"; 3 | import Country from "./Country"; 4 | 5 | export default combineReducers({ 6 | countries: Countries, 7 | country: Country, 8 | }); -------------------------------------------------------------------------------- /src/router/Routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import Countries from "../components/Countries"; 4 | import Country from "../components/Country"; 5 | 6 | 7 | export default () => { 8 | return( 9 |
10 | 11 | 12 |
13 | ); 14 | }; -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import Routes from './Routes'; 4 | 5 | export default () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from "redux"; 2 | import rootReducer from "./reducers"; 3 | import reduxThunk from "redux-thunk"; 4 | 5 | export default createStore(rootReducer, {}, applyMiddleware(reduxThunk)); -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /webpack.build.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: './src/index.js', 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: 'bundle.js' 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.js$/, 14 | use: 'babel-loader', 15 | exclude: /node_modules/ 16 | }, 17 | { 18 | test: /\.scss$/, 19 | use: [ 20 | { loader: 'style-loader' }, 21 | { loader: 'css-loader' }, 22 | { loader: 'sass-loader' }, 23 | ] 24 | } 25 | ] 26 | } 27 | }; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: './src/index.js', 7 | output: { 8 | filename: 'bundle.js' 9 | }, 10 | devServer: { 11 | contentBase: path.join(__dirname), 12 | compress: true, 13 | port: 8080, 14 | historyApiFallback: true 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | use: 'babel-loader', 21 | exclude: /node_modules/ 22 | }, 23 | { 24 | test: /\.scss$/, 25 | use: [ 26 | { loader: 'style-loader' }, 27 | { loader: 'css-loader' }, 28 | { loader: 'sass-loader' }, 29 | ] 30 | } 31 | ] 32 | }, 33 | plugins: [ 34 | new HtmlWebpackPlugin({ 35 | template: 'template.html' 36 | }) 37 | ] 38 | }; --------------------------------------------------------------------------------