├── src ├── components │ ├── TagsList.js │ ├── __snapshots__ │ │ ├── TagsList.spec.js.snap │ │ └── QuestionList.spec.js.snap │ ├── TagsList.spec.js │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── NotificationsViewer.js.snap │ │ │ └── QuestionDetail.js.snap │ │ ├── NotificationsViewer.js │ │ └── QuestionDetail.js │ ├── NotificationsViewer.spec.js │ ├── QuestionDetail.jsx │ ├── QuestionList.jsx │ ├── QuestionList.spec.js │ └── NotificationsViewer.jsx ├── reducers │ ├── index.js │ ├── questions.js │ └── questions.spec.js ├── App.spec.js ├── services │ ├── NotificationsService.js │ └── __mocks__ │ │ └── NotificationsService.js ├── sagas │ ├── fetch-question-saga.js │ ├── fetch-questions-saga.js │ └── fetch-question-saga.spec.js ├── App.jsx ├── getStore.js └── index.jsx ├── refresh.MD ├── __mocks__ └── isomorphic-fetch.js ├── .babelrc ├── data ├── api-real-url.js └── mock-questions.json ├── public └── index.html ├── .gitignore ├── webpack.config.prod.babel.js ├── webpack.config.dev.babel.js ├── package.json ├── README.md └── server └── index.jsx /src/components/TagsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export default ({tags})=>( 3 |
4 | {tags.map(tag=>{tag})} 5 |
6 | ) -------------------------------------------------------------------------------- /refresh.MD: -------------------------------------------------------------------------------- 1 | # Notes regarding 2021 Refresh 2 | 3 | - Updated all packages automatically to remove security errors 4 | - Forced fix causes call stack exceeded error 5 | - Replaced webpack uglify plugin -------------------------------------------------------------------------------- /__mocks__/isomorphic-fetch.js: -------------------------------------------------------------------------------- 1 | // import jest from 'jest'; 2 | let __value = 42; 3 | const isomorphicFetch = jest.fn(()=>__value); 4 | isomorphicFetch.__setValue = v=> __value = v; 5 | export default isomorphicFetch; -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exports of this module is an object where the keys are the names of the properties the reducers operate on, 3 | * and the value is the actual reducer 4 | */ 5 | export { questions } from './questions'; -------------------------------------------------------------------------------- /src/App.spec.js: -------------------------------------------------------------------------------- 1 | import delay from 'redux-saga'; 2 | 3 | it("async test 1",done=>{ 4 | setTimeout(done,100); 5 | }); 6 | it("async test 2",()=>{ 7 | return new Promise(resolve=>setTimeout(resolve,100)) 8 | }); 9 | it("async test 3",async ()=>await delay(100)); 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "node": "10" 9 | } 10 | } 11 | ] 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-proposal-object-rest-spread" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/services/NotificationsService.js: -------------------------------------------------------------------------------- 1 | import { delay } from 'redux-saga'; 2 | 3 | export default { 4 | 5 | async getNotifications(){ 6 | 7 | // console.warn("Contacting real notifications server!"); 8 | await delay(2000); 9 | return { count : 3 }; 10 | 11 | } 12 | 13 | }; 14 | -------------------------------------------------------------------------------- /src/services/__mocks__/NotificationsService.js: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | 3 | export default { 4 | 5 | __setCount(_count){ 6 | 7 | count = _count; 8 | 9 | }, 10 | async getNotifications(){ 11 | 12 | // console.warn("Using mock notification service"); 13 | return { count }; 14 | 15 | } 16 | }; 17 | 18 | -------------------------------------------------------------------------------- /src/components/__snapshots__/TagsList.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`The tags list renders as expected 1`] = ` 4 |
5 | 6 | css 7 | 8 | 9 | html 10 | 11 | 12 | typescript 13 | 14 | 15 | coffeescript 16 | 17 |
18 | `; 19 | -------------------------------------------------------------------------------- /src/components/TagsList.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TagsList from './TagsList' 3 | import renderer from 'react-test-renderer'; 4 | 5 | 6 | describe("The tags list",()=>{ 7 | /** 8 | * The tagsList can be tested against an expected snapshot value, as in below. 9 | */ 10 | it ("renders as expected",()=>{ 11 | const tree = renderer 12 | .create() 13 | .toJSON(); 14 | 15 | expect(tree).toMatchSnapshot(); 16 | }); 17 | }); -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/NotificationsViewer.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`The stateful notifications viewer Should have a passing snapshot 1`] = ` 4 |
7 |
10 | Loading... 11 |
12 |
13 | `; 14 | 15 | exports[`The stateful notifications viewer Should have a passing snapshot 2`] = ` 16 |
19 |
22 | 5 Notifications 23 |
24 |
25 | `; 26 | -------------------------------------------------------------------------------- /data/api-real-url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The URL to receive a list of questions in JSON from StackOverflow. 3 | * Works if you paste it into your browser's URL bar. 4 | * Subject to eventual deprecation by its authors (Use mock data after that point) 5 | */ 6 | export const questions = `https://api.stackexchange.com/2.0/questions?site=stackoverflow`; 7 | /** 8 | * The URL to receive details on a single question. 9 | * This request also returns the body of the question 10 | * @param id 11 | * The question ID to fetch 12 | */ 13 | export const question = (id)=>`https://api.stackexchange.com/2.0/questions/${id}?site=stackoverflow&filter=withbody`; -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/QuestionDetail.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`The question detail view The display component Should not regress 1`] = ` 4 |
5 |

8 | The meaning of life 9 |

10 |
11 |
14 |
15 | 16 | hitchhiking 17 | 18 |
19 |
20 |
21 |

22 | 42 23 |

24 |
25 |
26 | 0 27 | Answers 28 |
29 |
30 |
31 | `; 32 | -------------------------------------------------------------------------------- /src/components/__snapshots__/QuestionList.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`The question list The items of the question list should display a list of items 1`] = ` 4 |
7 |

8 | The meaning of life 9 |

