├── .babelrc ├── .bundle └── config ├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .firebaserc ├── .gitignore ├── .nvmrc ├── .scss-lint.yml ├── .storybook ├── .babelrc ├── addons.js ├── config.js ├── storybook.scss └── webpack.config.js ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── app ├── analytics │ ├── constants.js │ ├── createGaProxy.js │ ├── google-analytics.js │ └── index.js ├── assets │ └── images │ │ └── hnpwa-logo.png ├── client.jsx ├── components │ ├── AppShell.jsx │ ├── HackerNewsItem │ │ ├── HackerNewsItem.scss │ │ ├── HackerNewsItem.spec.jsx │ │ ├── HackerNewsItem.stories.jsx │ │ ├── __snapshots__ │ │ │ └── HackerNewsItem.spec.jsx.snap │ │ └── index.jsx │ ├── HackerNewsList │ │ ├── HackerNewsList.scss │ │ ├── HackerNewsList.spec.jsx │ │ ├── HackerNewsList.stories.jsx │ │ ├── __snapshots__ │ │ │ └── HackerNewsList.spec.jsx.snap │ │ └── index.jsx │ ├── HackerNewsListItem │ │ ├── HackerNewsListItem.scss │ │ ├── HackerNewsListItem.spec.jsx │ │ ├── HackerNewsListItem.stories.jsx │ │ ├── __snapshots__ │ │ │ └── HackerNewsListItem.spec.jsx.snap │ │ └── index.jsx │ ├── HackerNewsUser │ │ ├── HackerNewsUser.scss │ │ ├── HackerNewsUser.spec.jsx │ │ ├── __snapshots__ │ │ │ └── HackerNewsUser.spec.jsx.snap │ │ └── index.jsx │ ├── Header │ │ ├── Header.scss │ │ ├── Header.spec.jsx │ │ ├── Header.stories.jsx │ │ ├── __snapshots__ │ │ │ └── Header.spec.jsx.snap │ │ └── index.jsx │ ├── LoadingIndicator │ │ ├── LoadingIndicator.scss │ │ ├── LoadingIndicator.spec.jsx │ │ ├── LoadingIndicator.stories.jsx │ │ ├── __snapshots__ │ │ │ └── LoadingIndicator.spec.jsx.snap │ │ └── index.jsx │ ├── LocationPagination │ │ ├── LocationPagination.spec.jsx │ │ ├── __snapshots__ │ │ │ └── LocationPagination.spec.jsx.snap │ │ └── index.jsx │ └── Pagination │ │ ├── Pagination.scss │ │ ├── Pagination.spec.jsx │ │ ├── Pagination.stories.jsx │ │ ├── __snapshots__ │ │ └── Pagination.spec.jsx.snap │ │ └── index.jsx ├── containers │ ├── HackerNewsComment │ │ ├── HackerNewsComment.scss │ │ ├── HackerNewsComment.spec.jsx │ │ ├── HackerNewsComment.stories.jsx │ │ ├── __snapshots__ │ │ │ └── HackerNewsComment.spec.jsx.snap │ │ └── index.jsx │ ├── HackerNewsItemContainer.jsx │ ├── HackerNewsListContainer.jsx │ ├── HackerNewsUserContainer.jsx │ └── LocationPaginationContainer.jsx ├── index.ejs ├── pages │ ├── Feed │ │ └── index.jsx │ ├── Item │ │ └── index.jsx │ ├── User │ │ └── index.jsx │ └── index.js ├── scss │ ├── base │ │ ├── mixins │ │ │ └── _mixins.scss │ │ ├── typography.scss │ │ └── variables │ │ │ ├── _colors.scss │ │ │ └── _sizes.scss │ └── style.scss ├── server.jsx └── store │ ├── __tests__ │ ├── actions.spec.js │ ├── flattenComments.spec.js │ ├── reducers │ │ ├── byId.spec.js │ │ ├── comments │ │ │ ├── byId.spec.js │ │ │ └── posts.spec.js │ │ ├── currentPage.spec.js │ │ ├── isFetching.spec.js │ │ ├── items.spec.js │ │ └── user.spec.js │ └── selectors.spec.js │ ├── actionTypes.js │ ├── actions.js │ ├── byId │ └── index.js │ ├── comments │ ├── byId.js │ ├── index.js │ └── posts.js │ ├── configureStore.js │ ├── currentPage │ └── index.js │ ├── flattenComments.js │ ├── history.js │ ├── isFetching │ └── index.js │ ├── items │ └── index.js │ ├── rootReducer.js │ ├── selectors.js │ └── user │ └── index.js ├── devServer.js ├── firebase.json ├── functions ├── index.js ├── package.json └── yarn.lock ├── jest.config.json ├── package.json ├── paths.js ├── postcss.config.js ├── public ├── favicon.ico ├── icon-144x144.png ├── icon-192x192.png ├── icon-512x512.png ├── logo.png ├── manifest.json ├── og-image.png └── service-worker.js ├── test ├── __mocks__ │ ├── fileMock.js │ └── styleMock.js ├── setup.js └── shim.js ├── webpack.base.config.js ├── webpack.dev.config.js ├── webpack.prod.config.js ├── webpack.server.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["es2015", { "modules": false }], "react", "stage-2"], 3 | "plugins": [ 4 | "react-hot-loader/babel", 5 | "syntax-dynamic-import", 6 | [ 7 | "transform-imports", { 8 | "react-router-dom": { 9 | "transform": "react-router-dom/es/${member}", 10 | "preventFullImport": true 11 | }, 12 | "lodash": { 13 | "transform": "lodash/${member}", 14 | "preventFullImport": true 15 | } 16 | } 17 | ] 18 | ], 19 | "env": { 20 | "test": { 21 | "plugins": [ 22 | "dynamic-import-node", 23 | "transform-es2015-modules-commonjs" 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_PATH: "bundler" 3 | BUNDLE_DISABLE_SHARED_GEMS: "true" 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | working_directory: ~/hnpwa-react 3 | docker: 4 | - image: circleci/node:8.5.0 5 | environment: 6 | TZ: "/usr/share/zoneinfo/Asia/Seoul" 7 | 8 | version: 2 9 | 10 | jobs: 11 | checkout: 12 | <<: *defaults 13 | steps: 14 | - checkout 15 | - save_cache: 16 | key: repository-{{ .Environment.CIRCLE_SHA1 }} 17 | paths: 18 | - ~/hnpwa-react 19 | 20 | dependency: 21 | <<: *defaults 22 | steps: 23 | - restore_cache: 24 | key: repository-{{ .Environment.CIRCLE_SHA1 }} 25 | - restore_cache: 26 | key: root-npm-dependency-{{ .Branch }}-{{ checksum "yarn.lock" }} 27 | - run: 28 | name: Install Root Dependency 29 | command: yarn install 30 | - save_cache: 31 | key: root-npm-dependency-{{ .Branch }}-{{ checksum "yarn.lock" }} 32 | paths: 33 | - ./node_modules 34 | - ~/.yarn-cache 35 | - restore_cache: 36 | key: function-npm-dependency-{{ .Branch }}-{{ checksum "functions/yarn.lock" }} 37 | - run: 38 | name: Install GCP Function Dependency 39 | command: cd functions && yarn install && cd .. 40 | - save_cache: 41 | key: function-npm-dependency-{{ .Branch }}-{{ checksum "functions/yarn.lock" }} 42 | paths: 43 | - ./functions/node_modules 44 | 45 | test: 46 | <<: *defaults 47 | steps: 48 | - restore_cache: 49 | key: repository-{{ .Environment.CIRCLE_SHA1 }} 50 | - restore_cache: 51 | key: root-npm-dependency-{{ .Branch }}-{{ checksum "yarn.lock" }} 52 | - run: 53 | name: Test 54 | command: yarn test:coverage -- --maxWorkers=2 55 | - store_artifacts: 56 | path: coverage 57 | destination: coverage 58 | 59 | build: 60 | <<: *defaults 61 | steps: 62 | - restore_cache: 63 | key: repository-{{ .Environment.CIRCLE_SHA1 }} 64 | - restore_cache: 65 | key: root-npm-dependency-{{ .Branch }}-{{ checksum "yarn.lock" }} 66 | - restore_cache: 67 | key: function-npm-dependency-{{ .Branch }}-{{ checksum "functions/yarn.lock" }} 68 | - run: 69 | name: Build 70 | command: yarn build 71 | - save_cache: 72 | key: build-{{ .Environment.CIRCLE_SHA1 }} 73 | paths: 74 | - ~/hnpwa-react/build 75 | - ~/hnpwa-react/functions/server.bundle.js 76 | - ~/hnpwa-react/functions/views 77 | 78 | deploy-stage: 79 | <<: *defaults 80 | steps: 81 | - restore_cache: 82 | key: repository-{{ .Environment.CIRCLE_SHA1 }} 83 | - restore_cache: 84 | key: root-npm-dependency-{{ .Branch }}-{{ checksum "yarn.lock" }} 85 | - restore_cache: 86 | key: function-npm-dependency-{{ .Branch }}-{{ checksum "functions/yarn.lock" }} 87 | - restore_cache: 88 | key: build-{{ .Environment.CIRCLE_SHA1 }} 89 | - run: 90 | name: Deploy to firebase 91 | command: yarn deploy:stage 92 | 93 | deploy-production: 94 | <<: *defaults 95 | steps: 96 | - restore_cache: 97 | key: repository-{{ .Environment.CIRCLE_SHA1 }} 98 | - restore_cache: 99 | key: root-npm-dependency-{{ .Branch }}-{{ checksum "yarn.lock" }} 100 | - restore_cache: 101 | key: function-npm-dependency-{{ .Branch }}-{{ checksum "functions/yarn.lock" }} 102 | - restore_cache: 103 | key: build-{{ .Environment.CIRCLE_SHA1 }} 104 | - run: 105 | name: Deploy to firebase 106 | command: yarn deploy 107 | 108 | workflows: 109 | version: 2 110 | test-and-build-deploy: 111 | jobs: 112 | - checkout: 113 | filters: 114 | tags: 115 | only: /.*/ 116 | - dependency: 117 | requires: 118 | - checkout 119 | filters: 120 | tags: 121 | only: /.*/ 122 | - test: 123 | requires: 124 | - dependency 125 | filters: 126 | tags: 127 | only: /.*/ 128 | - build: 129 | requires: 130 | - test 131 | filters: 132 | branches: 133 | only: master 134 | tags: 135 | only: /.*/ 136 | - deploy-stage: 137 | requires: 138 | - build 139 | filters: 140 | branches: 141 | only: master 142 | tags: 143 | ignore: /.*/ 144 | - deploy-production: 145 | requires: 146 | - build 147 | filters: 148 | branches: 149 | ignore: /.*/ 150 | tags: 151 | only: /.*/ 152 | 153 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | build/ 4 | bundler/ 5 | functions/ 6 | scripts/ 7 | 8 | devServer.js 9 | generate-sw.js 10 | webpack.*.config.js 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "plugins": ["react", "jsx-a11y", "import"], 4 | "settings": { 5 | "import/resolver": { 6 | "eslint-import-resolver-webpack": { 7 | "config": "webpack.base.config.js" 8 | } 9 | } 10 | }, 11 | "rules": { 12 | "no-underscore-dangle": [ 13 | "error", 14 | { 15 | "allow": ["__PRELOADED_STATE__", "__REDUX_DEVTOOLS_EXTENSION_COMPOSE__"] 16 | } 17 | ], 18 | "jsx-a11y/href-no-hash": 0 19 | }, 20 | "globals": { 21 | "__DEV__": false, 22 | "__PROD__": false, 23 | "__SERVER__": false 24 | }, 25 | "env": { 26 | "browser": true 27 | }, 28 | "parser": "babel-eslint", 29 | "parserOptions": { 30 | "allowImportExportEverywhere": true 31 | }, 32 | "overrides": [ 33 | { 34 | "files": ["*.stories.jsx"], 35 | "rules": { 36 | "import/no-extraneous-dependencies": [2, { 37 | "devDependencies": true 38 | }] 39 | } 40 | }, 41 | { 42 | "files": ["*.spec.js", "*.spec.jsx"], 43 | "plugins": ["react", "jsx-a11y", "import", "jest"], 44 | "env": { 45 | "jest": true 46 | }, 47 | "rules": { 48 | "import/no-extraneous-dependencies": [2, { 49 | "devDependencies": true 50 | }] 51 | } 52 | }, 53 | { 54 | "files": ["service-worker.js"], 55 | "env": { 56 | "serviceworker": true 57 | }, 58 | "rules": { 59 | "comma-dangle": 0 60 | } 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "hnpwa-react" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | bundler/ 3 | coverage/ 4 | functions/node_modules/ 5 | functions/server.bundle.js 6 | functions/views/ 7 | gh-pages/ 8 | node_modules/ 9 | stats.json 10 | .idea/ 11 | .env 12 | 13 | *.log 14 | npm-debug.log* 15 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.5.0 2 | -------------------------------------------------------------------------------- /.scss-lint.yml: -------------------------------------------------------------------------------- 1 | severity: error 2 | 3 | linters: 4 | 5 | BorderZero: 6 | enabled: true 7 | convention: zero 8 | 9 | BemDepth: 10 | enabled: true 11 | 12 | DeclarationOrder: 13 | enabled: false 14 | 15 | ExtendDirective: 16 | enabled: true 17 | 18 | LeadingZero: 19 | enabled: false 20 | 21 | NameFormat: 22 | enabled: true 23 | 24 | PrivateNamingConvention: 25 | enabled: true 26 | prefix: _ 27 | 28 | PropertySortOrder: 29 | enabled: true 30 | 31 | QualifyingElement: 32 | enabled: false 33 | 34 | SelectorFormat: 35 | enabled: true 36 | convention: hyphenated_BEM 37 | class_convention: ^(?!js-).* 38 | class_convention_explanation: should not be written in the form js-* 39 | 40 | SingleLinePerProperty: 41 | enabled: true 42 | allow_single_line_rule_sets: false 43 | 44 | StringQuotes: 45 | enabled: true 46 | style: double_quotes 47 | -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-2"] 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-knobs/register'; 3 | import '@storybook/addon-options/register'; 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, setAddon } from '@storybook/react'; 2 | import infoAddon from '@storybook/addon-info'; 3 | import { setOptions } from '@storybook/addon-options'; 4 | 5 | import '../app/scss/style.scss'; 6 | import './storybook.scss'; 7 | 8 | setAddon(infoAddon); 9 | 10 | const req = require.context('../app', true, /\.stories\.jsx$/); 11 | 12 | 13 | setOptions({ 14 | name: 'HNPWA with React', 15 | url: 'https://github.com/taehwanno/hnpwa-react', 16 | }); 17 | 18 | function loadStories() { 19 | req.keys().forEach(path => req(path)); 20 | } 21 | 22 | configure(loadStories, module); 23 | -------------------------------------------------------------------------------- /.storybook/storybook.scss: -------------------------------------------------------------------------------- 1 | // scss-lint:disable all 2 | body { 3 | background-color: rgba(0, 0, 0, 0.05); 4 | background-image: repeating-linear-gradient(0deg, transparent, transparent 7px, rgba(0, 0, 0, 0.2) 1px, transparent 8px), repeating-linear-gradient(90deg, transparent, transparent 7px, rgba(0, 0, 0, 0.2) 1px, transparent 8px); 5 | background-size: 8px 8px; 6 | display: block; 7 | margin: 8px; 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const paths = require('../paths'); 4 | 5 | module.exports = { 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.stories.jsx?$/, 10 | include: [paths.app], 11 | exclude: /node_modules/, 12 | enforce: 'pre', 13 | loader: 'eslint-loader', 14 | }, 15 | { 16 | test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|cur)$/, 17 | loader: 'file-loader', 18 | options: { 19 | name: '[path][name].[ext]', 20 | }, 21 | }, 22 | { 23 | test: /\.scss$/, 24 | use: [ 25 | 'style-loader', 26 | 'css-loader?sourceMap', 27 | 'postcss-loader?sourceMap', 28 | 'resolve-url-loader', 29 | 'sass-loader?sourceMap', 30 | { 31 | loader: 'sass-resources-loader', 32 | options: { 33 | resources: [ 34 | './app/scss/base/**/_*.scss', 35 | ], 36 | }, 37 | }, 38 | ] 39 | }, 40 | ], 41 | }, 42 | resolve: { 43 | alias: { 44 | assets: paths.assets, 45 | components: paths.components, 46 | containers: paths.containers, 47 | pages: paths.pages, 48 | store: paths.store, 49 | }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'sass', '~> 3.4', '>= 3.4.23' 3 | gem 'rake', '~> 12.0' 4 | gem 'scss_lint', '~> 0.51.0' 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | rake (12.0.0) 5 | sass (3.4.23) 6 | scss_lint (0.51.0) 7 | rake (>= 0.9, < 13) 8 | sass (~> 3.4.20) 9 | 10 | PLATFORMS 11 | ruby 12 | 13 | DEPENDENCIES 14 | rake (~> 12.0) 15 | sass (~> 3.4, >= 3.4.23) 16 | scss_lint (~> 0.51.0) 17 | 18 | BUNDLED WITH 19 | 1.13.7 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Taehwan, No 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HNPWA with React [](https://circleci.com/gh/taehwanno/hnpwa-react/tree/master) 2 | 3 | > Hacker News readers as Progressive Web Apps with React, React Router, Redux, Immutable.js 4 | 5 | Live Demo: https://hnpwa-react.firebaseapp.com/ 6 | 7 |
You\'re right. But it was after that pain-staking experience that I became fully engrossed in using unittests for all non-trivial functionality. Live and learn.', 24 | comments: Immutable.List(), 25 | parent: 3339842, 26 | })], 27 | [3339842, Immutable.Map({ 28 | id: 3339842, 29 | level: 1, 30 | user: 'Confusion', 31 | time: 1323601529, 32 | time_ago: '6 years ago', 33 | timeAgo: '6 years ago', 34 | content: '
If I have to venture a guess, I guess you didn\'t have a comprehensive set of tests at the function/method level of the code? Having that would probably have caught the bug, because you would have written a test for correctly executing the code in that branch.', 35 | comments: Immutable.List([3340746]), 36 | parent: 3338903, 37 | })], 38 | [3338903, Immutable.Map({ 39 | id: 3338903, 40 | level: 0, 41 | user: 'akg', 42 | time: 1323563056, 43 | time_ago: '6 years ago', 44 | timeAgo: '6 years ago', 45 | content: '
Reminds me of the time I had written a physical simulation engine back in grad school and there was a "minus" sign error. Of course, the error was rare enough that we didn\'t notice it until after the code was used in a real production environment. Tracking down one minus sign in several hundred thousands of lines is a pain. Not to mention the uneasy feeling you get after you solve it, "How was everything ever working correctly before!? What else did we overlook?"', 46 | comments: Immutable.List([3339842]), 47 | parent: 3338485, 48 | })], 49 | [3339723, Immutable.Map({ 50 | id: 3339723, 51 | level: 0, 52 | user: 'TwoBit', 53 | time: 1323596319, 54 | time_ago: '6 years ago', 55 | timeAgo: '6 years ago', 56 | content: '
They could have solved that bug with one developer in ten minutes by just telling the PS3 to generate a core dump and running addr2line.exe on the core dump report\'s callstacks.
And the report places the blame on the server instead of their code. Clearly it\'s their code\'s fault for doing blocking sockets calls in a main thread.',
57 | comments: Immutable.List(),
58 | parent: 3338485,
59 | })],
60 | [3338485, Immutable.Map({
61 | id: 3338485,
62 | title: 'Lamest bug we ever encountered',
63 | points: 76,
64 | user: 'exch',
65 | time: 1323550709,
66 | time_ago: '6 years ago',
67 | timeAgo: '6 years ago',
68 | type: 'link',
69 | url: 'http://joostdevblog.blogspot.com/2011/12/lamest-bug-we-ever-encountered.html',
70 | domain: 'joostdevblog.blogspot.com',
71 | comments_count: 23,
72 | commentsCount: 23,
73 | comments: Immutable.List([3338903, 3339723]),
74 | })],
75 | ])),
76 | posts: Immutable.Map(),
77 | });
78 |
79 | const preloadedState = Immutable.Map({ comments });
80 |
81 | const store = createStore(rootReducer, preloadedState);
82 |
83 | stories
84 | .add('default', () => (
85 |
20 | There are no items to show. 21 |
22 |There are no items to show.
} 30 | {!isFetching && feeds.map((feed, index) => ( 31 |24 | User: 25 | | 26 |27 | taehwanno 28 | | 29 |
32 | Created: 33 | | 34 |35 | 3 years ago 36 | | 37 |
40 | Karma: 41 | | 42 |43 | 1234 44 | | 45 |
User: | 53 |{user} | 54 |
Created: | 57 |{information.get('created')} | 58 |
Karma: | 61 |{information.get('karma')} | 62 |
How you think React with TypeScript?
', 18 | }); 19 | const comments = Immutable.List([1, 2, 3, 4]); 20 | const wrapper = shallow(You\'re right. But it was after that pain-staking experience that I became fully engrossed in using unittests for all non-trivial functionality. Live and learn.', 23 | comments: Immutable.List(), 24 | parent: 3339842, 25 | })], 26 | [3339842, Immutable.Map({ 27 | id: 3339842, 28 | level: 1, 29 | user: 'Confusion', 30 | time: 1323601529, 31 | time_ago: '6 years ago', 32 | timeAgo: '6 years ago', 33 | content: '
If I have to venture a guess, I guess you didn\'t have a comprehensive set of tests at the function/method level of the code? Having that would probably have caught the bug, because you would have written a test for correctly executing the code in that branch.',
34 | comments: Immutable.List([3340746]),
35 | parent: 3338903,
36 | })],
37 | ])),
38 | posts: Immutable.Map(),
39 | });
40 |
41 | const preloadedState = Immutable.Map({ comments });
42 |
43 | const store = createStore(rootReducer, preloadedState);
44 |
45 | stories
46 | .add('default', () => (
47 |
Reminds me of the time I had written a physical simulation engine back in grad school and there was a "minus" sign error. Of course, the error was rare enough that we didn\'t notice it until after the code was used in a real production environment. Tracking down one minus sign in several hundred thousands of lines is a pain. Not to mention the uneasy feeling you get after you solve it, "How was everything ever working correctly before!? What else did we overlook?"', 68 | comments: [ 69 | { 70 | id: 3339842, 71 | level: 1, 72 | user: 'Confusion', 73 | time: 1323601529, 74 | time_ago: '6 years ago', 75 | content: '
If I have to venture a guess, I guess you didn\'t have a comprehensive set of tests at the function/method level of the code? Having that would probably have caught the bug, because you would have written a test for correctly executing the code in that branch.', 76 | comments: [ 77 | { 78 | id: 3340746, 79 | level: 2, 80 | user: 'akg', 81 | time: 1323630681, 82 | time_ago: '6 years ago', 83 | content: '
You\'re right. But it was after that pain-staking experience that I became fully engrossed in using unittests for all non-trivial functionality. Live and learn.', 84 | comments: [], 85 | }, 86 | ], 87 | }, 88 | ], 89 | }, 90 | { 91 | id: 3339723, 92 | level: 0, 93 | user: 'TwoBit', 94 | time: 1323596319, 95 | time_ago: '6 years ago', 96 | content: '
They could have solved that bug with one developer in ten minutes by just telling the PS3 to generate a core dump and running addr2line.exe on the core dump report\'s callstacks.
And the report places the blame on the server instead of their code. Clearly it\'s their code\'s fault for doing blocking sockets calls in a main thread.', 97 | comments: [], 98 | }, 99 | ], 100 | comments_count: 23, 101 | }; 102 | 103 | nock('https://node-hnapi.herokuapp.com/') 104 | .get(`/item/${id}`) 105 | .reply(200, responseBody); 106 | 107 | const expectedActions = [ 108 | { 109 | type: ACTIONS.HACKER_COMMENTS_FETCH_REQUEST, 110 | payload: id, 111 | }, 112 | { 113 | type: ACTIONS.HACKER_COMMENTS_FETCH_SUCCESS, 114 | payload: { 115 | ...responseBody, 116 | timeAgo: responseBody.time_ago, 117 | comments: flattenComments(responseBody).map(v => ({ ...v, timeAgo: v.time_ago })), 118 | }, 119 | }, 120 | ]; 121 | const store = mockStore({ comments: Immutable.Map() }); 122 | 123 | return store.dispatch(actions.fetchHackerComments(id)).then(() => { 124 | expect(store.getActions()).toEqual(expectedActions); 125 | }); 126 | }); 127 | 128 | it('creates HACKER_COMMENTS_FETCH_FAILURE when fetching comments has been failed', () => { 129 | const id = 1234; 130 | 131 | nock('https://node-hnapi.herokuapp.com/') 132 | .get(`/item/${id}`) 133 | .reply(400); 134 | 135 | const expectedActions = [ 136 | { 137 | type: ACTIONS.HACKER_COMMENTS_FETCH_REQUEST, 138 | payload: id, 139 | }, 140 | { 141 | type: ACTIONS.HACKER_COMMENTS_FETCH_FAILURE, 142 | payload: new Error('Bad Request'), 143 | error: true, 144 | }, 145 | ]; 146 | const store = mockStore({ user: Immutable.Map() }); 147 | 148 | return store.dispatch(actions.fetchHackerComments(id)).catch(() => { 149 | expect(store.getActions()).toEqual(expectedActions); 150 | }); 151 | }); 152 | }); 153 | }); 154 | 155 | describe('HACKER_NEWS_FETCH_* action creators', () => { 156 | it('should create an action to fetch hacker news request', () => { 157 | const type = 'news'; 158 | const page = 1; 159 | const expectedAction = { 160 | type: ACTIONS.HACKER_NEWS_FETCH_REQUEST, 161 | payload: { 162 | type, 163 | page, 164 | }, 165 | }; 166 | expect(actions.hackerNewsFetchRequest(type, page)).toEqual(expectedAction); 167 | }); 168 | 169 | it('should create an action to fetch hacker news success', () => { 170 | const type = 'news'; 171 | const page = 1; 172 | const data = []; 173 | const expectedAction = { 174 | type: ACTIONS.HACKER_NEWS_FETCH_SUCCESS, 175 | payload: { 176 | type, 177 | page, 178 | data, 179 | }, 180 | }; 181 | expect(actions.hackerNewsFetchSuccess(type, page, data)).toEqual(expectedAction); 182 | }); 183 | 184 | it('should create an action to fetch hacker news failure', () => { 185 | const error = new Error(); 186 | const expectedAction = { 187 | type: ACTIONS.HACKER_NEWS_FETCH_FAILURE, 188 | payload: error, 189 | error: true, 190 | }; 191 | expect(actions.hackerNewsFetchFailure(error)).toEqual(expectedAction); 192 | }); 193 | 194 | describe('async actions', () => { 195 | afterEach(() => { 196 | nock.cleanAll(); 197 | }); 198 | 199 | it('creates HACKER_NEWS_FETCH_SUCCESS when fetching news has been done', () => { 200 | const type = 'news'; 201 | const page = 1; 202 | 203 | nock('https://node-hnapi.herokuapp.com/') 204 | .get(`/${type}?page=${page}`) 205 | .reply(200, [{ 206 | comments_count: 116, 207 | domain: 'blog.ycombinator.com', 208 | id: 15348384, 209 | points: 140, 210 | time: 1506524153, 211 | time_ago: '2 hours ago', 212 | title: 'Interview with Mr. Money Mustache', 213 | type: 'link', 214 | url: 'https://blog.ycombinator.com/dont-start-a-blog-start-a-cult-mr-money-mustache/', 215 | user: 'craigcannon', 216 | }]); 217 | 218 | const expectedActions = [ 219 | { 220 | type: ACTIONS.HACKER_NEWS_FETCH_REQUEST, 221 | payload: { type, page }, 222 | }, 223 | { 224 | type: ACTIONS.HACKER_NEWS_FETCH_SUCCESS, 225 | payload: { 226 | type, 227 | page, 228 | data: [{ 229 | commentsCount: 116, 230 | domain: 'blog.ycombinator.com', 231 | id: 15348384, 232 | points: 140, 233 | time: 1506524153, 234 | timeAgo: '2 hours ago', 235 | title: 'Interview with Mr. Money Mustache', 236 | type: 'link', 237 | url: 'https://blog.ycombinator.com/dont-start-a-blog-start-a-cult-mr-money-mustache/', 238 | user: 'craigcannon', 239 | }], 240 | }, 241 | }, 242 | ]; 243 | const store = mockStore({ 244 | byId: {}, 245 | currentPage: 1, 246 | items: {}, 247 | }); 248 | 249 | return store.dispatch(actions.fetchHackerNews(type, page)).then(() => { 250 | expect(store.getActions()).toEqual(expectedActions); 251 | }); 252 | }); 253 | 254 | it('creates HACKER_NEWS_FETCH_FAILURE when fetching news has been failed', () => { 255 | const type = 'news'; 256 | const page = 1; 257 | 258 | nock('https://node-hnapi.herokuapp.com/') 259 | .get(`/${type}?page=${page}`) 260 | .reply(400); 261 | 262 | const expectedActions = [ 263 | { 264 | type: ACTIONS.HACKER_NEWS_FETCH_REQUEST, 265 | payload: { type, page }, 266 | }, 267 | { 268 | type: ACTIONS.HACKER_NEWS_FETCH_FAILURE, 269 | payload: new Error('Bad Request'), 270 | error: true, 271 | }, 272 | ]; 273 | const store = mockStore({ 274 | byId: {}, 275 | currentPage: 1, 276 | items: {}, 277 | }); 278 | 279 | return store.dispatch(actions.fetchHackerNews(type, page)).catch(() => { 280 | expect(store.getActions()).toEqual(expectedActions); 281 | }); 282 | }); 283 | }); 284 | }); 285 | 286 | describe('HACKER_USER_FETCH_* action creators', () => { 287 | it('should create an action to fetch hacker user request', () => { 288 | const id = 'taehwanno'; 289 | const expectedAction = { 290 | type: ACTIONS.HACKER_USER_FETCH_REQUEST, 291 | payload: id, 292 | }; 293 | expect(actions.hackerUserFetchRequest(id)).toEqual(expectedAction); 294 | }); 295 | 296 | it('should create an action to fetch hacker user success', () => { 297 | const data = {}; 298 | const expectedAction = { 299 | type: ACTIONS.HACKER_USER_FETCH_SUCCESS, 300 | payload: data, 301 | }; 302 | expect(actions.hackerUserFetchSuccess(data)).toEqual(expectedAction); 303 | }); 304 | 305 | it('should create an action to fetch hacker user failure', () => { 306 | const error = new Error(); 307 | const expectedAction = { 308 | type: ACTIONS.HACKER_USER_FETCH_FAILURE, 309 | payload: error, 310 | error: true, 311 | }; 312 | expect(actions.hackerUserFetchFailure(error)).toEqual(expectedAction); 313 | }); 314 | 315 | describe('async actions', () => { 316 | afterEach(() => { 317 | nock.cleanAll(); 318 | }); 319 | 320 | it('creates HACKER_USER_FETCH_SUCCESS when fetching user has been done', () => { 321 | const id = 'taehwanno'; 322 | 323 | nock('https://node-hnapi.herokuapp.com/') 324 | .get(`/user/${id}`) 325 | .reply(200, { 326 | id, 327 | created_time: 1284124124, 328 | created: '7 years ago', 329 | karma: 1224, 330 | avg: null, 331 | about: null, 332 | }); 333 | 334 | const expectedActions = [ 335 | { 336 | type: ACTIONS.HACKER_USER_FETCH_REQUEST, 337 | payload: id, 338 | }, 339 | { 340 | type: ACTIONS.HACKER_USER_FETCH_SUCCESS, 341 | payload: { 342 | id, 343 | created_time: 1284124124, 344 | created: '7 years ago', 345 | karma: 1224, 346 | avg: null, 347 | about: null, 348 | }, 349 | }, 350 | ]; 351 | const store = mockStore({ user: Immutable.Map() }); 352 | 353 | return store.dispatch(actions.fetchHackerUser(id)).then(() => { 354 | expect(store.getActions()).toEqual(expectedActions); 355 | }); 356 | }); 357 | 358 | it('creates HACKER_USER_FETCH_FAILURE when fetching user has been failed', () => { 359 | const id = 'taehwanno'; 360 | 361 | nock('https://node-hnapi.herokuapp.com/') 362 | .get(`/user/${id}`) 363 | .reply(400); 364 | 365 | const expectedActions = [ 366 | { 367 | type: ACTIONS.HACKER_USER_FETCH_REQUEST, 368 | payload: id, 369 | }, 370 | { 371 | type: ACTIONS.HACKER_USER_FETCH_FAILURE, 372 | payload: new Error('Bad Request'), 373 | error: true, 374 | }, 375 | ]; 376 | const store = mockStore({ user: Immutable.Map() }); 377 | 378 | return store.dispatch(actions.fetchHackerUser(id)).catch(() => { 379 | expect(store.getActions()).toEqual(expectedActions); 380 | }); 381 | }); 382 | }); 383 | }); 384 | }); 385 | -------------------------------------------------------------------------------- /app/store/__tests__/flattenComments.spec.js: -------------------------------------------------------------------------------- 1 | import flattenComments from '../flattenComments'; 2 | 3 | describe('flattenComments', () => { 4 | it('should return flatComments', () => { 5 | expect(flattenComments({ 6 | id: 3338485, 7 | title: 'Lamest bug we ever encountered', 8 | points: 76, 9 | user: 'exch', 10 | time: 1323550709, 11 | time_ago: '6 years ago', 12 | type: 'link', 13 | url: 'http://joostdevblog.blogspot.com/2011/12/lamest-bug-we-ever-encountered.html', 14 | domain: 'joostdevblog.blogspot.com', 15 | comments: [ 16 | { 17 | id: 3338903, 18 | level: 0, 19 | user: 'akg', 20 | time: 1323563056, 21 | time_ago: '6 years ago', 22 | content: '
Reminds me of the time I had written a physical simulation engine back in grad school and there was a "minus" sign error. Of course, the error was rare enough that we didn\'t notice it until after the code was used in a real production environment. Tracking down one minus sign in several hundred thousands of lines is a pain. Not to mention the uneasy feeling you get after you solve it, "How was everything ever working correctly before!? What else did we overlook?"', 23 | comments: [ 24 | { 25 | id: 3339842, 26 | level: 1, 27 | user: 'Confusion', 28 | time: 1323601529, 29 | time_ago: '6 years ago', 30 | content: '
If I have to venture a guess, I guess you didn\'t have a comprehensive set of tests at the function/method level of the code? Having that would probably have caught the bug, because you would have written a test for correctly executing the code in that branch.', 31 | comments: [ 32 | { 33 | id: 3340746, 34 | level: 2, 35 | user: 'akg', 36 | time: 1323630681, 37 | time_ago: '6 years ago', 38 | content: '
You\'re right. But it was after that pain-staking experience that I became fully engrossed in using unittests for all non-trivial functionality. Live and learn.', 39 | comments: [], 40 | }, 41 | ], 42 | }, 43 | ], 44 | }, 45 | { 46 | id: 3339723, 47 | level: 0, 48 | user: 'TwoBit', 49 | time: 1323596319, 50 | time_ago: '6 years ago', 51 | content: '
They could have solved that bug with one developer in ten minutes by just telling the PS3 to generate a core dump and running addr2line.exe on the core dump report\'s callstacks.
And the report places the blame on the server instead of their code. Clearly it\'s their code\'s fault for doing blocking sockets calls in a main thread.', 52 | comments: [], 53 | }, 54 | ], 55 | comments_count: 23, 56 | })).toEqual([ 57 | { 58 | id: 3340746, 59 | level: 2, 60 | user: 'akg', 61 | time: 1323630681, 62 | time_ago: '6 years ago', 63 | content: '
You\'re right. But it was after that pain-staking experience that I became fully engrossed in using unittests for all non-trivial functionality. Live and learn.', 64 | comments: [], 65 | parent: 3339842, 66 | }, 67 | { 68 | id: 3339842, 69 | level: 1, 70 | user: 'Confusion', 71 | time: 1323601529, 72 | time_ago: '6 years ago', 73 | content: '
If I have to venture a guess, I guess you didn\'t have a comprehensive set of tests at the function/method level of the code? Having that would probably have caught the bug, because you would have written a test for correctly executing the code in that branch.', 74 | comments: [3340746], 75 | parent: 3338903, 76 | }, 77 | { 78 | id: 3338903, 79 | level: 0, 80 | user: 'akg', 81 | time: 1323563056, 82 | time_ago: '6 years ago', 83 | content: '
Reminds me of the time I had written a physical simulation engine back in grad school and there was a "minus" sign error. Of course, the error was rare enough that we didn\'t notice it until after the code was used in a real production environment. Tracking down one minus sign in several hundred thousands of lines is a pain. Not to mention the uneasy feeling you get after you solve it, "How was everything ever working correctly before!? What else did we overlook?"', 84 | comments: [3339842], 85 | parent: 3338485, 86 | }, 87 | { 88 | id: 3339723, 89 | level: 0, 90 | user: 'TwoBit', 91 | time: 1323596319, 92 | time_ago: '6 years ago', 93 | content: '
They could have solved that bug with one developer in ten minutes by just telling the PS3 to generate a core dump and running addr2line.exe on the core dump report\'s callstacks.
And the report places the blame on the server instead of their code. Clearly it\'s their code\'s fault for doing blocking sockets calls in a main thread.', 94 | comments: [], 95 | parent: 3338485, 96 | }]); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /app/store/__tests__/reducers/byId.spec.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | import * as ACTIONS from 'store/actionTypes'; 4 | import byId from 'store/byId'; 5 | 6 | describe('byId reducer', () => { 7 | it('should return the initial state', () => { 8 | expect(byId(undefined, {})).toEqual(Immutable.Map()); 9 | }); 10 | 11 | it('should handle HACKER_NEWS_FETCH_SUCCESS', () => { 12 | const secondState = Immutable.Map(new Map([ 13 | [ 14 | 1, Immutable.Map({ 15 | id: 1, 16 | title: 'Interview with Mr. Money Mustache', 17 | }), 18 | ]])); 19 | 20 | expect(byId(Immutable.Map(), { 21 | type: ACTIONS.HACKER_NEWS_FETCH_SUCCESS, 22 | payload: { 23 | data: [ 24 | { 25 | id: 1, 26 | title: 'Interview with Mr. Money Mustache', 27 | }, 28 | ], 29 | }, 30 | })).toEqual(secondState); 31 | 32 | expect(byId(secondState, { 33 | type: ACTIONS.HACKER_NEWS_FETCH_SUCCESS, 34 | payload: { 35 | data: [ 36 | { 37 | id: 2, 38 | title: 'React with TypeScript', 39 | }, 40 | ], 41 | }, 42 | })).toEqual(secondState.set( 43 | 2, 44 | Immutable.Map({ 45 | id: 2, 46 | title: 'React with TypeScript', 47 | }), 48 | )); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /app/store/__tests__/reducers/comments/byId.spec.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | import * as ACTIONS from 'store/actionTypes'; 4 | import byId from 'store/comments/byId'; 5 | 6 | describe('comments.byId reducer', () => { 7 | it('should return the initial state', () => { 8 | expect(byId(undefined, {})).toEqual(Immutable.Map()); 9 | }); 10 | 11 | it('should handle HACKER_COMMENTS_FETCH_SUCCESS', () => { 12 | expect(byId(undefined, { 13 | type: ACTIONS.HACKER_COMMENTS_FETCH_SUCCESS, 14 | payload: { 15 | id: 3338485, 16 | title: 'Lamest bug we ever encountered', 17 | points: 76, 18 | user: 'exch', 19 | time: 1323550709, 20 | time_ago: '6 years ago', 21 | timeAgo: '6 years ago', 22 | type: 'link', 23 | url: 'http://joostdevblog.blogspot.com/2011/12/lamest-bug-we-ever-encountered.html', 24 | domain: 'joostdevblog.blogspot.com', 25 | comments_count: 23, 26 | commentsCount: 23, 27 | comments: [ 28 | { 29 | id: 3340746, 30 | level: 2, 31 | user: 'akg', 32 | time: 1323630681, 33 | time_ago: '6 years ago', 34 | timeAgo: '6 years ago', 35 | content: '
You\'re right. But it was after that pain-staking experience that I became fully engrossed in using unittests for all non-trivial functionality. Live and learn.', 36 | comments: [], 37 | parent: 3339842, 38 | }, 39 | { 40 | id: 3339842, 41 | level: 1, 42 | user: 'Confusion', 43 | time: 1323601529, 44 | time_ago: '6 years ago', 45 | content: '
If I have to venture a guess, I guess you didn\'t have a comprehensive set of tests at the function/method level of the code? Having that would probably have caught the bug, because you would have written a test for correctly executing the code in that branch.', 46 | comments: [3340746], 47 | parent: 3338903, 48 | }, 49 | { 50 | id: 3338903, 51 | level: 0, 52 | user: 'akg', 53 | time: 1323563056, 54 | time_ago: '6 years ago', 55 | timeAgo: '6 years ago', 56 | content: '
Reminds me of the time I had written a physical simulation engine back in grad school and there was a "minus" sign error. Of course, the error was rare enough that we didn\'t notice it until after the code was used in a real production environment. Tracking down one minus sign in several hundred thousands of lines is a pain. Not to mention the uneasy feeling you get after you solve it, "How was everything ever working correctly before!? What else did we overlook?"', 57 | comments: [3339842], 58 | parent: 3338485, 59 | }, 60 | { 61 | id: 3339723, 62 | level: 0, 63 | user: 'TwoBit', 64 | time: 1323596319, 65 | time_ago: '6 years ago', 66 | timeAgo: '6 years ago', 67 | content: '
They could have solved that bug with one developer in ten minutes by just telling the PS3 to generate a core dump and running addr2line.exe on the core dump report\'s callstacks.
And the report places the blame on the server instead of their code. Clearly it\'s their code\'s fault for doing blocking sockets calls in a main thread.', 68 | comments: [], 69 | parent: 3338485, 70 | }, 71 | ], 72 | }, 73 | })).toEqual(Immutable.Map(new Map([ 74 | [3340746, Immutable.Map({ 75 | id: 3340746, 76 | level: 2, 77 | user: 'akg', 78 | time: 1323630681, 79 | time_ago: '6 years ago', 80 | timeAgo: '6 years ago', 81 | content: '
You\'re right. But it was after that pain-staking experience that I became fully engrossed in using unittests for all non-trivial functionality. Live and learn.', 82 | comments: Immutable.List(), 83 | parent: 3339842, 84 | })], 85 | [3339842, Immutable.Map({ 86 | id: 3339842, 87 | level: 1, 88 | user: 'Confusion', 89 | time: 1323601529, 90 | time_ago: '6 years ago', 91 | content: '
If I have to venture a guess, I guess you didn\'t have a comprehensive set of tests at the function/method level of the code? Having that would probably have caught the bug, because you would have written a test for correctly executing the code in that branch.', 92 | comments: Immutable.List([3340746]), 93 | parent: 3338903, 94 | })], 95 | [3338903, Immutable.Map({ 96 | id: 3338903, 97 | level: 0, 98 | user: 'akg', 99 | time: 1323563056, 100 | time_ago: '6 years ago', 101 | timeAgo: '6 years ago', 102 | content: '
Reminds me of the time I had written a physical simulation engine back in grad school and there was a "minus" sign error. Of course, the error was rare enough that we didn\'t notice it until after the code was used in a real production environment. Tracking down one minus sign in several hundred thousands of lines is a pain. Not to mention the uneasy feeling you get after you solve it, "How was everything ever working correctly before!? What else did we overlook?"', 103 | comments: Immutable.List([3339842]), 104 | parent: 3338485, 105 | })], 106 | [3339723, Immutable.Map({ 107 | id: 3339723, 108 | level: 0, 109 | user: 'TwoBit', 110 | time: 1323596319, 111 | time_ago: '6 years ago', 112 | timeAgo: '6 years ago', 113 | content: '
They could have solved that bug with one developer in ten minutes by just telling the PS3 to generate a core dump and running addr2line.exe on the core dump report\'s callstacks.
And the report places the blame on the server instead of their code. Clearly it\'s their code\'s fault for doing blocking sockets calls in a main thread.', 114 | comments: Immutable.List(), 115 | parent: 3338485, 116 | })], 117 | [3338485, Immutable.Map({ 118 | id: 3338485, 119 | title: 'Lamest bug we ever encountered', 120 | points: 76, 121 | user: 'exch', 122 | time: 1323550709, 123 | time_ago: '6 years ago', 124 | timeAgo: '6 years ago', 125 | type: 'link', 126 | url: 'http://joostdevblog.blogspot.com/2011/12/lamest-bug-we-ever-encountered.html', 127 | domain: 'joostdevblog.blogspot.com', 128 | comments_count: 23, 129 | commentsCount: 23, 130 | comments: Immutable.List([3338903, 3339723]), 131 | })], 132 | ]))); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /app/store/__tests__/reducers/comments/posts.spec.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | import * as ACTIONS from 'store/actionTypes'; 4 | import posts from 'store/comments/posts'; 5 | 6 | describe('comments.posts reducer', () => { 7 | it('should return the initial state', () => { 8 | expect(posts(undefined, {})).toEqual(Immutable.Map()); 9 | }); 10 | 11 | it('should handle HACKER_COMMENTS_FETCH_SUCCESS', () => { 12 | expect(posts(undefined, { 13 | type: ACTIONS.HACKER_COMMENTS_FETCH_SUCCESS, 14 | payload: { 15 | comments_count: 40, 16 | id: 3338485, 17 | title: 'Lamest bug we ever encountered', 18 | points: 76, 19 | user: 'exch', 20 | time: 1323550709, 21 | time_ago: '6 years ago', 22 | type: 'link', 23 | url: 'http://joostdevblog.blogspot.com/2011/12/lamest-bug-we-ever-encountered.html', 24 | domain: 'joostdevblog.blogspot.com', 25 | }, 26 | })).toEqual(Immutable.Map(new Map([ 27 | [3338485, Immutable.Map({ 28 | commentsCount: 40, 29 | id: 3338485, 30 | title: 'Lamest bug we ever encountered', 31 | points: 76, 32 | user: 'exch', 33 | time: 1323550709, 34 | timeAgo: '6 years ago', 35 | type: 'link', 36 | url: 'http://joostdevblog.blogspot.com/2011/12/lamest-bug-we-ever-encountered.html', 37 | domain: 'joostdevblog.blogspot.com', 38 | })], 39 | ]))); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /app/store/__tests__/reducers/currentPage.spec.js: -------------------------------------------------------------------------------- 1 | import * as ACTIONS from 'store/actionTypes'; 2 | import currentPage from 'store/currentPage'; 3 | 4 | describe('currentPage reducer', () => { 5 | it('should return the initial state', () => { 6 | expect(currentPage(undefined, {})).toBe(1); 7 | }); 8 | 9 | it('should handle HACKER_NEWS_FETCH_REQUEST', () => { 10 | expect(currentPage(1, { 11 | type: ACTIONS.HACKER_NEWS_FETCH_REQUEST, 12 | payload: { 13 | page: 2, 14 | }, 15 | })).toBe(2); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app/store/__tests__/reducers/isFetching.spec.js: -------------------------------------------------------------------------------- 1 | import * as ACTIONS from 'store/actionTypes'; 2 | import isFetching from 'store/isFetching'; 3 | 4 | describe('isFetching reducer', () => { 5 | it('should return the initial state', () => { 6 | expect(isFetching(undefined, {})).toBe(false); 7 | }); 8 | 9 | it('should handle HACKER_COMMENTS_FETCH_REQUEST', () => { 10 | expect(isFetching(false, { 11 | type: ACTIONS.HACKER_COMMENTS_FETCH_REQUEST, 12 | })).toBe(true); 13 | }); 14 | 15 | it('should handle HACKER_COMMENTS_FETCH_SUCCESS', () => { 16 | expect(isFetching(true, { 17 | type: ACTIONS.HACKER_COMMENTS_FETCH_SUCCESS, 18 | })).toBe(false); 19 | }); 20 | 21 | it('should handle HACKER_COMMENTS_FETCH_FAILURE', () => { 22 | expect(isFetching(true, { 23 | type: ACTIONS.HACKER_COMMENTS_FETCH_FAILURE, 24 | })).toBe(false); 25 | }); 26 | 27 | it('should handle HACKER_NEWS_FETCH_REQUEST', () => { 28 | expect(isFetching(false, { 29 | type: ACTIONS.HACKER_NEWS_FETCH_REQUEST, 30 | })).toBe(true); 31 | }); 32 | 33 | it('should handle HACKER_NEWS_FETCH_SUCCESS', () => { 34 | expect(isFetching(true, { 35 | type: ACTIONS.HACKER_NEWS_FETCH_SUCCESS, 36 | })).toBe(false); 37 | }); 38 | 39 | it('should handle HACKER_NEWS_FETCH_FAILURE', () => { 40 | expect(isFetching(true, { 41 | type: ACTIONS.HACKER_NEWS_FETCH_FAILURE, 42 | })).toBe(false); 43 | }); 44 | 45 | it('should handle HACKER_USER_FETCH_REQUEST', () => { 46 | expect(isFetching(false, { 47 | type: ACTIONS.HACKER_USER_FETCH_REQUEST, 48 | })).toBe(true); 49 | }); 50 | 51 | it('should handle HACKER_USER_FETCH_SUCCESS', () => { 52 | expect(isFetching(true, { 53 | type: ACTIONS.HACKER_USER_FETCH_SUCCESS, 54 | })).toBe(false); 55 | }); 56 | 57 | it('should handle HACKER_USER_FETCH_FAILURE', () => { 58 | expect(isFetching(true, { 59 | type: ACTIONS.HACKER_USER_FETCH_FAILURE, 60 | })).toBe(false); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /app/store/__tests__/reducers/items.spec.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | import * as ACTIONS from 'store/actionTypes'; 4 | import items from 'store/items'; 5 | 6 | describe('items reducer', () => { 7 | it('should return the initial state', () => { 8 | expect(items(undefined, {})).toEqual(Immutable.fromJS({ 9 | ask: {}, 10 | jobs: {}, 11 | newest: {}, 12 | news: {}, 13 | show: {}, 14 | })); 15 | }); 16 | 17 | it('should handle HACKER_NEWS_FETCH_SUCCESS', () => { 18 | expect(items(undefined, { 19 | type: ACTIONS.HACKER_NEWS_FETCH_SUCCESS, 20 | payload: { 21 | type: 'ask', 22 | page: 1, 23 | data: [ 24 | { 25 | id: 10, 26 | }, 27 | ], 28 | }, 29 | })).toEqual(Immutable.fromJS({ 30 | ask: Immutable.Map(new Map([ 31 | [1, Immutable.List([10])], 32 | ])), 33 | jobs: {}, 34 | newest: {}, 35 | news: {}, 36 | show: {}, 37 | })); 38 | 39 | expect(items(Immutable.fromJS({ 40 | ask: Immutable.Map(new Map([ 41 | [1, Immutable.List([10])], 42 | ])), 43 | jobs: {}, 44 | newest: {}, 45 | news: {}, 46 | show: {}, 47 | }), { 48 | type: ACTIONS.HACKER_NEWS_FETCH_SUCCESS, 49 | payload: { 50 | type: 'jobs', 51 | page: 1, 52 | data: [ 53 | { 54 | id: 11, 55 | }, 56 | ], 57 | }, 58 | })).toEqual(Immutable.fromJS({ 59 | ask: Immutable.Map(new Map([ 60 | [1, Immutable.List([10])], 61 | ])), 62 | jobs: Immutable.Map(new Map([ 63 | [1, Immutable.List([11])], 64 | ])), 65 | newest: {}, 66 | news: {}, 67 | show: {}, 68 | })); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /app/store/__tests__/reducers/user.spec.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | import * as ACTIONS from 'store/actionTypes'; 4 | import user from 'store/user'; 5 | 6 | describe('user reducer', () => { 7 | it('should return the initial state', () => { 8 | expect(user(undefined, {})).toBe(Immutable.Map()); 9 | }); 10 | 11 | it('should handle HACKER_USER_FETCH_SUCCESS', () => { 12 | expect(user(undefined, { 13 | type: ACTIONS.HACKER_USER_FETCH_SUCCESS, 14 | payload: { 15 | id: 'taehwanno', 16 | created_time: 1284124124, 17 | created: '3 years ago', 18 | karma: 0, 19 | avg: null, 20 | about: null, 21 | }, 22 | })).toEqual(Immutable.Map({ 23 | taehwanno: Immutable.Map({ 24 | id: 'taehwanno', 25 | createdTime: 1284124124, 26 | created: '3 years ago', 27 | karma: 0, 28 | avg: null, 29 | about: null, 30 | }), 31 | })); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /app/store/__tests__/selectors.spec.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import * as selectors from '../selectors'; 3 | 4 | describe('selectors', () => { 5 | const state = Immutable.fromJS({ 6 | byId: {}, 7 | comments: { 8 | byId: Immutable.Map(), 9 | posts: Immutable.Map(), 10 | }, 11 | currentPage: 1, 12 | isFetching: false, 13 | items: { 14 | ask: {}, 15 | jobs: {}, 16 | newest: {}, 17 | news: {}, 18 | show: {}, 19 | }, 20 | user: Immutable.Map(), 21 | }); 22 | 23 | it('should select byId', () => { 24 | expect(selectors.getById(state)).toEqual(state.get('byId')); 25 | }); 26 | 27 | it('should select currentPage', () => { 28 | expect(selectors.getCurrentPage(state)).toBe(state.get('currentPage')); 29 | }); 30 | 31 | it('should select isFetching', () => { 32 | expect(selectors.getIsFetching(state)).toBe(state.get('isFetching')); 33 | }); 34 | 35 | it('should select items', () => { 36 | expect(selectors.getItems(state)).toEqual(state.get('items')); 37 | }); 38 | 39 | it('should select props.type', () => { 40 | const props = { type: 'news' }; 41 | expect(selectors.getFeedType(null, props)).toBe(props.type); 42 | }); 43 | 44 | describe('getFeeds', () => { 45 | it('should return Immutable.List when specific type feeds not exist in current page', () => { 46 | expect(selectors.getFeeds(state, { type: 'news' })).toEqual(Immutable.List()); 47 | }); 48 | 49 | it('should return feeds when specific type feeds exist in current page', () => { 50 | const props = { type: 'ask' }; 51 | const currentState = Immutable.Map({ 52 | byId: Immutable.Map(new Map([ 53 | [1, { title: 'React with TypeScript', id: 1 }], 54 | [2, { title: 'TypeScript vs Flow', id: 2 }], 55 | ])), 56 | currentPage: 1, 57 | items: Immutable.Map({ 58 | ask: Immutable.Map(new Map([ 59 | [1, Immutable.List([1, 2])], 60 | ])), 61 | jobs: Immutable.Map(), 62 | newest: Immutable.Map(), 63 | news: Immutable.Map(), 64 | show: Immutable.Map(), 65 | }), 66 | }); 67 | expect(selectors.getFeeds(currentState, props)) 68 | .toEqual(currentState 69 | .getIn(['items', 'ask', currentState.get('currentPage')]) 70 | .map(id => currentState.getIn(['byId', id]))); 71 | }); 72 | }); 73 | 74 | it('should return feed count', () => { 75 | const props = { type: 'ask' }; 76 | const currentState = Immutable.Map({ 77 | byId: Immutable.Map(new Map([ 78 | [1, { title: 'React with TypeScript', id: 1 }], 79 | [2, { title: 'TypeScript vs Flow', id: 2 }], 80 | ])), 81 | currentPage: 1, 82 | items: Immutable.Map({ 83 | ask: Immutable.Map(new Map([ 84 | [1, Immutable.List([1, 2])], 85 | ])), 86 | jobs: Immutable.Map(), 87 | newest: Immutable.Map(), 88 | news: Immutable.Map(), 89 | show: Immutable.Map(), 90 | }), 91 | }); 92 | 93 | expect(selectors.getFeedCount(state, props)).toBe(0); 94 | expect(selectors.getFeedCount(currentState, props)).toBe(2); 95 | }); 96 | 97 | it('should select props.user', () => { 98 | const props = { user: 'taehwanno' }; 99 | expect(selectors.getUserId(null, props)).toBe(props.user); 100 | }); 101 | 102 | describe('getSpecificUser', () => { 103 | it('should return null when specific user information not exist', () => { 104 | expect(selectors.getSpecificUser(state, { user: 'taehwanno' })).toEqual(null); 105 | }); 106 | 107 | it('should return user information when specific user exist', () => { 108 | const props = { user: 'taehwanno' }; 109 | const currentState = Immutable.Map({ 110 | user: Immutable.Map({ 111 | taehwanno: { 112 | id: 'taehwanno', 113 | karma: 0, 114 | created: '3 years ago', 115 | }, 116 | }), 117 | }); 118 | expect(selectors.getSpecificUser(currentState, props)) 119 | .toEqual(currentState.getIn(['user', 'taehwanno'])); 120 | }); 121 | }); 122 | 123 | it('should select comments', () => { 124 | expect(selectors.getComments(state)).toEqual(state.get('comments')); 125 | }); 126 | 127 | it('should select props.commentId', () => { 128 | const props = { commentId: 1234 }; 129 | expect(selectors.getCommentId(null, props)).toBe(props.commentId); 130 | }); 131 | 132 | it('should select props.itemId', () => { 133 | const props = { itemId: 1234 }; 134 | expect(selectors.getItemId(null, props)).toBe(props.itemId); 135 | }); 136 | 137 | it('should select comments.byId', () => { 138 | expect(selectors.getCommentsById(state)).toEqual(state.getIn(['comments', 'byId'])); 139 | }); 140 | 141 | it('should select comments.posts', () => { 142 | expect(selectors.getCommentsPosts(state)).toEqual(state.getIn(['comments', 'posts'])); 143 | }); 144 | 145 | describe('getItem', () => { 146 | it('should return null when matched itemId\'s comment not exist', () => { 147 | expect(selectors.getItem(state, { itemId: 1234 })).toEqual(null); 148 | }); 149 | 150 | it('should return item when matched itemId\'s comment exist', () => { 151 | const comment = Immutable.Map(); 152 | expect(selectors.getItem(Immutable.Map({ 153 | comments: Immutable.Map({ 154 | posts: Immutable.Map(new Map([ 155 | [1234, comment], 156 | ])), 157 | }), 158 | }), { itemId: 1234 })).toEqual(comment); 159 | }); 160 | }); 161 | 162 | it('should select matched id\'s comments', () => { 163 | const comment = Immutable.Map({ comments: Immutable.List([1, 2]) }); 164 | const currentState = Immutable.Map({ 165 | comments: Immutable.Map({ 166 | byId: Immutable.Map(new Map([[4321, comment]])), 167 | }), 168 | }); 169 | expect(selectors.getChildrenComments(currentState, { commentId: 4321 })) 170 | .toEqual(comment.get('comments')); 171 | }); 172 | 173 | it('should return selector that select children comments', () => { 174 | const comment = Immutable.Map({ comments: Immutable.List([1, 2]) }); 175 | const currentState = Immutable.Map({ 176 | comments: Immutable.Map({ 177 | byId: Immutable.Map(new Map([[4321, comment]])), 178 | }), 179 | }); 180 | const getChildrenComments = selectors.makeGetChildrenComments(); 181 | expect(getChildrenComments(currentState, { commentId: 4321 })).toEqual(comment.get('comments')); 182 | }); 183 | 184 | it('should return selector that select comment contents', () => { 185 | const comment = Immutable.Map(); 186 | const currentState = Immutable.Map({ 187 | comments: Immutable.Map({ 188 | byId: Immutable.Map(new Map([[4321, comment]])), 189 | }), 190 | }); 191 | const getCommentContents = selectors.makeGetCommentContents(); 192 | expect(getCommentContents(currentState, { commentId: 4321 })).toEqual(comment); 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /app/store/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const HACKER_COMMENTS_FETCH_REQUEST = 'HACKER_COMMENTS_FETCH_REQUEST'; 2 | export const HACKER_COMMENTS_FETCH_SUCCESS = 'HACKER_COMMENTS_FETCH_SUCCESS'; 3 | export const HACKER_COMMENTS_FETCH_FAILURE = 'HACKER_COMMENTS_FETCH_FAILURE'; 4 | export const HACKER_NEWS_FETCH_REQUEST = 'HACKER_NEWS_FETCH_REQUEST'; 5 | export const HACKER_NEWS_FETCH_SUCCESS = 'HACKER_NEWS_FETCH_SUCCESS'; 6 | export const HACKER_NEWS_FETCH_FAILURE = 'HACKER_NEWS_FETCH_FAILURE'; 7 | export const HACKER_USER_FETCH_REQUEST = 'HACKER_USER_FETCH_REQUEST'; 8 | export const HACKER_USER_FETCH_SUCCESS = 'HACKER_USER_FETCH_SUCCESS'; 9 | export const HACKER_USER_FETCH_FAILURE = 'HACKER_USER_FETCH_FAILURE'; 10 | -------------------------------------------------------------------------------- /app/store/actions.js: -------------------------------------------------------------------------------- 1 | import * as ACTIONS from './actionTypes'; 2 | import flattenComment from './flattenComments'; 3 | 4 | export function hackerCommentsFetchRequest(id) { 5 | return { 6 | type: ACTIONS.HACKER_COMMENTS_FETCH_REQUEST, 7 | payload: id, 8 | }; 9 | } 10 | 11 | export function hackerCommentsFetchSuccess(data) { 12 | return { 13 | type: ACTIONS.HACKER_COMMENTS_FETCH_SUCCESS, 14 | payload: data, 15 | }; 16 | } 17 | 18 | export function hackerCommentsFetchFailure(error) { 19 | return { 20 | type: ACTIONS.HACKER_COMMENTS_FETCH_FAILURE, 21 | payload: error, 22 | error: true, 23 | }; 24 | } 25 | 26 | export function fetchHackerComments(id) { 27 | return function thunk(dispatch) { 28 | dispatch(hackerCommentsFetchRequest(id)); 29 | 30 | return fetch(`https://node-hnapi.herokuapp.com/item/${id}`) 31 | .then(response => 32 | response.json().then(data => 33 | dispatch(hackerCommentsFetchSuccess({ 34 | ...data, 35 | timeAgo: data.time_ago, 36 | comments: flattenComment(data).map(v => ({ ...v, timeAgo: v.time_ago })), 37 | })))) 38 | .catch(error => dispatch(hackerCommentsFetchFailure(error))); 39 | }; 40 | } 41 | 42 | export function hackerNewsFetchRequest(type, page) { 43 | return { 44 | type: ACTIONS.HACKER_NEWS_FETCH_REQUEST, 45 | payload: { 46 | type, 47 | page, 48 | }, 49 | }; 50 | } 51 | 52 | export function hackerNewsFetchSuccess(type, page, data) { 53 | return { 54 | type: ACTIONS.HACKER_NEWS_FETCH_SUCCESS, 55 | payload: { 56 | type, 57 | page, 58 | data, 59 | }, 60 | }; 61 | } 62 | 63 | export function hackerNewsFetchFailure(error) { 64 | return { 65 | type: ACTIONS.HACKER_NEWS_FETCH_FAILURE, 66 | payload: error, 67 | error: true, 68 | }; 69 | } 70 | 71 | export function fetchHackerNews(type, page) { 72 | return function thunk(dispatch) { 73 | dispatch(hackerNewsFetchRequest(type, page)); 74 | 75 | return fetch(`https://node-hnapi.herokuapp.com/${type}?page=${page}`) 76 | .then(response => 77 | response.json() 78 | .then(data => data.map((v) => { 79 | const { comments_count: commentsCount, time_ago: timeAgo, ...rest } = v; 80 | return { ...rest, commentsCount, timeAgo }; 81 | })) 82 | .then(data => dispatch(hackerNewsFetchSuccess(type, page, data)))) 83 | .catch(error => dispatch(hackerNewsFetchFailure(error))); 84 | }; 85 | } 86 | 87 | export function hackerUserFetchRequest(id) { 88 | return { 89 | type: ACTIONS.HACKER_USER_FETCH_REQUEST, 90 | payload: id, 91 | }; 92 | } 93 | 94 | export function hackerUserFetchSuccess(data) { 95 | return { 96 | type: ACTIONS.HACKER_USER_FETCH_SUCCESS, 97 | payload: data, 98 | }; 99 | } 100 | 101 | export function hackerUserFetchFailure(error) { 102 | return { 103 | type: ACTIONS.HACKER_USER_FETCH_FAILURE, 104 | payload: error, 105 | error: true, 106 | }; 107 | } 108 | 109 | export function fetchHackerUser(id) { 110 | return function thunk(dispatch) { 111 | dispatch(hackerUserFetchRequest(id)); 112 | 113 | return fetch(`https://node-hnapi.herokuapp.com/user/${id}`) 114 | .then(response => response.json().then(data => dispatch(hackerUserFetchSuccess(data)))) 115 | .catch(error => dispatch(hackerUserFetchFailure(error))); 116 | }; 117 | } 118 | -------------------------------------------------------------------------------- /app/store/byId/index.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import * as ACTIONS from '../actionTypes'; 3 | 4 | const initialState = Immutable.Map(); 5 | 6 | function byIdReducer(state = initialState, action) { 7 | let newState = state; 8 | 9 | switch (action.type) { 10 | case ACTIONS.HACKER_NEWS_FETCH_SUCCESS: 11 | action.payload.data.forEach((v) => { 12 | newState = newState.set(v.id, Immutable.Map(v)); 13 | }); 14 | return newState; 15 | default: 16 | return state; 17 | } 18 | } 19 | 20 | export default byIdReducer; 21 | -------------------------------------------------------------------------------- /app/store/comments/byId.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import * as ACTIONS from '../actionTypes'; 3 | 4 | const initialState = Immutable.Map(); 5 | 6 | function byId(state = initialState, action) { 7 | switch (action.type) { 8 | // eslint-disable-next-line no-case-declarations 9 | case ACTIONS.HACKER_COMMENTS_FETCH_SUCCESS: 10 | let newState = state; 11 | action.payload.comments.forEach((comment) => { 12 | newState = newState.set(comment.id, Immutable.Map({ 13 | ...comment, 14 | comments: Immutable.List(comment.comments), 15 | })); 16 | }); 17 | 18 | return newState.set(action.payload.id, Immutable.Map({ 19 | ...action.payload, 20 | commentsCount: action.payload.comments_count, 21 | comments: Immutable.List(action.payload.comments 22 | .filter(v => v.parent === action.payload.id) 23 | .map(v => v.id)), 24 | })); 25 | default: 26 | return state; 27 | } 28 | } 29 | 30 | export default byId; 31 | -------------------------------------------------------------------------------- /app/store/comments/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux-immutable'; 2 | 3 | import byId from './byId'; 4 | import posts from './posts'; 5 | 6 | const commentsReducer = combineReducers({ 7 | byId, 8 | posts, 9 | }); 10 | 11 | export default commentsReducer; 12 | -------------------------------------------------------------------------------- /app/store/comments/posts.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import * as ACTIONS from '../actionTypes'; 3 | 4 | const initialState = Immutable.Map(); 5 | 6 | function postsReducer(state = initialState, action) { 7 | switch (action.type) { 8 | case ACTIONS.HACKER_COMMENTS_FETCH_SUCCESS: 9 | return state.set(action.payload.id, Immutable.Map({ 10 | commentsCount: action.payload.comments_count, 11 | id: action.payload.id, 12 | title: action.payload.title, 13 | points: action.payload.points, 14 | user: action.payload.user, 15 | time: action.payload.time, 16 | timeAgo: action.payload.time_ago, 17 | type: action.payload.type, 18 | url: action.payload.url, 19 | domain: action.payload.domain, 20 | })); 21 | default: 22 | return state; 23 | } 24 | } 25 | 26 | export default postsReducer; 27 | -------------------------------------------------------------------------------- /app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { createStore, applyMiddleware, compose } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | 5 | import rootReducer from './rootReducer'; 6 | 7 | const configureStore = ({ routerMiddleware = null, preloadedState = Immutable.Map() } = {}) => { 8 | const middlewares = [thunk]; 9 | 10 | if (routerMiddleware) { 11 | middlewares.push(routerMiddleware); 12 | } 13 | 14 | let enhancer = applyMiddleware(...middlewares); 15 | 16 | if (__DEV__ && !__SERVER__) { 17 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 18 | enhancer = composeEnhancer(applyMiddleware(...middlewares)); 19 | } 20 | 21 | const store = createStore(rootReducer, preloadedState, enhancer); 22 | 23 | if (__DEV__) { 24 | if (module.hot) { 25 | module.hot.accept('./rootReducer', () => { 26 | // eslint-disable-next-line global-require 27 | const nextRootReducer = require('./rootReducer').default; 28 | store.replaceReducer(nextRootReducer); 29 | }); 30 | } 31 | } 32 | 33 | return store; 34 | }; 35 | 36 | export default configureStore; 37 | -------------------------------------------------------------------------------- /app/store/currentPage/index.js: -------------------------------------------------------------------------------- 1 | import * as ACTIONS from '../actionTypes'; 2 | 3 | const initialState = 1; 4 | 5 | function currentPageReducer(state = initialState, action) { 6 | switch (action.type) { 7 | case ACTIONS.HACKER_NEWS_FETCH_REQUEST: 8 | return action.payload.page; 9 | default: 10 | return state; 11 | } 12 | } 13 | 14 | export default currentPageReducer; 15 | -------------------------------------------------------------------------------- /app/store/flattenComments.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flatten nested comments 3 | * 4 | * @param {Object} data 5 | * @param {Number} data.id - Hacker news item id 6 | * @param {Array