10 |
13 |
14 | 15 | css 16 | 17 | 18 | javascript 19 | 20 |
21 |
22 |
23 | 27 | 30 | 31 |
32 |
33 | `; 34 | -------------------------------------------------------------------------------- /src/sagas/fetch-question-saga.js: -------------------------------------------------------------------------------- 1 | import { takeEvery, put } from 'redux-saga/effects' 2 | import fetch from 'isomorphic-fetch'; 3 | 4 | export default function * () { 5 | /** 6 | * Every time REQUEST_FETCH_QUESTION, fork a handleFetchQuestion process for it 7 | */ 8 | yield takeEvery(`REQUEST_FETCH_QUESTION`,handleFetchQuestion); 9 | } 10 | 11 | /** 12 | * Fetch question details from the local proxy API 13 | */ 14 | export function * handleFetchQuestion ({question_id}) { 15 | const raw = yield fetch(`/api/questions/${question_id}`); 16 | const json = yield raw.json(); 17 | const question = json.items[0]; 18 | /** 19 | * Notify application that question has been fetched 20 | */ 21 | yield put({type:`FETCHED_QUESTION`,question}); 22 | } -------------------------------------------------------------------------------- /src/sagas/fetch-questions-saga.js: -------------------------------------------------------------------------------- 1 | import { put, take } from 'redux-saga/effects' 2 | import fetch from 'isomorphic-fetch'; 3 | /** 4 | * Fetch questions saga gets a list of all new 5 | * questions in response to a particular view being loaded 6 | */ 7 | export default function * () { 8 | while (true) { 9 | /** 10 | * Wait for a request to fetch questions, then fetch data from the API and notify the application 11 | * that new questions have been loaded. 12 | */ 13 | yield take(`REQUEST_FETCH_QUESTIONS`); 14 | console.log("Got fetch questions request"); 15 | const raw = yield fetch('/api/questions'); 16 | const json = yield raw.json(); 17 | const questions = json.items; 18 | yield put({type:`FETCHED_QUESTIONS`,questions}); 19 | } 20 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Isomorphic React 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | <%= preloadedApplication %> 16 |
17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/reducers/questions.js: -------------------------------------------------------------------------------- 1 | import unionWith from 'lodash/unionWith'; 2 | 3 | /** 4 | * Questions reducer, deals mostly with actions dispatched from sagas. 5 | */ 6 | export const questions = (state = [],{type,question,questions})=>{ 7 | /** 8 | * Question Equality returns true if two questions are equal, based on a weak check of their question_id property 9 | * @param a 10 | * The first question 11 | * @param b 12 | * The second question 13 | * @returns {boolean} 14 | * Whether the questions are equal 15 | */ 16 | const questionEquality = (a = {},b = {})=>{ 17 | return a.question_id == b.question_id 18 | }; 19 | 20 | /** 21 | * Create a new state by combining the existing state with the question(s) that has been newly fetched 22 | */ 23 | if (type === `FETCHED_QUESTION`) { 24 | state = unionWith([question],state,questionEquality); 25 | } 26 | 27 | if (type === `FETCHED_QUESTIONS`) { 28 | state = unionWith(state,questions,questionEquality); 29 | } 30 | return state; 31 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | .idea 14 | 15 | public/bundle.js 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | dist 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | -------------------------------------------------------------------------------- /src/components/NotificationsViewer.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import delay from 'redux-saga'; 4 | 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | import { shallow, configure } from 'enzyme'; 7 | 8 | configure({adapter: new Adapter()}); 9 | 10 | import NotificationsViewer from './NotificationsViewer'; 11 | 12 | jest.mock('../services/NotificationsService'); 13 | 14 | const notificationService = require('../services/NotificationsService').default; 15 | 16 | notificationService.default = jest.fn(); 17 | 18 | describe('The notification viewer', () => { 19 | 20 | beforeAll(() => { 21 | notificationService.default.mockClear(); 22 | notificationService.__setCount(42); 23 | }); 24 | 25 | it('should display the correct number of notifications', async() => { 26 | const tree = renderer.create(); 27 | const wrapper = shallow(); 28 | 29 | await delay(); 30 | 31 | const instance = tree.root; 32 | 33 | await wrapper.instance().componentDidMount(); 34 | 35 | const component = instance.findByProps({className: `notifications`}); 36 | const text = component.children[0]; 37 | 38 | expect(text).toEqual('42 Notifications'); 39 | }); 40 | }) 41 | -------------------------------------------------------------------------------- /src/sagas/fetch-question-saga.spec.js: -------------------------------------------------------------------------------- 1 | import { handleFetchQuestion } from './fetch-question-saga'; 2 | import fetch from 'isomorphic-fetch'; 3 | /** 4 | * This test is an example of two important Jest testing principles, 5 | * 1) we're mocking the "fetch" module, so that we don't actually make a request every time we run the test 6 | * The module, isomorphic fetch, is conveniently mocked automatically be including the file __mocks__/isomorphic-fetch.js adjacent to to the Node.js folder 7 | * 2) we're using an async function to automatically deal with the fact that our app isn't synchronous 8 | */ 9 | describe("Fetch questions saga",()=>{ 10 | beforeAll(()=>{ 11 | fetch.__setValue([{question_id:42}]); 12 | }); 13 | it("should get the questions from the correct endpoint in response to the appropriate action",async ()=>{ 14 | const gen = handleFetchQuestion({question_id:42}); 15 | /** 16 | * At this point, isomorphic fetch must have been mocked, 17 | * or an error will occur, or, worse, an unexpected side effect! 18 | */ 19 | const { value } = await gen.next(); 20 | expect(value).toEqual([{question_id:42}]); 21 | 22 | /** 23 | * We can also assert that fetch has been called with the values expected (note that we used a spy in the file where we mock fetch.) 24 | */ 25 | expect(fetch).toHaveBeenCalledWith(`/api/questions/42`); 26 | }); 27 | }); -------------------------------------------------------------------------------- /src/components/__tests__/NotificationsViewer.js: -------------------------------------------------------------------------------- 1 | import NotificationsViewer from '../NotificationsViewer' 2 | import renderer from 'react-test-renderer'; 3 | import React from 'react'; 4 | import delay from 'redux-saga'; 5 | 6 | /** 7 | * Locally created modules must be explicitly 8 | * mocked (unlike NPM modules, which need only that the mock file exists 9 | */ 10 | jest.mock('../../services/NotificationsService'); 11 | 12 | /** 13 | * This require statement now imports the mock service. We will use this reference to inject a return value. 14 | */ 15 | const notificationsService = require('../../services/NotificationsService').default; 16 | 17 | describe("The stateful notifications viewer",()=>{ 18 | 19 | beforeAll(()=>{ 20 | notificationsService.__setCount(5); 21 | }); 22 | 23 | it("Should display the correct number of notifications", async ()=>{ 24 | const tree = renderer 25 | .create( 26 | 27 | ); 28 | 29 | await delay(); 30 | const instance = tree.root; 31 | const component = instance.findByProps({className:`notifications`}); 32 | const text = component.children[0]; 33 | expect(text).toEqual("5 Notifications"); 34 | }); 35 | 36 | it("Should have a passing snapshot", async ()=>{ 37 | const tree = renderer.create(); 38 | expect(tree).toMatchSnapshot(); 39 | await delay(); 40 | expect(tree).toMatchSnapshot(); 41 | }) 42 | }); -------------------------------------------------------------------------------- /webpack.config.prod.babel.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | const webpack = require('webpack'); 4 | 5 | /** 6 | * Production Webpack Config bundles JS, then uglifies it and exports it to the "dist" directory 7 | * See Development webpack config for detailed comments 8 | */ 9 | 10 | module.exports = { 11 | mode: 'development', 12 | entry: [ 13 | 'babel-regenerator-runtime', 14 | path.resolve(__dirname, 'src') 15 | ], 16 | output: { 17 | path: path.resolve(__dirname, 'dist'), 18 | filename: 'bundle.js', 19 | publicPath: '/' 20 | }, 21 | optimization: { 22 | minimize: true, 23 | minimizer: [new TerserPlugin()], 24 | }, 25 | plugins: [ 26 | new webpack.DefinePlugin({ 27 | 'process.env': { 28 | NODE_ENV: JSON.stringify('production'), 29 | WEBPACK: true 30 | } 31 | }), 32 | /* 33 | * Uglifies JS which improves performance 34 | * React will throw console warnings if this is not implemented 35 | */ 36 | ], 37 | resolve: { 38 | extensions: ['.js', '.json', '.jsx'], 39 | }, 40 | module: { 41 | rules: [ 42 | { 43 | test: /.jsx?$/, 44 | use: { 45 | loader: 'babel-loader' 46 | }, 47 | include: path.resolve(__dirname, 'src') 48 | } 49 | ] 50 | } 51 | }; -------------------------------------------------------------------------------- /src/reducers/questions.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Placing every test inside a "describe" block is not a requirement but makes for a good, clean means of organization. 3 | */ 4 | import { questions } from './questions'; 5 | 6 | /** 7 | * Testing Reducers is the easiest part of a a React / Redux application. 8 | * Reducers are pure functions, so they never depend on dependencies that need to be mocked, and 9 | * can be relied upon to return the same data given similar arguments. 10 | */ 11 | describe("The questions reducer",()=>{ 12 | it ("Should return the same state on an inapplicable action",()=>{ 13 | const state = ["foo","bar"]; 14 | const newState = questions(state,{type:"UNDEFINED_ACTION"}); 15 | 16 | /** 17 | * In this case both toBe and toEqual will return true. However, the real test in this example is toBe, since we want literally the same object both times. 18 | * 19 | */ 20 | expect(newState).toBe(state); 21 | expect(newState).toEqual(state); 22 | }); 23 | 24 | it ("Should add a new questions to the list on a FETCHED_QUESTION action",()=>{ 25 | const state = [{question_id:"foo"},{question_id:"bar"}]; 26 | const newState = questions(state,{type:`FETCHED_QUESTION`,question:{question_id:"baz"}}); 27 | 28 | /** Here a map is used to allow a simple one-liner as an equality check. 29 | * TODO... is there a more applicable matcher available? 30 | */ 31 | expect(newState.map(q=>q.question_id)) 32 | .toEqual(["baz","foo","bar"]); 33 | }); 34 | }); -------------------------------------------------------------------------------- /src/components/__tests__/QuestionDetail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In this file we explore testing a React Redux component by writing seperate tests for the display and the container 3 | */ 4 | import { QuestionDetailDisplay , mapStateToProps } from '../QuestionDetail' 5 | import renderer from 'react-test-renderer'; 6 | import React from 'react'; 7 | 8 | describe("The question detail view",()=>{ 9 | describe("The display component",()=>{ 10 | it("Should not regress",()=>{ 11 | const tree = renderer 12 | .create( 13 | 19 | ); 20 | expect(tree.toJSON()).toMatchSnapshot(); 21 | 22 | }) ; 23 | }); 24 | 25 | /** 26 | * Note that since Map State to Props is a pure function, we can easily test it here 27 | * without needing to scaffold the entire application around it. 28 | */ 29 | describe("Map state to props",()=>{ 30 | const sampleQuestion = { 31 | question_id:42, 32 | body:"Space is big" 33 | }; 34 | 35 | const appState = { 36 | questions:[sampleQuestion] 37 | }; 38 | 39 | const ownProps = { 40 | question_id: 42 41 | }; 42 | 43 | const componentState = mapStateToProps(appState,ownProps); 44 | 45 | expect(componentState).toEqual(sampleQuestion); 46 | 47 | }); 48 | }); -------------------------------------------------------------------------------- /src/components/QuestionDetail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Markdown from 'react-markdown'; 3 | import TagsList from './TagsList' 4 | import { connect } from 'react-redux'; 5 | 6 | /** 7 | * Question Detail Display outputs a view containing question information when passed a question 8 | * as its prop 9 | * If no question is found, that means the saga that is loading it has not completed, and display an interim message 10 | */ 11 | export const QuestionDetailDisplay = ({title,body,answer_count,tags})=>( 12 |
13 |

14 | {title} 15 |

16 | {body ? 17 |
18 |
19 | 20 |
21 | 22 |
23 | {answer_count} Answers 24 |
25 |
: 26 |
27 | {/* If saga has not yet gotten question details, display loading message instead. */} 28 |

29 | Loading Question... 30 |

31 |
32 | } 33 |
34 | ); 35 | 36 | export const mapStateToProps = (state,ownProps)=>({ 37 | /** 38 | * Find the question in the state that matches the ID provided and pass it to the display component 39 | */ 40 | ...state.questions.find(({question_id})=>question_id == ownProps.question_id) 41 | }); 42 | 43 | /** 44 | * Create and export a connected component 45 | */ 46 | export default connect(mapStateToProps)(QuestionDetailDisplay); -------------------------------------------------------------------------------- /src/components/QuestionList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TagsList from './TagsList' 3 | import { connect } from 'react-redux'; 4 | import { 5 | Link 6 | } from 'react-router-dom' 7 | 8 | /** 9 | * Each entry in the QuestionList is represtented by a QuestionListItem, which displays high-level information 10 | * about a question in a format that works well in a list 11 | */ 12 | export const QuestionListItem = ({tags,answer_count,title,views,question_id})=>( 13 |
14 |

15 | {title} 16 |

17 |
18 | 19 |
20 |
21 | 22 | 23 | 24 |
25 |
); 26 | 27 | /** 28 | * Display all questions in an array provided to it as a simple list 29 | */ 30 | export const QuestionList = ({questions})=>( 31 |
32 | { questions ? 33 |
34 | {questions.map(question=>)} 35 |
: 36 |
37 | Loading questions... 38 |
39 | } 40 |
41 | ); 42 | 43 | /** 44 | * Get the list of questions from the application's state 45 | * It is populated by a ../sagas/fetch-question(s)-saga. 46 | */ 47 | const mapStateToProps = ({questions})=>({ 48 | questions 49 | }); 50 | 51 | /** 52 | * Create and export a connected component 53 | */ 54 | export default connect(mapStateToProps)(QuestionList); -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import QuestionList from './components/QuestionList' 4 | import QuestionDetail from './components/QuestionDetail' 5 | import NotificationsViewer from './components/NotificationsViewer' 6 | 7 | import { connect } from 'react-redux'; 8 | import { Route, Link } from 'react-router-dom'; 9 | 10 | /** 11 | * App Component is the highest level real component in the application, it is the parent of the routes and an 12 | * an ancestors of all other compoents 13 | */ 14 | const AppDisplay = ()=>( 15 |
16 |
17 | 18 |

Isomorphic React

19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 | 27 | {/*Specify a route for the main page which renders when the path is empty*/} 28 | }/> 29 | 30 | {/*Specify a route for questions where the detail renders differently depending on the question selected, the ID of which is passed in at render time*/} 31 | {/*It would be possible to read the current path from within the component during rendering, but this way all data is passed in through props.*/} 32 | }/> 33 |
34 | ); 35 | 36 | const mapStateToProps = (state,ownProps)=>({ 37 | ...state, 38 | }); 39 | 40 | /** 41 | * The connected component exported below forms the 42 | * core of our application and is used both on the server and the client 43 | */ 44 | export default connect(mapStateToProps)(AppDisplay); 45 | -------------------------------------------------------------------------------- /webpack.config.dev.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 3 | import path from 'path'; 4 | 5 | /** 6 | * Development webpack config designed to be loaded by express development server 7 | */ 8 | 9 | module.exports = { 10 | /** 11 | * The scripts in entry are combined in order to create our bundle 12 | */ 13 | mode: 'development', 14 | entry: [ 15 | 'babel-regenerator-runtime', 16 | path.resolve(__dirname, 'src') 17 | ], 18 | output: { 19 | path: path.resolve(__dirname, 'dist'), 20 | filename: 'bundle.js', 21 | publicPath: '/' 22 | }, 23 | optimization: { 24 | minimizer: [ 25 | // we specify a custom UglifyJsPlugin here to get source maps in production 26 | new UglifyJsPlugin({ 27 | cache: true, 28 | parallel: true, 29 | uglifyOptions: { 30 | compress: false, 31 | ecma: 6, 32 | mangle: true 33 | }, 34 | sourceMap: true 35 | }) 36 | ] 37 | }, 38 | plugins: [ 39 | new webpack.DefinePlugin({ 40 | 'process.env': { 41 | NODE_ENV: JSON.stringify('production'), 42 | WEBPACK: true 43 | } 44 | }), 45 | /* 46 | * Uglifies JS which improves performance 47 | * React will throw console warnings if this is not implemented 48 | */ 49 | ], 50 | resolve: { 51 | extensions: ['.js', '.json', '.jsx'], 52 | }, 53 | module: { 54 | rules: [ 55 | { 56 | test: /.jsx?$/, 57 | use: { 58 | loader: 'babel-loader' 59 | }, 60 | include: path.resolve(__dirname, 'src') 61 | } 62 | ] 63 | } 64 | }; -------------------------------------------------------------------------------- /src/components/QuestionList.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In this file we explore testing a React-Redux component by wrapping it in a MemoryRouter and checking the results 3 | */ 4 | 5 | import React from 'react'; 6 | import { QuestionListItem } from './QuestionList' 7 | import renderer from 'react-test-renderer'; 8 | import { MemoryRouter } from 'react-router'; 9 | 10 | 11 | /** 12 | * Here we test a React-Redux component. The key takeaway is that it is easier to test the decoupled "display" 13 | component and consider the "connect" component as an implementation detail. 14 | */ 15 | describe("The question list",()=>{ 16 | describe("The items of the question list",()=>{ 17 | it ("should display a list of items",()=>{ 18 | /** 19 | * Here we can run into a problem - since the component contains a link, it will throw an error by default unless 20 | * this component is nested in a router. 21 | * As a solution, the React Router GitHub page suggest wrapping the component in a memory router. 22 | * https://github.com/ReactTraining/react-router/issues/4795 23 | * This actually makes sense since any automatic solution would be too unpredictable. 24 | */ 25 | const tree = renderer 26 | .create( 27 | 28 | 33 | 34 | ); 35 | 36 | /** 37 | * Here we are using the basic functionality of the React Test Renderer to run assertions against 38 | * the rendered HTML (and thus avoiding additional libraries such as Enzyme) 39 | */ 40 | expect(tree.root.findByType("h3").children[0]).toEqual("The meaning of life"); 41 | expect(tree.toJSON()).toMatchSnapshot(); 42 | }); 43 | 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/NotificationsViewer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | /* 3 | 4 | import NotificationsService from '../services/NotificationsService'; 5 | 6 | export default class NotificationsViewer extends React.Component { 7 | constructor(...args) { 8 | super(...args); 9 | 10 | this.state = { 11 | count: -1 12 | } 13 | } 14 | 15 | async componentDidMount () { 16 | let { count } = await NotificationsService.GetNotifications(); 17 | console.log('componentDidMount count:', count); 18 | 19 | this.setState({ 20 | count 21 | }); 22 | } 23 | 24 | componentDidUpdate() { 25 | console.log('componentDidUpdate count:', this.state.count); 26 | } 27 | 28 | render() { 29 | return ( 30 |
31 |
32 | {this.state.count != -1 ? `${this.state.count} Notifications Awaiting` : `Loading...`} 33 |
34 |
35 | ) 36 | } 37 | } 38 | */ 39 | import NotificationsService from '../services/NotificationsService' 40 | 41 | /** 42 | * The following demo class was written in a stateful manner, using the `extends` syntax, for the purpose 43 | * of demonstrating how to test such a class. It is not functionally complete and not universal. 44 | * The structure of this component is NOT recommended, please use React-Redux instead. 45 | */ 46 | 47 | export default class extends React.Component { 48 | constructor(...args) { 49 | super(...args); 50 | this.state = { 51 | count: -1, 52 | } 53 | } 54 | async componentDidMount() { 55 | let {count} = await NotificationsService.getNotifications(); 56 | this.setState({ 57 | count, 58 | }); 59 | this.state.count = count; 60 | } 61 | render() { 62 | return ( 63 |
64 |
65 | {this.state.count != -1 ? `${this.state.count} Notifications` : `Loading...`} 66 |
67 |
68 | 69 | ) 70 | } 71 | } -------------------------------------------------------------------------------- /src/getStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers,applyMiddleware } from 'redux' 2 | import { routerReducer as router, routerMiddleware } from 'react-router-redux' 3 | import { createLogger } from 'redux-logger'; 4 | import createSagaMiddleware from 'redux-saga'; 5 | import fetchQuestionSaga from './sagas/fetch-question-saga' 6 | import fetchQuestionsSaga from './sagas/fetch-questions-saga' 7 | import * as reducers from './reducers' 8 | 9 | /** 10 | * Get store creates a new instance of the store configurable for use 11 | * on the client (for a living app) and the server (for pre-rendered HTML) 12 | * @param history 13 | * A history component. Should be browserHistory for client, and memoryHistroy for server. 14 | * @param defaultState 15 | * The default state of the application. Since this is used by React Router, this can affect 16 | * the application's initial render 17 | */ 18 | export default function(history,defaultState = {}){ 19 | /** 20 | * Create middleware for React-router and pass in history 21 | */ 22 | const middleware = routerMiddleware(history); 23 | 24 | /** 25 | * Create saga middleware to run our sagas 26 | */ 27 | const sagaMiddleware = createSagaMiddleware(); 28 | 29 | /** 30 | * Create a logger to provide insights to the application's state from the developer window 31 | * You are encouraged to remove this for production. 32 | */ 33 | 34 | 35 | const middlewareChain = [middleware, sagaMiddleware]; 36 | if(process.env.NODE_ENV === 'development') { 37 | const logger = createLogger(); 38 | middlewareChain.push(logger); 39 | } 40 | 41 | /** 42 | * Create a store with the above middlewares, as well as an object containing reducers 43 | */ 44 | const store = createStore(combineReducers({ 45 | ...reducers, 46 | router 47 | }), defaultState,applyMiddleware(...middlewareChain)); 48 | 49 | /** 50 | * Run the sagas which will in turn wait for the appropriate action type before making requests 51 | */ 52 | sagaMiddleware.run(fetchQuestionSaga); 53 | sagaMiddleware.run(fetchQuestionsSaga); 54 | 55 | /** 56 | * Return the store to the caller for application initialization 57 | */ 58 | return store; 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isomorphic-react", 3 | "version": "1.0.0", 4 | "description": "An isomorphic React application implementing latest development standards", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "cross-env webpack --config ./webpack.config.prod.babel.js", 8 | "postinstall": "npm install -g babel-cli@7.0.0-beta.3 cross-env@5.0.5 webpack@3.6.0", 9 | "start": "npm run start-prod", 10 | "start-dev": "cross-env NODE_ENV=development babel-node server --useServerRender=true --useLiveData=false", 11 | "start-prod": "npm run build && cross-env NODE_ENV=production babel-node server --useServerRender=true --useLiveData=false", 12 | "test": "jest", 13 | "test-dev": "jest --watch", 14 | "test-update-snapshots": "jest -u" 15 | }, 16 | "keywords": [], 17 | "author": "Daniel Stern (Code Whisperer)", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@babel/cli": "^7.0.0", 21 | "@babel/core": "^7.0.0", 22 | "@babel/node": "^7.2.2", 23 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 24 | "@babel/plugin-transform-regenerator": "^7.0.0", 25 | "@babel/preset-env": "^7.0.0", 26 | "@babel/preset-react": "^7.0.0", 27 | "babel-loader": "^8.0.0", 28 | "babel-preset-react-hmre": "^1.1.1", 29 | "babel-regenerator-runtime": "^6.5.0", 30 | "copy-webpack-plugin": "^7.0.0", 31 | "cross-env": "^5.0.5", 32 | "enzyme": "^3.10.0", 33 | "enzyme-adapter-react-16": "^1.14.0", 34 | "express": "^4.16.1", 35 | "express-yields": "^1.1.1", 36 | "fs-extra": "^4.0.2", 37 | "history": "^4.7.2", 38 | "isomorphic-fetch": "^3.0.0", 39 | "jest": "^26.6.3", 40 | "lodash": "^4.17.20", 41 | "optimist": "^0.6.1", 42 | "react": "^16.0.0", 43 | "react-dom": "^16.0.0", 44 | "react-hot-loader": "^3.0.0-beta.7", 45 | "react-markdown": "^2.5.0", 46 | "react-redux": "^5.0.6", 47 | "react-router-dom": "^4.2.2", 48 | "react-router-redux": "^5.0.0-alpha.6", 49 | "react-router-server": "^4.2.2", 50 | "react-test-renderer": "^16.8.6", 51 | "redux": "^3.7.2", 52 | "redux-logger": "^3.0.6", 53 | "redux-saga": "^0.15.6", 54 | "request": "^2.83.0", 55 | "request-promise": "^4.2.2", 56 | "terser-webpack-plugin": "^5.1.1", 57 | "webpack": "^5.21.2", 58 | "webpack-dev-middleware": "^1.12.0", 59 | "webpack-hot-middleware": "^2.19.1" 60 | }, 61 | "jest": { 62 | "verbose": true, 63 | "testURL": "http://localhost/" 64 | }, 65 | "devDependencies": { 66 | "webpack-cli": "^3.3.11" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the main entry point of the client application and is loaded by Webpack. 3 | * It is NOT loaded by the server at any time as the configurations used (i.e.,browserHistory) only work in the client context. 4 | * The server may load the App component when server rendering. 5 | */ 6 | import App from './App' 7 | import ReactDOM from 'react-dom' 8 | import React from 'react'; 9 | import { Provider } from 'react-redux'; 10 | import { ConnectedRouter } from 'react-router-redux' 11 | import getStore from './getStore'; 12 | import createHistory from 'history/createBrowserHistory'; 13 | 14 | const history = createHistory(); 15 | const store = getStore(history); 16 | 17 | if (module.hot) { 18 | /** 19 | * If using hot module reloading, watch for any changes to App or its descendent modules. 20 | * Then, reload the application without restarting or changing the reducers, storeo r state. 21 | */ 22 | module.hot.accept('./App', () => { 23 | const NextApp = require('./App').default; 24 | render(NextApp); 25 | }); 26 | } 27 | 28 | /** 29 | * Render the app, 30 | * encapsulated inside a router, which will automatically let Route tags render based on the Redux store, 31 | * which itself is encapsulated inside a provider, which gives child connected components access to the Redux store 32 | */ 33 | const render = (_App)=>{ 34 | ReactDOM.render( 35 | 36 | 37 | <_App /> 38 | 39 | 40 | ,document.getElementById("AppContainer")); 41 | }; 42 | 43 | /** 44 | * Listen for changes to the store 45 | */ 46 | store.subscribe(()=>{ 47 | const state = store.getState(); 48 | /** 49 | * When the questions array is populated, that means the saga round trip has completed, 50 | * and the application can be rendered. 51 | * Rendering before the questions arrived would result in the server-generated content being replaced with 52 | * a blank page. 53 | */ 54 | if (state.questions.length > 0) { 55 | render(App); 56 | } 57 | }); 58 | 59 | /** 60 | * Reads the current path, which corresponds to the route the user is seeing, and makes a request 61 | * the the appropriate saga to fetch any data that might be required. 62 | * @param location 63 | * The current URL that is loaded 64 | */ 65 | const fetchDataForLocation = location=>{ 66 | /** 67 | * If the location is the standard route, fetch an undetailed list of all questions 68 | **/ 69 | if (location.pathname === "/"){ 70 | store.dispatch({type:`REQUEST_FETCH_QUESTIONS`}) 71 | } 72 | 73 | /** 74 | * If the location is the details route, fetch details for one question 75 | */ 76 | if (location.pathname.includes(`questions`)) { 77 | store.dispatch({type:`REQUEST_FETCH_QUESTION`,question_id:location.pathname.split('/')[2]}); 78 | } 79 | }; 80 | /** 81 | * Initialize data fetching procedure 82 | */ 83 | fetchDataForLocation(history.location); 84 | 85 | /** 86 | * Listen to changes in path, and trigger 87 | * data fetching procedure on any relevant changes 88 | */ 89 | history.listen(fetchDataForLocation); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Isomorphic React 2 | 3 | # Updated 2021 Version 4 | - As of February 2021, the Jest Branch is now merged into the main branch of this project 5 | 6 | ## A Starter Isomorphic React Application with All Best Practices and No Frills 7 | ![image](https://user-images.githubusercontent.com/4268152/31387801-c091f5c8-ad99-11e7-9cb6-42fcde98fc88.png) 8 | ### About The Application 9 | This application is a basic API client which gathers data from an outside API (in this case, Stackoverflow) and generates an isomorphic, single-page application (SPA). 10 | 11 | ### Why Isomorphic React? 12 | Great question! 13 | - Uses React / Redux as main application engine 14 | - Supports hot reloading and server rendering! 15 | - Uses React Router (in a combination with server rendering that is truly amazing) 16 | - No fluff, just the good stuff 17 | 18 | ### Getting Started 19 | 1) Clone the repository 20 | 2) install dependencies 21 | `npm install && npm run postinstall` 22 | 3) Run the dev server 23 | `npm run start-dev` 24 | 4) Navigate to the application's url 25 | `http://localhost:3000/` 26 | 27 | ### Usage 28 | #### Enabling / Disabling Server Rendering 29 | Server rendering is great, but sometimes we want to disable it when there's an error in our render and we'd rather troubleshoot it in the client. 30 | This setting is passed in as a CLI argument via the `--useServerRender=true` argument. 31 | You can modify this in `package.json` to `--useServerRender=false` which will disable any server-side rendering functionality. 32 | 33 | #### Enabling / Disabling Live Data 34 | This application is designed to grab the latest data from `Stackoverflow.com`. However, their API has a strict request limit which means that no questions will be returned after X requests (usually 300). 35 | Therefore, the application comes loaded with mock-questions in the data directory. 36 | To ease the learning process by eliminating potential sources of error, live data is disabled by default. 37 | However, you are strongly encouraged to use live data once you understand the associated pitfalls. 38 | * Note: You can increase your allotted requests to a much larger number by registering an application here, 39 | `https://stackapps.com/apps/oauth/register` and then appending the key to the URLs in `data/api-real-url.js` 40 | 41 | ### Production Build 42 | This application fully supports a production build setting, which disables live reloading in favor of precompiled and uglified JS, which boosts performance. 43 | To run production, run the command `npm run start-prod`, which automatically triggers the `build` script. 44 | This mode is recommended for production. However, this boilerplate has never been used in actual production so utilize caution if deploying as a real application. 45 | 46 | ### Troubleshooting 47 | #### `unexpected token import` 48 | This error appears when babel is not configured correctly. This can actually be caused by outdated global dependencies, and is hard to fix. For best results, try the following - 49 | - Install `babel-register` as a local saved dependency 50 | - Update global versions of `babel`, `webpack` and all dependencies to latest / course versions 51 | 52 | #### Any Error That is Taking a Long Time to Troubleshoot 53 | Things can always go wrong in the world of programming. If this happens, clone the master branch of this repo to a new directory and run the installation instructions. If desired, you can work backwards, pruning extra files until you get the application in the state you want. 54 | 55 | #### Problems with the Repo 56 | I want this repo to work perfectly for as many users as possible. Got a problem? Open an issue! Let's figure out a solution together. 57 | 58 | -------------------------------------------------------------------------------- /server/index.jsx: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import express from 'express'; 3 | import webpack from 'webpack'; 4 | import yields from 'express-yields'; 5 | import fs from 'fs-extra'; 6 | import App from '../src/App'; 7 | import { delay } from 'redux-saga'; 8 | import { renderToString } from 'react-dom/server' 9 | import React from 'react' 10 | import { argv } from 'optimist'; 11 | import { questions, question } from '../data/api-real-url'; 12 | import { get } from 'request-promise'; 13 | import { ConnectedRouter } from 'react-router-redux'; 14 | import getStore from '../src/getStore' 15 | import { Provider } from 'react-redux'; 16 | import createHistory from 'history/createMemoryHistory'; 17 | 18 | /** 19 | * Try and find a specific port as provided by an external cloud host, or go with a default value 20 | */ 21 | const port = process.env.PORT || 3000; 22 | const app = express(); 23 | 24 | /** 25 | * Get basic configuration settings from arguments 26 | * This can be replaced with webpack configuration or other global variables as required 27 | * When useServerRender is true, the application will be pre-rendered on the server. Otherwise, 28 | * just the normal HTML page will load and the app will bootstrap after it has made the required AJAX calls 29 | */ 30 | const useServerRender = argv.useServerRender === 'true'; 31 | 32 | /** 33 | * When useLiveData is true, the application attempts to contact Stackoverflow and interact with its actual API. 34 | * NOTE: Without an API key, the server will cut you off after 300 requests. To solve this, get an API key from 35 | * Stackoverflow (for free at https://stackapps.com/apps/oauth/register) 36 | * OR, just disable useLiveData 37 | */ 38 | const useLiveData = argv.useLiveData === 'true'; 39 | 40 | /** 41 | * The block below will run during development and facilitates live-reloading 42 | * If the process is development, set up the full live reload server 43 | */ 44 | if(process.env.NODE_ENV === 'development') { 45 | /** 46 | * Get the development configuration from webpack.config. 47 | */ 48 | const config = require('../webpack.config.dev.babel.js'); 49 | 50 | /** 51 | * Create a webpack compiler which will output our bundle.js based on the application's code 52 | */ 53 | const compiler = webpack(config); 54 | 55 | /** 56 | * Use webpack-dev-middleware, which facilitates creating a bundle.js in memory and updating it automatically 57 | * based on changed files 58 | */ 59 | app.use(require('webpack-dev-middleware')(compiler,{ 60 | /** 61 | * @noInfo Only display warnings and errors to the concsole 62 | */ 63 | noInfo: true, 64 | stats: { 65 | assets: false, 66 | colors: true, 67 | version: false, 68 | hash: false, 69 | timings: false, 70 | chunks: false, 71 | chunkModules: false 72 | } 73 | })); 74 | 75 | /** 76 | * Hot middleware allows the page to reload automatically while we are working on it. 77 | * Can be used instead of react-hot-middleware if Redux is being used to manage app state 78 | */ 79 | app.use(require('webpack-hot-middleware')(compiler)); 80 | } else { 81 | /** 82 | * If the process is production, just serve the file from the dist folder 83 | * Build should have been run beforehand 84 | */ 85 | app.use(express.static(path.resolve(__dirname, '../dist'))); 86 | } 87 | 88 | /** 89 | * Returns a response object with an [items] property containing a list of the 30 or so newest questions 90 | */ 91 | function * getQuestions (){ 92 | let data; 93 | if (useLiveData) { 94 | /** 95 | * If live data is used, contact the external API 96 | */ 97 | data = yield get(questions,{gzip:true}); 98 | } else { 99 | /** 100 | * If live data is not used, read the mock questions file 101 | */ 102 | data = yield fs.readFile('./data/mock-questions.json',"utf-8"); 103 | } 104 | 105 | /** 106 | * Parse the data and return it 107 | */ 108 | return JSON.parse(data); 109 | } 110 | 111 | function * getQuestion (question_id) { 112 | let data; 113 | if (useLiveData) { 114 | /** 115 | * If live data is used, contact the external API 116 | */ 117 | data = yield get(question(question_id),{gzip:true,json:true}); 118 | } else { 119 | /** 120 | * If live data is not used, get the list of mock questions and return the one that 121 | * matched the provided ID 122 | */ 123 | const questions = yield getQuestions(); 124 | const question = questions.items.find(_question=>_question.question_id == question_id); 125 | /** 126 | * Create a mock body for the question 127 | */ 128 | question.body = `Mock question body: ${question_id}`; 129 | data = {items:[question]}; 130 | } 131 | return data; 132 | } 133 | 134 | /** 135 | * Creates an api route localhost:3000/api/questions, which returns a list of questions 136 | * using the getQuestions utility 137 | */ 138 | app.get('/api/questions',function *(req,res){ 139 | const data = yield getQuestions(); 140 | /** 141 | * Insert a small delay here so that the async/hot-reloading aspects of the application are 142 | * more obvious. You are strongly encouraged to remove the delay for production. 143 | */ 144 | yield delay(150); 145 | res.json(data); 146 | }); 147 | 148 | /** 149 | * Special route for returning detailed information on a single question 150 | */ 151 | app.get('/api/questions/:id',function *(req,res){ 152 | const data = yield getQuestion(req.params.id); 153 | /** 154 | * Remove this delay for production. 155 | */ 156 | yield delay(150); 157 | res.json(data); 158 | }); 159 | 160 | /** 161 | * Create a route that triggers only when one of the two view URLS are accessed 162 | */ 163 | app.get(['/','/questions/:id'], function *(req,res){ 164 | /** 165 | * Read the raw index HTML file 166 | */ 167 | let index = yield fs.readFile('./public/index.html',"utf-8"); 168 | 169 | /** 170 | * Create a memoryHistory, which can be 171 | * used to pre-configure our Redux state and routes 172 | */ 173 | const history = createHistory({ 174 | /** 175 | * By setting initialEntries to the current path, the application will correctly render into the 176 | * right view when server rendering 177 | */ 178 | initialEntries: [req.path], 179 | }); 180 | 181 | /** 182 | * Create a default initial state which will be populated based on the route 183 | */ 184 | const initialState = { 185 | questions:[] 186 | }; 187 | 188 | /** 189 | * Check to see if the route accessed is the "question detail" route 190 | */ 191 | if (req.params.id) { 192 | /** 193 | * If there is req.params.id, this must be the question detail route. 194 | * You are encouraged to create more robust conditions if you add more routes 195 | */ 196 | const question_id = req.params.id; 197 | /** 198 | * Get the question that corresponds to the request, and preload the initial state with it 199 | */ 200 | const response = yield getQuestion(question_id); 201 | const questionDetails = response.items[0]; 202 | initialState.questions = [{...questionDetails,question_id}]; 203 | } else { 204 | /** 205 | * Otherwise, we are on the "new questions view", so preload the state with all the new questions (not including their bodies or answers) 206 | */ 207 | const questions = yield getQuestions(); 208 | initialState.questions = [...questions.items] 209 | } 210 | 211 | /** 212 | * Create a redux store that will be used only for server-rendering our application (the client will use a different store) 213 | */ 214 | const store = getStore(history,initialState); 215 | 216 | /** 217 | * If server render is used, replace the specified block in index with the application's rendered HTML 218 | */ 219 | if (useServerRender) { 220 | const appRendered = renderToString( 221 | /** 222 | * Surround the application in a provider with a store populated with our initialState and memoryHistory 223 | */ 224 | 225 | 226 | 227 | 228 | 229 | ); 230 | index = index.replace(`<%= preloadedApplication %>`,appRendered) 231 | } else { 232 | /** 233 | * If server render is not used, just output a loading message, and the application will appear 234 | * when React boostraps on the client side. 235 | */ 236 | index = index.replace(`<%= preloadedApplication %>`,`Please wait while we load the application.`); 237 | } 238 | res.send(index); 239 | }); 240 | 241 | /** 242 | * Listen on the specified port for requests to serve the application 243 | */ 244 | app.listen(port, '0.0.0.0', ()=>console.info(`Listening at http://localhost:${port}`)); 245 | -------------------------------------------------------------------------------- /data/mock-questions.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "tags": [ 5 | "javascript", 6 | "php", 7 | "jquery", 8 | "html", 9 | "css" 10 | ], 11 | "owner": { 12 | "reputation": 1, 13 | "user_id": 8721033, 14 | "user_type": "registered", 15 | "profile_image": "https://www.gravatar.com/avatar/93cfa7973b584f2c9965e11543eb9cdb?s=128&d=identicon&r=PG&f=1", 16 | "display_name": "Revati", 17 | "link": "https://stackoverflow.com/users/8721033/revati" 18 | }, 19 | "is_answered": false, 20 | "view_count": 15, 21 | "answer_count": 2, 22 | "score": 0, 23 | "last_activity_date": 1507130830, 24 | "creation_date": 1507130641, 25 | "question_id": 46568554, 26 | "link": "https://stackoverflow.com/questions/46568554/show-div-on-button-click-but-dynamically", 27 | "title": "show div on button click but dynamically" 28 | }, 29 | { 30 | "tags": [ 31 | "ruby-on-rails", 32 | "upload", 33 | "carrierwave", 34 | "simple-form", 35 | "cloudinary" 36 | ], 37 | "owner": { 38 | "reputation": 6, 39 | "user_id": 7924124, 40 | "user_type": "registered", 41 | "accept_rate": 0, 42 | "profile_image": "https://i.stack.imgur.com/xoUo8.jpg?s=128&g=1", 43 | "display_name": "Tana", 44 | "link": "https://stackoverflow.com/users/7924124/tana" 45 | }, 46 | "is_answered": false, 47 | "view_count": 1, 48 | "answer_count": 0, 49 | "score": 0, 50 | "last_activity_date": 1507130828, 51 | "creation_date": 1507130828, 52 | "question_id": 46568618, 53 | "link": "https://stackoverflow.com/questions/46568618/preview-carrierwave-upload-when-editing-the-photo-before-submit", 54 | "title": "preview carrierwave upload when editing the photo before submit" 55 | }, 56 | { 57 | "tags": [ 58 | "angular", 59 | "angularjs-material" 60 | ], 61 | "owner": { 62 | "reputation": 1, 63 | "user_id": 6473639, 64 | "user_type": "registered", 65 | "profile_image": "https://lh3.googleusercontent.com/-PmFLItBSzrQ/AAAAAAAAAAI/AAAAAAAAAA4/q0iY3Pr3B9k/photo.jpg?sz=128", 66 | "display_name": "mohammadali ghanbari", 67 | "link": "https://stackoverflow.com/users/6473639/mohammadali-ghanbari" 68 | }, 69 | "is_answered": false, 70 | "view_count": 14, 71 | "answer_count": 0, 72 | "score": 0, 73 | "last_activity_date": 1507130828, 74 | "creation_date": 1507018689, 75 | "last_edit_date": 1507130828, 76 | "question_id": 46540098, 77 | "link": "https://stackoverflow.com/questions/46540098/bootstrapping-app-manually-after-getting-some-data", 78 | "title": "Bootstrapping app manually after getting some data" 79 | }, 80 | { 81 | "tags": [ 82 | "c#", 83 | "wpf", 84 | "xaml" 85 | ], 86 | "owner": { 87 | "reputation": 5, 88 | "user_id": 6501892, 89 | "user_type": "registered", 90 | "accept_rate": 25, 91 | "profile_image": "https://www.gravatar.com/avatar/5462dcdd16cfa5884455aed305fc268e?s=128&d=identicon&r=PG&f=1", 92 | "display_name": "Arrayoob", 93 | "link": "https://stackoverflow.com/users/6501892/arrayoob" 94 | }, 95 | "is_answered": true, 96 | "view_count": 25, 97 | "accepted_answer_id": 46567345, 98 | "answer_count": 2, 99 | "score": 0, 100 | "last_activity_date": 1507130828, 101 | "creation_date": 1507126052, 102 | "last_edit_date": 1507130828, 103 | "question_id": 46567001, 104 | "link": "https://stackoverflow.com/questions/46567001/c-wpf-loading-images-from-resources", 105 | "title": "C# WPF loading images from Resources" 106 | }, 107 | { 108 | "tags": [ 109 | "r", 110 | "shiny" 111 | ], 112 | "owner": { 113 | "reputation": 6, 114 | "user_id": 7380707, 115 | "user_type": "registered", 116 | "profile_image": "https://www.gravatar.com/avatar/07ec6ff9b846c71e9197f273ab2eafb4?s=128&d=identicon&r=PG&f=1", 117 | "display_name": "MC1277", 118 | "link": "https://stackoverflow.com/users/7380707/mc1277" 119 | }, 120 | "is_answered": false, 121 | "view_count": 7, 122 | "answer_count": 0, 123 | "score": 1, 124 | "last_activity_date": 1507130827, 125 | "creation_date": 1507130101, 126 | "last_edit_date": 1507130827, 127 | "question_id": 46568394, 128 | "link": "https://stackoverflow.com/questions/46568394/how-to-loop-through-multiple-upload-widegets-in-shiny", 129 | "title": "How to loop through multiple upload widegets in shiny?" 130 | }, 131 | { 132 | "tags": [ 133 | "c++", 134 | "qt", 135 | "qml" 136 | ], 137 | "owner": { 138 | "reputation": 32, 139 | "user_id": 8258001, 140 | "user_type": "registered", 141 | "accept_rate": 57, 142 | "profile_image": "https://www.gravatar.com/avatar/a0d8237cca328f3028ac9030020505d0?s=128&d=identicon&r=PG&f=1", 143 | "display_name": "OTmn", 144 | "link": "https://stackoverflow.com/users/8258001/otmn" 145 | }, 146 | "is_answered": false, 147 | "view_count": 8, 148 | "answer_count": 0, 149 | "score": 1, 150 | "last_activity_date": 1507130827, 151 | "creation_date": 1507130092, 152 | "last_edit_date": 1507130827, 153 | "question_id": 46568387, 154 | "link": "https://stackoverflow.com/questions/46568387/managing-graphical-user-interface-with-qml-in-c-app", 155 | "title": "Managing graphical user interface with qml in c++ app" 156 | }, 157 | { 158 | "tags": [ 159 | "javascript", 160 | "node.js", 161 | "express" 162 | ], 163 | "owner": { 164 | "reputation": 1458, 165 | "user_id": 2293679, 166 | "user_type": "registered", 167 | "accept_rate": 83, 168 | "profile_image": "https://i.stack.imgur.com/Kr5s1.png?s=128&g=1", 169 | "display_name": "Þaw", 170 | "link": "https://stackoverflow.com/users/2293679/%c3%9eaw" 171 | }, 172 | "is_answered": false, 173 | "view_count": 2, 174 | "answer_count": 0, 175 | "score": 0, 176 | "last_activity_date": 1507130826, 177 | "creation_date": 1507130826, 178 | "question_id": 46568617, 179 | "link": "https://stackoverflow.com/questions/46568617/is-is-okay-to-include-my-connection-in-all-request-in-nodejs", 180 | "title": "Is is okay to include my connection in all request in NodeJS?" 181 | }, 182 | { 183 | "tags": [ 184 | "android", 185 | "android-viewpager" 186 | ], 187 | "owner": { 188 | "reputation": 13, 189 | "user_id": 2765734, 190 | "user_type": "registered", 191 | "profile_image": "https://www.gravatar.com/avatar/7839903308fcb598023a13e53cda4805?s=128&d=identicon&r=PG&f=1", 192 | "display_name": "user2765734", 193 | "link": "https://stackoverflow.com/users/2765734/user2765734" 194 | }, 195 | "is_answered": true, 196 | "view_count": 2278, 197 | "answer_count": 1, 198 | "score": 0, 199 | "last_activity_date": 1507130824, 200 | "creation_date": 1427672892, 201 | "question_id": 29336352, 202 | "link": "https://stackoverflow.com/questions/29336352/viewpager-for-tutorial-when-app-first-starts-only", 203 | "title": "ViewPager for tutorial when app first starts only" 204 | }, 205 | { 206 | "tags": [ 207 | "php", 208 | "mysql", 209 | "xampp" 210 | ], 211 | "owner": { 212 | "reputation": 1, 213 | "user_id": 7817919, 214 | "user_type": "registered", 215 | "profile_image": "https://www.gravatar.com/avatar/70c183a29e32ba03ae07b965a5ecc111?s=128&d=identicon&r=PG&f=1", 216 | "display_name": "Jaymar Enconado", 217 | "link": "https://stackoverflow.com/users/7817919/jaymar-enconado" 218 | }, 219 | "is_answered": false, 220 | "view_count": 2, 221 | "answer_count": 0, 222 | "score": 0, 223 | "last_activity_date": 1507130824, 224 | "creation_date": 1507130824, 225 | "question_id": 46568616, 226 | "link": "https://stackoverflow.com/questions/46568616/what-happen-to-phpmyadmin", 227 | "title": "What happen to phpmyadmin" 228 | }, 229 | { 230 | "tags": [ 231 | "sql", 232 | "database" 233 | ], 234 | "owner": { 235 | "reputation": 1, 236 | "user_id": 3830725, 237 | "user_type": "registered", 238 | "profile_image": "https://www.gravatar.com/avatar/dd0c1dfc1d9f79ea57528374943b94fe?s=128&d=identicon&r=PG&f=1", 239 | "display_name": "user3830725", 240 | "link": "https://stackoverflow.com/users/3830725/user3830725" 241 | }, 242 | "is_answered": true, 243 | "view_count": 13, 244 | "answer_count": 1, 245 | "score": 0, 246 | "last_activity_date": 1507130824, 247 | "creation_date": 1507129683, 248 | "last_edit_date": 1507130824, 249 | "question_id": 46568244, 250 | "link": "https://stackoverflow.com/questions/46568244/database-sql-query-to-get-the-values-based-on-the-minimum-date-of-end-of-month-d", 251 | "title": "Database sql query to get the values based on the minimum date of end of month data" 252 | }, 253 | { 254 | "tags": [ 255 | "c#", 256 | ".net", 257 | "winforms" 258 | ], 259 | "owner": { 260 | "reputation": 39, 261 | "user_id": 7281886, 262 | "user_type": "registered", 263 | "accept_rate": 25, 264 | "profile_image": "https://www.gravatar.com/avatar/31d73d44425ba14b30e46e1b4125b3f9?s=128&d=identicon&r=PG&f=1", 265 | "display_name": "Ricky L.", 266 | "link": "https://stackoverflow.com/users/7281886/ricky-l" 267 | }, 268 | "is_answered": false, 269 | "view_count": 13, 270 | "answer_count": 2, 271 | "score": 0, 272 | "last_activity_date": 1507130823, 273 | "creation_date": 1507130569, 274 | "question_id": 46568531, 275 | "link": "https://stackoverflow.com/questions/46568531/why-is-my-label-ignoring-new-lines-n-coming-from-the-app-config-file", 276 | "title": "Why is my Label ignoring new lines ('') coming from the App.config file?" 277 | }, 278 | { 279 | "tags": [ 280 | "javascript", 281 | "object", 282 | "group-by" 283 | ], 284 | "owner": { 285 | "reputation": 1, 286 | "user_id": 8720127, 287 | "user_type": "registered", 288 | "profile_image": "https://www.gravatar.com/avatar/31bde0775d972627f7d6a1c6a6ccb734?s=128&d=identicon&r=PG&f=1", 289 | "display_name": "Otti", 290 | "link": "https://stackoverflow.com/users/8720127/otti" 291 | }, 292 | "is_answered": false, 293 | "view_count": 17, 294 | "answer_count": 0, 295 | "score": -1, 296 | "last_activity_date": 1507130821, 297 | "creation_date": 1507128605, 298 | "last_edit_date": 1507130821, 299 | "question_id": 46567911, 300 | "link": "https://stackoverflow.com/questions/46567911/groupby-in-object-js", 301 | "title": "groupBy in object JS" 302 | }, 303 | { 304 | "tags": [ 305 | "python", 306 | "python-2.7", 307 | "sympy" 308 | ], 309 | "owner": { 310 | "reputation": 1, 311 | "user_id": 8690199, 312 | "user_type": "registered", 313 | "profile_image": "https://graph.facebook.com/1431695686879084/picture?type=large", 314 | "display_name": "Connor Johnson", 315 | "link": "https://stackoverflow.com/users/8690199/connor-johnson" 316 | }, 317 | "is_answered": true, 318 | "view_count": 26, 319 | "answer_count": 2, 320 | "score": 0, 321 | "last_activity_date": 1507130820, 322 | "creation_date": 1507128667, 323 | "last_edit_date": 1507130820, 324 | "question_id": 46567926, 325 | "link": "https://stackoverflow.com/questions/46567926/is-there-a-way-to-just-print-the-y-when-your-answer-is-x-y", 326 | "title": "Is there a way to just print the () when your answer is (x, y)?" 327 | }, 328 | { 329 | "tags": [ 330 | "javascript", 331 | "reactjs", 332 | "antd" 333 | ], 334 | "owner": { 335 | "reputation": 379, 336 | "user_id": 7750163, 337 | "user_type": "registered", 338 | "profile_image": "https://www.gravatar.com/avatar/5f2b163135cf123d8dd534277d1c8cb7?s=128&d=identicon&r=PG&f=1", 339 | "display_name": "Valera", 340 | "link": "https://stackoverflow.com/users/7750163/valera" 341 | }, 342 | "is_answered": false, 343 | "view_count": 13, 344 | "answer_count": 1, 345 | "score": 0, 346 | "last_activity_date": 1507130818, 347 | "creation_date": 1507101512, 348 | "question_id": 46559121, 349 | "link": "https://stackoverflow.com/questions/46559121/antd-multiple-select-remove-values-that-are-not-in-options-list-anymore", 350 | "title": "Antd multiple select remove values that are not in options list anymore" 351 | }, 352 | { 353 | "tags": [ 354 | "javascript", 355 | "angularjs" 356 | ], 357 | "owner": { 358 | "reputation": 36, 359 | "user_id": 7437087, 360 | "user_type": "registered", 361 | "accept_rate": 100, 362 | "profile_image": "https://lh5.googleusercontent.com/-5D-PfT8Y4EQ/AAAAAAAAAAI/AAAAAAAAABY/C59Q4iS5dGU/photo.jpg?sz=128", 363 | "display_name": "Rui Queirós", 364 | "link": "https://stackoverflow.com/users/7437087/rui-queir%c3%b3s" 365 | }, 366 | "is_answered": false, 367 | "view_count": 15, 368 | "answer_count": 1, 369 | "score": 0, 370 | "last_activity_date": 1507130817, 371 | "creation_date": 1507130479, 372 | "question_id": 46568514, 373 | "link": "https://stackoverflow.com/questions/46568514/how-to-set-decimal-mask-in-javascript", 374 | "title": "How to set decimal mask in javascript" 375 | }, 376 | { 377 | "tags": [ 378 | "c#" 379 | ], 380 | "owner": { 381 | "reputation": 8, 382 | "user_id": 7158391, 383 | "user_type": "registered", 384 | "profile_image": "https://www.gravatar.com/avatar/ad4d43f723bb1a8e22ebaf2af4bc26f4?s=128&d=identicon&r=PG&f=1", 385 | "display_name": "NewLearner", 386 | "link": "https://stackoverflow.com/users/7158391/newlearner" 387 | }, 388 | "is_answered": false, 389 | "view_count": 15, 390 | "answer_count": 2, 391 | "score": -1, 392 | "last_activity_date": 1507130815, 393 | "creation_date": 1507129620, 394 | "question_id": 46568222, 395 | "link": "https://stackoverflow.com/questions/46568222/system-io-exception-cannot-access-since-already-used-by-another-process", 396 | "title": "System IO Exception Cannot access since already used by another process" 397 | }, 398 | { 399 | "tags": [ 400 | "jquery", 401 | "asp.net", 402 | "vb.net", 403 | "html-table", 404 | "ascii" 405 | ], 406 | "owner": { 407 | "reputation": 20, 408 | "user_id": 3788803, 409 | "user_type": "registered", 410 | "accept_rate": 40, 411 | "profile_image": "https://i.stack.imgur.com/4fOsH.png?s=128&g=1", 412 | "display_name": "brettwbyron", 413 | "link": "https://stackoverflow.com/users/3788803/brettwbyron" 414 | }, 415 | "is_answered": false, 416 | "view_count": 2, 417 | "answer_count": 0, 418 | "score": 0, 419 | "last_activity_date": 1507130813, 420 | "creation_date": 1507130813, 421 | "question_id": 46568611, 422 | "link": "https://stackoverflow.com/questions/46568611/create-ascii-table-style-form-in-asp-net", 423 | "title": "Create ASCII table style form in asp.net" 424 | }, 425 | { 426 | "tags": [ 427 | "css" 428 | ], 429 | "owner": { 430 | "reputation": 9642, 431 | "user_id": 282772, 432 | "user_type": "registered", 433 | "accept_rate": 57, 434 | "profile_image": "https://www.gravatar.com/avatar/c66ee7eaf29dea1e0c2ab8b522970e7c?s=128&d=identicon&r=PG", 435 | "display_name": "Francesco", 436 | "link": "https://stackoverflow.com/users/282772/francesco" 437 | }, 438 | "is_answered": false, 439 | "view_count": 43, 440 | "closed_date": 1507074169, 441 | "answer_count": 3, 442 | "score": -1, 443 | "last_activity_date": 1507130811, 444 | "creation_date": 1507042041, 445 | "last_edit_date": 1507130811, 446 | "question_id": 46547385, 447 | "link": "https://stackoverflow.com/questions/46547385/absolute-div-centered-and-responsive", 448 | "closed_reason": "duplicate", 449 | "title": "Absolute div centered and responsive?" 450 | }, 451 | { 452 | "tags": [ 453 | "php", 454 | "apache", 455 | "email", 456 | "cpu" 457 | ], 458 | "owner": { 459 | "reputation": 97, 460 | "user_id": 6414306, 461 | "user_type": "registered", 462 | "profile_image": "https://www.gravatar.com/avatar/d3d24f39b6149337809bd0d8c67ea393?s=128&d=identicon&r=PG&f=1", 463 | "display_name": "A. Jain", 464 | "link": "https://stackoverflow.com/users/6414306/a-jain" 465 | }, 466 | "is_answered": false, 467 | "view_count": 11, 468 | "answer_count": 2, 469 | "score": -2, 470 | "last_activity_date": 1507130807, 471 | "creation_date": 1507129385, 472 | "question_id": 46568148, 473 | "link": "https://stackoverflow.com/questions/46568148/issue-in-sending-bulk-emails-using-php", 474 | "title": "issue in sending bulk emails using php" 475 | }, 476 | { 477 | "tags": [ 478 | "vue.js", 479 | "vuejs2", 480 | "vue-router" 481 | ], 482 | "owner": { 483 | "reputation": 1365, 484 | "user_id": 4230636, 485 | "user_type": "registered", 486 | "accept_rate": 100, 487 | "profile_image": "https://i.stack.imgur.com/288F8.jpg?s=128&g=1", 488 | "display_name": "Maciej Kwas", 489 | "link": "https://stackoverflow.com/users/4230636/maciej-kwas" 490 | }, 491 | "is_answered": false, 492 | "view_count": 3, 493 | "answer_count": 0, 494 | "score": 0, 495 | "last_activity_date": 1507130805, 496 | "creation_date": 1507130805, 497 | "question_id": 46568609, 498 | "link": "https://stackoverflow.com/questions/46568609/cache-view-in-vue-js", 499 | "title": "cache view in vue.js" 500 | }, 501 | { 502 | "tags": [ 503 | "android", 504 | "android-layout", 505 | "expandablelistview" 506 | ], 507 | "owner": { 508 | "reputation": 10, 509 | "user_id": 2139092, 510 | "user_type": "registered", 511 | "profile_image": "https://www.gravatar.com/avatar/5f9790a7cf614de2d7bab317a5d3178c?s=128&d=identicon&r=PG", 512 | "display_name": "NathW", 513 | "link": "https://stackoverflow.com/users/2139092/nathw" 514 | }, 515 | "is_answered": false, 516 | "view_count": 2, 517 | "answer_count": 0, 518 | "score": 0, 519 | "last_activity_date": 1507130805, 520 | "creation_date": 1507130805, 521 | "question_id": 46568610, 522 | "link": "https://stackoverflow.com/questions/46568610/change-image-in-expandable-listview", 523 | "title": "Change image in expandable listview" 524 | }, 525 | { 526 | "tags": [ 527 | "python", 528 | "python-2.7" 529 | ], 530 | "owner": { 531 | "reputation": 83, 532 | "user_id": 7096651, 533 | "user_type": "registered", 534 | "accept_rate": 73, 535 | "profile_image": "https://lh5.googleusercontent.com/-kukJg6_RD_A/AAAAAAAAAAI/AAAAAAAAAXM/WZnOuABNBkg/photo.jpg?sz=128", 536 | "display_name": "Henry Spike", 537 | "link": "https://stackoverflow.com/users/7096651/henry-spike" 538 | }, 539 | "is_answered": false, 540 | "view_count": 27, 541 | "answer_count": 1, 542 | "score": 1, 543 | "last_activity_date": 1507130804, 544 | "creation_date": 1507130159, 545 | "last_edit_date": 1507130351, 546 | "question_id": 46568410, 547 | "link": "https://stackoverflow.com/questions/46568410/how-to-draw-this-bow-tie-pattern-using-python-2-7", 548 | "title": "How to draw this bow tie pattern using Python 2.7?" 549 | }, 550 | { 551 | "tags": [ 552 | "angularjs", 553 | "firebase", 554 | "angularfire2" 555 | ], 556 | "owner": { 557 | "reputation": 321, 558 | "user_id": 1241876, 559 | "user_type": "registered", 560 | "accept_rate": 50, 561 | "profile_image": "https://www.gravatar.com/avatar/6d69cb33ea9c23e47e3e897e53991e08?s=128&d=identicon&r=PG", 562 | "display_name": "Rajesh Jain", 563 | "link": "https://stackoverflow.com/users/1241876/rajesh-jain" 564 | }, 565 | "is_answered": false, 566 | "view_count": 2, 567 | "answer_count": 0, 568 | "score": 0, 569 | "last_activity_date": 1507130803, 570 | "creation_date": 1507130803, 571 | "question_id": 46568606, 572 | "link": "https://stackoverflow.com/questions/46568606/namespace-firebase-has-no-exported-member-promise", 573 | "title": "Namespace'firebase' has no exported member 'Promise';" 574 | }, 575 | { 576 | "tags": [ 577 | "java", 578 | "tomcat", 579 | "struts2", 580 | "struts" 581 | ], 582 | "owner": { 583 | "reputation": 6, 584 | "user_id": 8689984, 585 | "user_type": "registered", 586 | "profile_image": "https://www.gravatar.com/avatar/2d02fa07ef70f9be2884f738c22a91c2?s=128&d=identicon&r=PG&f=1", 587 | "display_name": "mrrk", 588 | "link": "https://stackoverflow.com/users/8689984/mrrk" 589 | }, 590 | "is_answered": false, 591 | "view_count": 5, 592 | "answer_count": 0, 593 | "score": 0, 594 | "last_activity_date": 1507130802, 595 | "creation_date": 1507128703, 596 | "last_edit_date": 1507130802, 597 | "question_id": 46567940, 598 | "link": "https://stackoverflow.com/questions/46567940/when-i-use-tomcat-filters-with-struts-regex-url-it-is-not-going-to-action-class", 599 | "title": "When I use tomcat filters with struts regex url, it is not going to action class" 600 | }, 601 | { 602 | "tags": [ 603 | "android", 604 | "android-activity" 605 | ], 606 | "owner": { 607 | "reputation": 30, 608 | "user_id": 2493686, 609 | "user_type": "registered", 610 | "profile_image": "https://www.gravatar.com/avatar/5b2384e09fc671bd436a48a670c81640?s=128&d=identicon&r=PG", 611 | "display_name": "CanProgram", 612 | "link": "https://stackoverflow.com/users/2493686/canprogram" 613 | }, 614 | "is_answered": false, 615 | "view_count": 23, 616 | "answer_count": 2, 617 | "score": 1, 618 | "last_activity_date": 1507130801, 619 | "creation_date": 1507126175, 620 | "last_edit_date": 1507129430, 621 | "question_id": 46567046, 622 | "link": "https://stackoverflow.com/questions/46567046/how-to-quit-while-activities-are-in-stack-to-avoid-redrawing-during-logout-back", 623 | "title": "How to quit while activities are in stack (to avoid redrawing during logout/back)?" 624 | }, 625 | { 626 | "tags": [ 627 | "hl7-fhir", 628 | "fhir", 629 | "dstu2-fhir" 630 | ], 631 | "owner": { 632 | "reputation": 41, 633 | "user_id": 6053132, 634 | "user_type": "registered", 635 | "profile_image": "https://www.gravatar.com/avatar/783747cfa17f1d9c10ad1a9488d83aad?s=128&d=identicon&r=PG&f=1", 636 | "display_name": "Tayyab", 637 | "link": "https://stackoverflow.com/users/6053132/tayyab" 638 | }, 639 | "is_answered": false, 640 | "view_count": 6, 641 | "answer_count": 2, 642 | "score": 0, 643 | "last_activity_date": 1507130800, 644 | "creation_date": 1507112914, 645 | "last_edit_date": 1507113767, 646 | "question_id": 46562642, 647 | "link": "https://stackoverflow.com/questions/46562642/enclosing-multiple-fhir-resource-in-a-single-resource", 648 | "title": "Enclosing multiple fhir resource in a single resource" 649 | }, 650 | { 651 | "tags": [ 652 | "c++" 653 | ], 654 | "owner": { 655 | "reputation": 3, 656 | "user_id": 8422746, 657 | "user_type": "registered", 658 | "profile_image": "https://www.gravatar.com/avatar/09f70e91764cc18c0364012ac611324d?s=128&d=identicon&r=PG&f=1", 659 | "display_name": "kti", 660 | "link": "https://stackoverflow.com/users/8422746/kti" 661 | }, 662 | "is_answered": false, 663 | "view_count": 3, 664 | "answer_count": 0, 665 | "score": 0, 666 | "last_activity_date": 1507130794, 667 | "creation_date": 1507130794, 668 | "question_id": 46568603, 669 | "link": "https://stackoverflow.com/questions/46568603/serialization-c-breakpoint-error", 670 | "title": "Serialization C++ Breakpoint error" 671 | }, 672 | { 673 | "tags": [ 674 | "algorithm" 675 | ], 676 | "owner": { 677 | "reputation": 15, 678 | "user_id": 5450739, 679 | "user_type": "registered", 680 | "profile_image": "https://www.gravatar.com/avatar/cad9972457705b0a98222ca0c907c6b3?s=128&d=identicon&r=PG&f=1", 681 | "display_name": "Guillaume Guigue", 682 | "link": "https://stackoverflow.com/users/5450739/guillaume-guigue" 683 | }, 684 | "is_answered": false, 685 | "view_count": 14, 686 | "answer_count": 0, 687 | "score": 0, 688 | "last_activity_date": 1507130792, 689 | "creation_date": 1507128242, 690 | "last_edit_date": 1507130792, 691 | "question_id": 46567775, 692 | "link": "https://stackoverflow.com/questions/46567775/hungarian-method-variant", 693 | "title": "Hungarian method variant" 694 | }, 695 | { 696 | "tags": [ 697 | "regex", 698 | "posix", 699 | "oracle-sqldeveloper", 700 | "regexp-substr", 701 | "posix-ere" 702 | ], 703 | "owner": { 704 | "reputation": 621, 705 | "user_id": 4184236, 706 | "user_type": "registered", 707 | "accept_rate": 79, 708 | "profile_image": "https://lh6.googleusercontent.com/--E1rubWN-04/AAAAAAAAAAI/AAAAAAAAAOA/UIG1KJoc3cg/photo.jpg?sz=128", 709 | "display_name": "Artemio Ramirez", 710 | "link": "https://stackoverflow.com/users/4184236/artemio-ramirez" 711 | }, 712 | "is_answered": true, 713 | "view_count": 75, 714 | "accepted_answer_id": 46537258, 715 | "answer_count": 3, 716 | "score": 2, 717 | "last_activity_date": 1507130790, 718 | "creation_date": 1506967872, 719 | "last_edit_date": 1506969093, 720 | "question_id": 46531249, 721 | "link": "https://stackoverflow.com/questions/46531249/posix-ere-regular-expression-to-find-repeated-substring", 722 | "title": "POSIX ERE Regular expression to find repeated substring" 723 | }, 724 | { 725 | "tags": [ 726 | "html", 727 | "contenteditable" 728 | ], 729 | "owner": { 730 | "reputation": 164, 731 | "user_id": 2059909, 732 | "user_type": "registered", 733 | "accept_rate": 0, 734 | "profile_image": "https://i.stack.imgur.com/iNlLR.jpg?s=128&g=1", 735 | "display_name": "Harry Binswanger", 736 | "link": "https://stackoverflow.com/users/2059909/harry-binswanger" 737 | }, 738 | "is_answered": false, 739 | "view_count": 7, 740 | "answer_count": 0, 741 | "score": -1, 742 | "last_activity_date": 1507130790, 743 | "creation_date": 1507130471, 744 | "last_edit_date": 1507130790, 745 | "question_id": 46568510, 746 | "link": "https://stackoverflow.com/questions/46568510/on-a-local-htm-file-how-to-edit-and-save-in-a-browser", 747 | "title": "On a local .htm file, how to edit and save in a browser" 748 | } 749 | ], 750 | "has_more": true, 751 | "quota_max": 300, 752 | "quota_remaining": 299 753 | } --------------------------------------------------------------------------------