[
  {
    "path": ".eslintrc",
    "content": "{\n  \"extends\": [\n    \"react-app\",\n    \"./node_modules/react-redux-typescript-scripts/eslint.js\"\n  ]\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/server/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n/server/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"printWidth\": 80,\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"trailingComma\": \"es5\"\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"typescript.tsdk\": \"node_modules/typescript/lib\"\n}"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at piotrek.witek@gmail.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n# React, Redux, TypeScript - RealWorld App\n\n## 🚧🚧🚧 UNDER CONSTRUCTION 🚧🚧🚧\n\n### **LIVE DEMO: [LINK](https://react-redux-typescript-realworld-app.netlify.com/)**\n\n_Reference implementation of RealWorld [JAMStack](https://jamstack.org/) Application based on [\"React, Redux, TypeScript Guide\"](https://github.com/piotrwitek/react-redux-typescript-guide)\nand [Create React App v3.0](https://facebook.github.io/create-react-app/)._\n\n</div>\n\n---\n\n## Features Roadmap:\n- [x] Routing with React-Router\n- [ ] User Identity\n  - [ ] External providers (Google, Github, Bitbucket)\n  - [ ] Registration / Authentication\n- [x] Cross-cutting Application Services\n  - [x] Local Storage\n  - [x] Client Logger\n  - [x] Toasts\n  - [ ] Analytics\n- [x] Feature Folders\n  - [x] `/articles` - Articles listing with CRUD Operations\n  - [ ] `/realtime-monitoring` - Realtime monitoring of connected users using Websockets\n- [x] REST API Integration (API Client)\n- [ ] WebSockets Integration\n- [ ] Serverless Lambda Functions (Netlify Functions)\n- [ ] Utilities (HOC, Hooks, Media Queries...)\n- [ ] Typesafe Styling/Theming with CSSinJS (`Emotion`)\n- [ ] ...\n\n---\n\n## Available Scripts\n\n### `npm start`\n\nRuns the app in the development modeat [http://localhost:3000](http://localhost:3000)\n\n### `npm test`\n\nLaunches the test runner in the interactive watch mode.<br>\nSee the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.\n\n### `npm run build`\n\nBuilds the app for production to the `build` folder.<br>\nSee the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.\n\n## Learn More\nThis project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).\n"
  },
  {
    "path": "lambdas/.babelrc",
    "content": "// lambda build config\n{\n  \"presets\": [\"@babel/preset-typescript\", \"@babel/preset-env\"],\n  \"plugins\": [\n    \"@babel/transform-runtime\",\n    \"@babel/plugin-proposal-class-properties\",\n    \"@babel/plugin-transform-object-assign\",\n    \"@babel/plugin-proposal-object-rest-spread\"\n  ]\n}\n"
  },
  {
    "path": "lambdas/build/hello.js",
    "content": "!function(t,r){for(var e in r)t[e]=r[e]}(exports,function(t){var r={};function e(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return t[n].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{enumerable:!0,get:n})},e.r=function(t){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(t,\"__esModule\",{value:!0})},e.t=function(t,r){if(1&r&&(t=e(t)),8&r)return t;if(4&r&&\"object\"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(e.r(n),Object.defineProperty(n,\"default\",{enumerable:!0,value:t}),2&r&&\"string\"!=typeof t)for(var o in t)e.d(n,o,function(r){return t[r]}.bind(null,o));return n},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,\"a\",r),r},e.o=function(t,r){return Object.prototype.hasOwnProperty.call(t,r)},e.p=\"\",e(e.s=3)}([function(t,r,e){t.exports=e(4)},function(t,r){function e(t,r,e,n,o,i,a){try{var u=t[i](a),c=u.value}catch(t){return void e(t)}u.done?r(c):Promise.resolve(c).then(n,o)}t.exports=function(t){return function(){var r=this,n=arguments;return new Promise(function(o,i){var a=t.apply(r,n);function u(t){e(a,o,i,u,c,\"next\",t)}function c(t){e(a,o,i,u,c,\"throw\",t)}u(void 0)})}}},function(t,r){t.exports=require(\"querystring\")},function(t,r,e){\"use strict\";e.r(r),e.d(r,\"handler\",function(){return f});var n=e(0),o=e.n(n),i=e(1),a=e.n(i),u=e(2),c=e.n(u),f=function(){var t=a()(o.a.mark(function t(r,e){var n,i;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:t.t0=r.httpMethod,t.next=\"GET\"===t.t0?3:\"POST\"===t.t0?4:7;break;case 3:return t.abrupt(\"return\",{statusCode:200,body:\"Hello, World!\"});case 4:return n=c.a.parse(r.body),i=n.name||\"World!\",t.abrupt(\"return\",{statusCode:200,body:\"Hello, \".concat(i)});case 7:return t.abrupt(\"return\",{statusCode:405,body:\"Method Not Allowed\"});case 8:case\"end\":return t.stop()}},t)}));return function(r,e){return t.apply(this,arguments)}}()},function(t,r,e){var n=function(t){\"use strict\";var r,e=Object.prototype,n=e.hasOwnProperty,o=\"function\"==typeof Symbol?Symbol:{},i=o.iterator||\"@@iterator\",a=o.asyncIterator||\"@@asyncIterator\",u=o.toStringTag||\"@@toStringTag\";function c(t,r,e,n){var o=r&&r.prototype instanceof d?r:d,i=Object.create(o.prototype),a=new P(n||[]);return i._invoke=function(t,r,e){var n=s;return function(o,i){if(n===h)throw new Error(\"Generator is already running\");if(n===p){if(\"throw\"===o)throw i;return k()}for(e.method=o,e.arg=i;;){var a=e.delegate;if(a){var u=_(a,e);if(u){if(u===y)continue;return u}}if(\"next\"===e.method)e.sent=e._sent=e.arg;else if(\"throw\"===e.method){if(n===s)throw n=p,e.arg;e.dispatchException(e.arg)}else\"return\"===e.method&&e.abrupt(\"return\",e.arg);n=h;var c=f(t,r,e);if(\"normal\"===c.type){if(n=e.done?p:l,c.arg===y)continue;return{value:c.arg,done:e.done}}\"throw\"===c.type&&(n=p,e.method=\"throw\",e.arg=c.arg)}}}(t,e,a),i}function f(t,r,e){try{return{type:\"normal\",arg:t.call(r,e)}}catch(t){return{type:\"throw\",arg:t}}}t.wrap=c;var s=\"suspendedStart\",l=\"suspendedYield\",h=\"executing\",p=\"completed\",y={};function d(){}function v(){}function g(){}var m={};m[i]=function(){return this};var w=Object.getPrototypeOf,b=w&&w(w(S([])));b&&b!==e&&n.call(b,i)&&(m=b);var x=g.prototype=d.prototype=Object.create(m);function L(t){[\"next\",\"throw\",\"return\"].forEach(function(r){t[r]=function(t){return this._invoke(r,t)}})}function E(t){var r;this._invoke=function(e,o){function i(){return new Promise(function(r,i){!function r(e,o,i,a){var u=f(t[e],t,o);if(\"throw\"!==u.type){var c=u.arg,s=c.value;return s&&\"object\"==typeof s&&n.call(s,\"__await\")?Promise.resolve(s.__await).then(function(t){r(\"next\",t,i,a)},function(t){r(\"throw\",t,i,a)}):Promise.resolve(s).then(function(t){c.value=t,i(c)},function(t){return r(\"throw\",t,i,a)})}a(u.arg)}(e,o,r,i)})}return r=r?r.then(i,i):i()}}function _(t,e){var n=t.iterator[e.method];if(n===r){if(e.delegate=null,\"throw\"===e.method){if(t.iterator.return&&(e.method=\"return\",e.arg=r,_(t,e),\"throw\"===e.method))return y;e.method=\"throw\",e.arg=new TypeError(\"The iterator does not provide a 'throw' method\")}return y}var o=f(n,t.iterator,e.arg);if(\"throw\"===o.type)return e.method=\"throw\",e.arg=o.arg,e.delegate=null,y;var i=o.arg;return i?i.done?(e[t.resultName]=i.value,e.next=t.nextLoc,\"return\"!==e.method&&(e.method=\"next\",e.arg=r),e.delegate=null,y):i:(e.method=\"throw\",e.arg=new TypeError(\"iterator result is not an object\"),e.delegate=null,y)}function O(t){var r={tryLoc:t[0]};1 in t&&(r.catchLoc=t[1]),2 in t&&(r.finallyLoc=t[2],r.afterLoc=t[3]),this.tryEntries.push(r)}function j(t){var r=t.completion||{};r.type=\"normal\",delete r.arg,t.completion=r}function P(t){this.tryEntries=[{tryLoc:\"root\"}],t.forEach(O,this),this.reset(!0)}function S(t){if(t){var e=t[i];if(e)return e.call(t);if(\"function\"==typeof t.next)return t;if(!isNaN(t.length)){var o=-1,a=function e(){for(;++o<t.length;)if(n.call(t,o))return e.value=t[o],e.done=!1,e;return e.value=r,e.done=!0,e};return a.next=a}}return{next:k}}function k(){return{value:r,done:!0}}return v.prototype=x.constructor=g,g.constructor=v,g[u]=v.displayName=\"GeneratorFunction\",t.isGeneratorFunction=function(t){var r=\"function\"==typeof t&&t.constructor;return!!r&&(r===v||\"GeneratorFunction\"===(r.displayName||r.name))},t.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,g):(t.__proto__=g,u in t||(t[u]=\"GeneratorFunction\")),t.prototype=Object.create(x),t},t.awrap=function(t){return{__await:t}},L(E.prototype),E.prototype[a]=function(){return this},t.AsyncIterator=E,t.async=function(r,e,n,o){var i=new E(c(r,e,n,o));return t.isGeneratorFunction(e)?i:i.next().then(function(t){return t.done?t.value:i.next()})},L(x),x[u]=\"Generator\",x[i]=function(){return this},x.toString=function(){return\"[object Generator]\"},t.keys=function(t){var r=[];for(var e in t)r.push(e);return r.reverse(),function e(){for(;r.length;){var n=r.pop();if(n in t)return e.value=n,e.done=!1,e}return e.done=!0,e}},t.values=S,P.prototype={constructor:P,reset:function(t){if(this.prev=0,this.next=0,this.sent=this._sent=r,this.done=!1,this.delegate=null,this.method=\"next\",this.arg=r,this.tryEntries.forEach(j),!t)for(var e in this)\"t\"===e.charAt(0)&&n.call(this,e)&&!isNaN(+e.slice(1))&&(this[e]=r)},stop:function(){this.done=!0;var t=this.tryEntries[0].completion;if(\"throw\"===t.type)throw t.arg;return this.rval},dispatchException:function(t){if(this.done)throw t;var e=this;function o(n,o){return u.type=\"throw\",u.arg=t,e.next=n,o&&(e.method=\"next\",e.arg=r),!!o}for(var i=this.tryEntries.length-1;i>=0;--i){var a=this.tryEntries[i],u=a.completion;if(\"root\"===a.tryLoc)return o(\"end\");if(a.tryLoc<=this.prev){var c=n.call(a,\"catchLoc\"),f=n.call(a,\"finallyLoc\");if(c&&f){if(this.prev<a.catchLoc)return o(a.catchLoc,!0);if(this.prev<a.finallyLoc)return o(a.finallyLoc)}else if(c){if(this.prev<a.catchLoc)return o(a.catchLoc,!0)}else{if(!f)throw new Error(\"try statement without catch or finally\");if(this.prev<a.finallyLoc)return o(a.finallyLoc)}}}},abrupt:function(t,r){for(var e=this.tryEntries.length-1;e>=0;--e){var o=this.tryEntries[e];if(o.tryLoc<=this.prev&&n.call(o,\"finallyLoc\")&&this.prev<o.finallyLoc){var i=o;break}}i&&(\"break\"===t||\"continue\"===t)&&i.tryLoc<=r&&r<=i.finallyLoc&&(i=null);var a=i?i.completion:{};return a.type=t,a.arg=r,i?(this.method=\"next\",this.next=i.finallyLoc,y):this.complete(a)},complete:function(t,r){if(\"throw\"===t.type)throw t.arg;return\"break\"===t.type||\"continue\"===t.type?this.next=t.arg:\"return\"===t.type?(this.rval=this.arg=t.arg,this.method=\"return\",this.next=\"end\"):\"normal\"===t.type&&r&&(this.next=r),y},finish:function(t){for(var r=this.tryEntries.length-1;r>=0;--r){var e=this.tryEntries[r];if(e.finallyLoc===t)return this.complete(e.completion,e.afterLoc),j(e),y}},catch:function(t){for(var r=this.tryEntries.length-1;r>=0;--r){var e=this.tryEntries[r];if(e.tryLoc===t){var n=e.completion;if(\"throw\"===n.type){var o=n.arg;j(e)}return o}}throw new Error(\"illegal catch attempt\")},delegateYield:function(t,e,n){return this.delegate={iterator:S(t),resultName:e,nextLoc:n},\"next\"===this.method&&(this.arg=r),y}},t}(t.exports);try{regeneratorRuntime=n}catch(t){Function(\"r\",\"regeneratorRuntime = r\")(n)}}]));"
  },
  {
    "path": "lambdas/src/hello.ts",
    "content": "import { ALBHandler } from 'aws-lambda';\nimport querystring from 'querystring';\n\nexport const handler: ALBHandler = async (event, context) => {\n  switch (event.httpMethod) {\n    case 'GET': {\n      return {\n        statusCode: 200,\n        body: `Hello, World!`,\n      };\n    }\n\n    case 'POST': {\n      const params = querystring.parse(event.body!);\n      const name = params.name || 'World!';\n\n      return {\n        statusCode: 200,\n        body: `Hello, ${name}`,\n      };\n    }\n\n    default:\n      return { statusCode: 405, body: 'Method Not Allowed' };\n  }\n};\n"
  },
  {
    "path": "netlify.toml",
    "content": "[build]\n  functions = \"lambdas/build\" #  netlify-lambda reads this for local dev server\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"react-redux-typescript-realworld-app\",\n  \"description\": \"RealWorld App implementation based on \\\"react-redux-typescript-guide\\\"\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"author\": \"Piotr Witek <piotrek.witek@gmail.com> (http://piotrwitek.github.io/)\",\n  \"repository\": \"https://github.com/piotrwitek/react-redux-typescript-realworld-app.git\",\n  \"homepage\": \"https://react-redux-typescript-realworld-app.netlify.com/\",\n  \"license\": \"MIT\",\n  \"main\": \"src/index.tsx\",\n  \"scripts\": {\n    \"start:client\": \"react-scripts start\",\n    \"start:lambdas\": \"netlify-lambda serve lambdas/src\",\n    \"start\": \"concurrently 'npm run start:client' 'npm run start:lambdas'\",\n    \"build:client\": \"react-scripts build\",\n    \"build:lambdas\": \"netlify-lambda build lambdas/src\",\n    \"build\": \"concurrently 'npm run build:client' 'npm run build:lambdas'\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\",\n    \"reinstall\": \"rm -rf ./node_modules && npm install\",\n    \"ci-check\": \"npm run prettier && npm run tsc && npm run test\",\n    \"prettier\": \"prettier --list-different 'src/**/*.ts' || (echo '\\nPlease run the following command to fix:\\nnpm run prettier:fix\\n'; exit 1)\",\n    \"prettier:fix\": \"prettier --write 'src/**/*.ts'\",\n    \"tsc\": \"tsc -p ./ --noEmit\",\n    \"tsc:watch\": \"tsc -p ./ --noEmit -w\",\n    \"deploy\": \"openode deploy\"\n  },\n  \"dependencies\": {\n    \"@babel/polyfill\": \"7.4.3\",\n    \"@emotion/core\": \"10.0.10\",\n    \"@emotion/styled\": \"10.0.10\",\n    \"@types/aws-lambda\": \"8.10.24\",\n    \"@types/jest\": \"24.0.11\",\n    \"@types/node\": \"11.13.7\",\n    \"@types/prop-types\": \"15.7.1\",\n    \"@types/react\": \"16.8.14\",\n    \"@types/react-dom\": \"16.8.4\",\n    \"@types/react-redux\": \"7.0.8\",\n    \"@types/react-router-dom\": \"4.3.2\",\n    \"axios\": \"0.18.0\",\n    \"connected-react-router\": \"6.4.0\",\n    \"cuid\": \"2.1.6\",\n    \"fast-deep-equal\": \"2.0.1\",\n    \"formik\": \"1.5.2\",\n    \"netlify-lambda\": \"1.4.5\",\n    \"prettier\": \"1.17.0\",\n    \"prop-types\": \"15.7.2\",\n    \"react\": \"16.8.6\",\n    \"react-dom\": \"16.8.6\",\n    \"react-redux\": \"7.0.2\",\n    \"react-redux-typescript-scripts\": \"1.5.0\",\n    \"react-router-dom\": \"5.0.0\",\n    \"react-scripts\": \"3.0.0\",\n    \"react-testing-library\": \"6.1.2\",\n    \"react-toastify\": \"5.1.0\",\n    \"redux\": \"4.0.1\",\n    \"redux-observable\": \"1.1.0\",\n    \"reselect\": \"4.0.0\",\n    \"rxjs\": \"6.5.1\",\n    \"tslib\": \"1.9.3\",\n    \"typesafe-actions\": \"4.1.2\",\n    \"typescript\": \"3.4.5\",\n    \"utility-types\": \"3.5.0\",\n    \"yup\": \"0.27.0\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"devDependencies\": {\n    \"@types/yup\": \"0.26.12\",\n    \"concurrently\": \"4.1.0\"\n  }\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"shortcut icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1, shrink-to-fit=no\"\n    />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "server/package.json",
    "content": "{\n  \"name\": \"server\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"start\": \"...\",\n    \"build\": \"...\",\n    \"deploy\": \"openode deploy\"\n  },\n  \"author\": \"Piotrek Witek\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"openode\": \"2.0.3\",\n    \"react-redux-typescript-scripts\": \"1.5.0\",\n    \"typescript\": \"3.4.5\"\n  },\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "server/src/env.ts",
    "content": "export const DB_HOST = process.env.DB_HOST;\n"
  },
  {
    "path": "server/tsconfig.json",
    "content": "{\n  \"include\": [\"src\", \"typings\"],\n  \"exclude\": [\"src/**/*.spec.*\"],\n  \"extends\": \"./node_modules/react-redux-typescript-scripts/tsconfig.json\",\n  \"compilerOptions\": {}\n}\n"
  },
  {
    "path": "src/App.test.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport App from './App';\n\nit('renders without crashing', () => {\n  const div = document.createElement('div');\n  ReactDOM.render(<App />, div);\n  ReactDOM.unmountComponentAtNode(div);\n});\n"
  },
  {
    "path": "src/App.tsx",
    "content": "import React, { Component } from 'react';\nimport { Provider } from 'react-redux';\nimport { ConnectedRouter } from 'connected-react-router';\nimport { Switch, Route } from 'react-router';\n\nimport store, { history } from './store';\nimport Home from './routes/Home';\nimport { getPath } from './router-paths';\nimport AddArticle from './routes/AddArticle';\nimport EditArticle from './routes/EditArticle';\nimport ViewArticle from './routes/ViewArticle';\n\nclass App extends Component {\n  render() {\n    return (\n      <Provider store={store}>\n        <ConnectedRouter history={history}>\n          <Switch>\n            <Route exact path={getPath('home')} render={Home} />\n            <Route exact path={getPath('addArticle')} render={AddArticle} />\n            <Route\n              exact\n              path={getPath('editArticle', ':articleId')}\n              render={props => <EditArticle {...props} />}\n            />\n            <Route\n              exact\n              path={getPath('viewArticle', ':articleId')}\n              render={props => <ViewArticle {...props} />}\n            />\n            <Route render={() => <div>Page not found!</div>} />\n          </Switch>\n        </ConnectedRouter>\n      </Provider>\n    );\n  }\n}\n\nexport default App;\n"
  },
  {
    "path": "src/components/BackLink.tsx",
    "content": "import React from 'react';\nimport areEqual from 'fast-deep-equal';\n\nimport { Link } from 'react-router-dom';\n\ninterface Props {}\n\nconst BackLink = React.memo<Props>(() => {\n  return (\n    <Link to=\"/\" className=\"link\">\n      {'< Back'}\n    </Link>\n  );\n}, areEqual);\n\nexport default BackLink;\n"
  },
  {
    "path": "src/components/FlexBox.tsx",
    "content": "import styled from '@emotion/styled/macro';\nimport { CSSObject } from '@emotion/core';\n\ntype Props = {\n  className?: string;\n  style?: React.CSSProperties;\n  /* @description will add spacing between children, work dependinng on row/column layout */\n  itemsSpacing?: number;\n  direction?: CSSObject['flexDirection'];\n  wrap?: CSSObject['flexWrap'];\n  justify?: CSSObject['justifyContent'];\n  align?: CSSObject['alignItems'];\n  grow?: CSSObject['flexGrow'];\n  shrink?: CSSObject['flexShrink'];\n};\n\nconst FlexBox = styled('div')<Props>(\n  ({\n    itemsSpacing,\n    direction: flexDirection,\n    justify: justifyContent,\n    wrap: flexWrap,\n    align: alignItems,\n    grow: flexGrow,\n    shrink: flexShrink,\n  }) => ({\n    display: 'flex',\n    ...(itemsSpacing != null && {\n      '> * + *': {\n        [flexDirection === 'row' ? 'marginLeft' : 'marginTop']: itemsSpacing,\n      },\n    }),\n    flexDirection,\n    flexWrap,\n    justifyContent,\n    alignItems,\n    flexGrow,\n    flexShrink,\n  })\n);\n\nexport default FlexBox as React.FC<Props>;\n"
  },
  {
    "path": "src/components/FlexColumn.tsx",
    "content": "import React from 'react';\nimport { CSSObject } from '@emotion/core';\n\nimport FlexBox from './FlexBox';\n\ntype Props = React.ComponentProps<typeof FlexBox> & {\n  direction?: CSSObject['flexDirection'];\n};\n\nexport default (props: Props) => <FlexBox direction=\"column\" {...props} />;\n"
  },
  {
    "path": "src/components/FlexRow.tsx",
    "content": "import React from 'react';\nimport { CSSObject } from '@emotion/core';\n\nimport FlexBox from './FlexBox';\n\ntype Props = React.ComponentProps<typeof FlexBox> & {\n  direction?: CSSObject['flexDirection'];\n};\n\nexport default (props: Props) => <FlexBox direction=\"row\" {...props} />;\n"
  },
  {
    "path": "src/features/app/epics.ts",
    "content": "import { RootEpic } from 'MyTypes';\nimport { tap, ignoreElements, filter, first, map } from 'rxjs/operators';\n\nimport { isActionOf } from 'typesafe-actions';\nimport {\n  loadArticlesAsync,\n  createArticleAsync,\n  updateArticleAsync,\n  deleteArticleAsync,\n} from '../articles/actions';\n\nexport const persistArticlesInLocalStorage: RootEpic = (\n  action$,\n  store,\n  { localStorage }\n) =>\n  action$.pipe(\n    filter(\n      isActionOf([\n        loadArticlesAsync.success,\n        createArticleAsync.success,\n        updateArticleAsync.success,\n        deleteArticleAsync.success,\n      ])\n    ),\n    tap(_ => {\n      // handle side-effects\n      localStorage.set('articles', store.value.articles.articles);\n    }),\n    ignoreElements()\n  );\n\nexport const loadDataOnAppStart: RootEpic = (action$, store, { api }) =>\n  action$.pipe(\n    first(),\n    map(loadArticlesAsync.request)\n  );\n"
  },
  {
    "path": "src/features/articles/actions.ts",
    "content": "import { Article } from 'MyModels';\nimport { createAsyncAction } from 'typesafe-actions';\n\nexport const loadArticlesAsync = createAsyncAction(\n  'LOAD_ARTICLES_REQUEST',\n  'LOAD_ARTICLES_SUCCESS',\n  'LOAD_ARTICLES_FAILURE'\n)<undefined, Article[], string>();\n\nexport const createArticleAsync = createAsyncAction(\n  'CREATE_ARTICLE_REQUEST',\n  'CREATE_ARTICLE_SUCCESS',\n  'CREATE_ARTICLE_FAILURE'\n)<Article, Article[], string>();\n\nexport const updateArticleAsync = createAsyncAction(\n  'UPDATE_ARTICLE_REQUEST',\n  'UPDATE_ARTICLE_SUCCESS',\n  'UPDATE_ARTICLE_FAILURE'\n)<Article, Article[], string>();\n\nexport const deleteArticleAsync = createAsyncAction(\n  'DELETE_ARTICLE_REQUEST',\n  'DELETE_ARTICLE_SUCCESS',\n  'DELETE_ARTICLE_FAILURE'\n)<Article, Article[], Article>();\n"
  },
  {
    "path": "src/features/articles/components/ArticleActionsMenu.tsx",
    "content": "import { RootState } from 'MyTypes';\nimport React from 'react';\nimport { connect } from 'react-redux';\nimport { Link } from 'react-router-dom';\n\nimport { getPath } from '../../../router-paths';\n\nconst mapStateToProps = (state: RootState) => ({});\nconst dispatchProps = {};\n\ntype Props = ReturnType<typeof mapStateToProps> & typeof dispatchProps;\n\ntype State = {};\n\nclass ArticleActionsMenu extends React.Component<Props, State> {\n  render() {\n    return (\n      <section>\n        <Link to={getPath('addArticle')}>Create article</Link>\n      </section>\n    );\n  }\n}\n\nexport default connect(\n  mapStateToProps,\n  dispatchProps\n)(ArticleActionsMenu);\n"
  },
  {
    "path": "src/features/articles/components/ArticleForm.tsx",
    "content": "import React from 'react';\nimport cuid from 'cuid';\nimport { Form, FormikProps, Field, withFormik, ErrorMessage } from 'formik';\nimport { Article } from 'MyModels';\nimport { compose } from 'redux';\nimport { connect } from 'react-redux';\nimport { push } from 'connected-react-router';\n\nimport { createArticleAsync, updateArticleAsync } from '../actions';\n// import { getPath } from '../../../router-paths';\n\ntype FormValues = Pick<Article, 'title' | 'content'> & {};\n\nconst dispatchProps = {\n  createArticle: (values: FormValues) =>\n    createArticleAsync.request({\n      id: cuid(),\n      ...values,\n    }),\n  updateArticle: (values: Article) =>\n    updateArticleAsync.request({\n      ...values,\n    }),\n  redirectToListing: () => push('/'),\n};\n\ntype Props = typeof dispatchProps & {\n  article?: Article;\n};\n\nconst InnerForm: React.FC<Props & FormikProps<FormValues>> = props => {\n  const { isSubmitting, dirty } = props;\n  return (\n    <Form>\n      <div>\n        <label htmlFor=\"title\">Title</label>\n        <br />\n        <Field\n          name=\"title\"\n          placeholder=\"Title\"\n          component=\"input\"\n          type=\"text\"\n          required\n          autoFocus\n        />\n        <ErrorMessage name=\"title\" />\n      </div>\n\n      <div>\n        <label htmlFor=\"title\">Content</label>\n        <br />\n        <Field\n          name=\"content\"\n          placeholder=\"Article content\"\n          component=\"textarea\"\n          required\n          type=\"text\"\n        />\n        <ErrorMessage name=\"content\" />\n      </div>\n\n      <button type=\"submit\" disabled={!dirty || isSubmitting}>\n        Submit\n      </button>\n    </Form>\n  );\n};\n\nexport default compose(\n  connect(\n    null,\n    dispatchProps\n  ),\n  withFormik<Props, FormValues>({\n    enableReinitialize: true,\n    // initialize values\n    mapPropsToValues: ({ article: data }) => ({\n      title: (data && data.title) || '',\n      content: (data && data.content) || '',\n    }),\n    handleSubmit: (values, form) => {\n      if (form.props.article != null) {\n        form.props.updateArticle({ ...form.props.article, ...values });\n      } else {\n        form.props.createArticle(values);\n      }\n\n      form.props.redirectToListing();\n      form.setSubmitting(false);\n    },\n  })\n)(InnerForm);\n"
  },
  {
    "path": "src/features/articles/components/ArticleList.tsx",
    "content": "import { RootState } from 'MyTypes';\nimport React from 'react';\nimport { connect } from 'react-redux';\n\nimport * as selectors from '../selectors';\n\nimport ArticleListItem from './ArticleListItem';\n\nconst mapStateToProps = (state: RootState) => ({\n  isLoading: state.articles.isLoadingArticles,\n  articles: selectors.getArticles(state),\n});\nconst dispatchProps = {};\n\ntype Props = ReturnType<typeof mapStateToProps> & typeof dispatchProps;\n\nconst ArticleList: React.FC<Props> = ({\n  isLoading,\n  articles: articles = [],\n}) => {\n  if (isLoading) {\n    return <p style={{ textAlign: 'center' }}>Loading articles...</p>;\n  }\n\n  if (articles.length === 0) {\n    return (\n      <p style={{ textAlign: 'center' }}>\n        No articles yet, please create new...\n      </p>\n    );\n  }\n\n  return (\n    <ul style={getStyle()}>\n      {articles.map(article => (\n        <li key={article.id}>\n          <ArticleListItem article={article} />\n        </li>\n      ))}\n    </ul>\n  );\n};\n\nconst getStyle = (): React.CSSProperties => ({\n  textAlign: 'left',\n  margin: 'auto',\n  maxWidth: 500,\n});\n\nexport default connect(\n  mapStateToProps,\n  dispatchProps\n)(ArticleList);\n"
  },
  {
    "path": "src/features/articles/components/ArticleListItem.tsx",
    "content": "import { Article } from 'MyModels';\nimport React from 'react';\nimport areEqual from 'fast-deep-equal';\nimport { connect } from 'react-redux';\n\nimport { deleteArticleAsync } from '../actions';\nimport { getPath } from '../../../router-paths';\nimport FlexRow from '../../../components/FlexRow';\nimport { Link } from 'react-router-dom';\n\nconst dispatchProps = {\n  deleteArticle: deleteArticleAsync.request,\n};\n\ntype Props = typeof dispatchProps & {\n  article: Article;\n};\n\nconst ArticleListItem = React.memo<Props>(({ article, deleteArticle }) => {\n  return (\n    <FlexRow>\n      <div style={getStyle()}>{article.title}</div>\n      <FlexRow itemsSpacing={20}>\n        <Link to={getPath('viewArticle', article.id)}>View</Link>\n        <Link to={getPath('editArticle', article.id)}>Edit</Link>\n        <div\n          className=\"link\"\n          onClick={() => deleteArticle(article)}\n          style={{ color: 'darkred' }}\n        >\n          Delete\n        </div>\n      </FlexRow>\n    </FlexRow>\n  );\n}, areEqual);\n\nconst getStyle = (): React.CSSProperties => ({\n  overflowX: 'hidden',\n  textOverflow: 'ellipsis',\n  width: '300px',\n});\n\nexport default connect(\n  null,\n  dispatchProps\n)(ArticleListItem);\n"
  },
  {
    "path": "src/features/articles/components/ArticleView.tsx",
    "content": "import React from 'react';\n\nimport { Article } from 'MyModels';\n\ntype Props = {\n  article: Article;\n};\n\nconst ArticleView: React.FC<Props> = ({ article }) => {\n  return (\n    <div>\n      <h3>{article.title}</h3>\n\n      <p>{article.content}</p>\n    </div>\n  );\n};\n\nexport default ArticleView;\n"
  },
  {
    "path": "src/features/articles/epics.ts",
    "content": "import { RootEpic } from 'MyTypes';\nimport { from, of } from 'rxjs';\nimport { filter, switchMap, map, catchError } from 'rxjs/operators';\nimport { isActionOf } from 'typesafe-actions';\n\nimport {\n  loadArticlesAsync,\n  createArticleAsync,\n  updateArticleAsync,\n  deleteArticleAsync,\n} from './actions';\n\nexport const loadArticlesEpic: RootEpic = (action$, state$, { api }) =>\n  action$.pipe(\n    filter(isActionOf(loadArticlesAsync.request)),\n    switchMap(() =>\n      from(api.articles.loadArticles()).pipe(\n        map(loadArticlesAsync.success),\n        catchError(message => of(loadArticlesAsync.failure(message)))\n      )\n    )\n  );\n\nexport const createArticlesEpic: RootEpic = (action$, state$, { api }) =>\n  action$.pipe(\n    filter(isActionOf(createArticleAsync.request)),\n    switchMap(action =>\n      from(api.articles.createArticle(action.payload)).pipe(\n        map(createArticleAsync.success),\n        catchError(message => of(createArticleAsync.failure(message)))\n      )\n    )\n  );\n\nexport const updateArticlesEpic: RootEpic = (action$, state$, { api }) =>\n  action$.pipe(\n    filter(isActionOf(updateArticleAsync.request)),\n    switchMap(action =>\n      from(api.articles.updateArticle(action.payload)).pipe(\n        map(updateArticleAsync.success),\n        catchError(message => of(updateArticleAsync.failure(message)))\n      )\n    )\n  );\n\nexport const deleteArticlesEpic: RootEpic = (action$, state$, { api, toast }) =>\n  action$.pipe(\n    filter(isActionOf(deleteArticleAsync.request)),\n    switchMap(action =>\n      from(api.articles.deleteArticle(action.payload)).pipe(\n        map(deleteArticleAsync.success),\n        catchError(message => {\n          toast.error(message);\n          return of(deleteArticleAsync.failure(action.payload));\n        })\n      )\n    )\n  );\n"
  },
  {
    "path": "src/features/articles/reducer.ts",
    "content": "import { Article } from 'MyModels';\nimport { combineReducers } from 'redux';\nimport { createReducer } from 'typesafe-actions';\n\nimport {\n  loadArticlesAsync,\n  createArticleAsync,\n  updateArticleAsync,\n  deleteArticleAsync,\n} from './actions';\n\nconst reducer = combineReducers({\n  isLoadingArticles: createReducer(false as boolean)\n    .handleAction([loadArticlesAsync.request], (state, action) => true)\n    .handleAction(\n      [loadArticlesAsync.success, loadArticlesAsync.failure],\n      (state, action) => false\n    ),\n  articles: createReducer([] as Article[])\n    .handleAction(\n      [\n        loadArticlesAsync.success,\n        createArticleAsync.success,\n        updateArticleAsync.success,\n        deleteArticleAsync.success,\n      ],\n      (state, action) => action.payload\n    )\n    .handleAction(createArticleAsync.request, (state, action) => [\n      ...state,\n      action.payload,\n    ])\n    .handleAction(updateArticleAsync.request, (state, action) =>\n      state.map(i => (i.id === action.payload.id ? action.payload : i))\n    )\n    .handleAction(deleteArticleAsync.request, (state, action) =>\n      state.filter(i => i.id !== action.payload.id)\n    )\n    .handleAction(deleteArticleAsync.failure, (state, action) =>\n      state.concat(action.payload)\n    ),\n});\n\nexport default reducer;\n"
  },
  {
    "path": "src/features/articles/selectors.ts",
    "content": "import { RootState } from 'MyTypes';\n// import { createSelector } from 'reselect';\n\nexport const getArticles = (state: RootState) => state.articles.articles;\n"
  },
  {
    "path": "src/features/articles/types.d.ts",
    "content": "declare module 'MyModels' {\n  export type Article = {\n    id: string;\n    title: string;\n    content: string;\n  };\n}\n"
  },
  {
    "path": "src/index.css",
    "content": "body {\n  margin: 0;\n  padding: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n\na,\n.link {\n  color: #61dafb;\n  cursor: pointer;\n  text-decoration: none;\n}\n\na:hover,\n.link:hover {\n  text-decoration: underline;\n}\n"
  },
  {
    "path": "src/index.tsx",
    "content": "import '@babel/polyfill';\nimport 'tslib';\nimport React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport './index.css';\nimport * as serviceWorker from './serviceWorker';\nimport App from './App';\n\nReactDOM.render(<App />, document.getElementById('root'));\n\n// If you want your app to work offline and load faster, you can change\n// unregister() to register() below. Note this comes with some pitfalls.\n// Learn more about service workers: https://bit.ly/CRA-PWA\nserviceWorker.unregister();\n"
  },
  {
    "path": "src/layouts/Main.css",
    "content": ".App {\n  min-width: 500px;\n}\n\n.App-header {\n  background-color: #282c34;\n  min-height: 80px;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: center;\n  font-size: 24px;\n  color: white;\n}\n\n.App-main {\n  margin: 0 auto;\n  width: 500px;\n  overflow-x: hidden;\n}\n\n.App-logo {\n  animation: App-logo-spin infinite 20s linear;\n  height: 40px;\n  pointer-events: none;\n}\n\n@keyframes App-logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "src/layouts/Main.tsx",
    "content": "import React, { FC } from 'react';\nimport { Link } from 'react-router-dom';\n\nimport './Main.css';\nimport logo from '../assets/logo.svg';\nimport FlexRow from '../components/FlexRow';\n\ntype Props = {\n  renderActionsMenu?: () => JSX.Element;\n};\n\nconst Main: FC<Props> = ({ children, renderActionsMenu }) => (\n  <div className=\"App\">\n    <header className=\"App-header\">\n      <FlexRow\n        grow={1}\n        align=\"center\"\n        justify=\"space-between\"\n        style={{ padding: '0 60px' }}\n      >\n        <FlexRow align=\"center\">\n          <img src={logo} className=\"App-logo\" alt=\"logo\" />\n          <Link className=\"App-link\" to=\"/\">\n            Demo App\n          </Link>\n        </FlexRow>\n        {renderActionsMenu && renderActionsMenu()}\n      </FlexRow>\n    </header>\n    <main className=\"App-main\">{children}</main>\n  </div>\n);\n\nexport default Main;\n"
  },
  {
    "path": "src/react-app-env.d.ts",
    "content": "/// <reference types=\"react-scripts\" />\n"
  },
  {
    "path": "src/router-paths.ts",
    "content": "const pathsMap = {\n  home: () => '/',\n  addArticle: () => '/add-article',\n  viewArticle: (articleId: string) => `/articles/${articleId}`,\n  editArticle: (articleId: string) => `/articles/${articleId}/edit`,\n};\ntype PathsMap = typeof pathsMap;\n\nexport const getPath = <TRoute extends keyof PathsMap>(\n  route: TRoute,\n  ...params: Parameters<PathsMap[TRoute]>\n) => {\n  const pathCb: (...args: any[]) => string = pathsMap[route];\n\n  return pathCb(...params);\n};\n"
  },
  {
    "path": "src/routes/AddArticle.tsx",
    "content": "import React from 'react';\n\nimport ArticleForm from '../features/articles/components/ArticleForm';\nimport Main from '../layouts/Main';\nimport BackLink from '../components/BackLink';\n\nexport default () => (\n  <Main renderActionsMenu={() => <BackLink />}>\n    <ArticleForm />\n  </Main>\n);\n"
  },
  {
    "path": "src/routes/EditArticle.tsx",
    "content": "import { RootState } from 'MyTypes';\nimport React from 'react';\nimport { match } from 'react-router';\n\nimport ArticleForm from '../features/articles/components/ArticleForm';\nimport Main from '../layouts/Main';\nimport BackLink from '../components/BackLink';\nimport { connect } from 'react-redux';\n\ntype OwnProps = {\n  match: match<{ articleId: string }>;\n};\n\nconst mapStateToProps = (state: RootState, ownProps: OwnProps) => ({\n  article: state.articles.articles.find(\n    i => i.id === ownProps.match.params.articleId\n  ),\n});\n\ntype Props = ReturnType<typeof mapStateToProps>;\n\nconst EditArticle = ({ article }: Props) => {\n  return (\n    <Main renderActionsMenu={() => <BackLink />}>\n      <ArticleForm article={article} />\n    </Main>\n  );\n};\n\nexport default connect(mapStateToProps)(EditArticle);\n"
  },
  {
    "path": "src/routes/Home.tsx",
    "content": "import React from 'react';\n\nimport ArticleList from '../features/articles/components/ArticleList';\nimport ArticleActionsMenu from '../features/articles/components/ArticleActionsMenu';\nimport Main from '../layouts/Main';\n\nexport default () => (\n  <Main renderActionsMenu={() => <ArticleActionsMenu />}>\n    <ArticleList />\n  </Main>\n);\n"
  },
  {
    "path": "src/routes/ViewArticle.tsx",
    "content": "import { RootState } from 'MyTypes';\nimport React from 'react';\nimport { connect } from 'react-redux';\nimport { match } from 'react-router';\n\nimport ArticleView from '../features/articles/components/ArticleView';\nimport Main from '../layouts/Main';\nimport BackLink from '../components/BackLink';\n\ntype OwnProps = {\n  match: match<{ articleId: string }>;\n};\n\nconst mapStateToProps = (state: RootState, ownProps: OwnProps) => ({\n  article: state.articles.articles.find(\n    i => i.id === ownProps.match.params.articleId\n  ),\n});\n\ntype Props = ReturnType<typeof mapStateToProps>;\n\nconst ViewArticle = ({ article }: Props) => {\n  if (!article) {\n    return <div>'Article doesn\\'t exist'</div>;\n  }\n\n  return (\n    <Main renderActionsMenu={() => <BackLink />}>\n      <ArticleView article={article} />\n    </Main>\n  );\n};\n\nexport default connect(mapStateToProps)(ViewArticle);\n"
  },
  {
    "path": "src/serviceWorker.ts",
    "content": "// This optional code is used to register a service worker.\n// register() is not called by default.\n\n// This lets the app load faster on subsequent visits in production, and gives\n// it offline capabilities. However, it also means that developers (and users)\n// will only see deployed updates on subsequent visits to a page, after all the\n// existing tabs open on the page have been closed, since previously cached\n// resources are updated in the background.\n\n// To learn more about the benefits of this model and instructions on how to\n// opt-in, read https://bit.ly/CRA-PWA\n\nconst isLocalhost = Boolean(\n  window.location.hostname === 'localhost' ||\n    // [::1] is the IPv6 localhost address.\n    window.location.hostname === '[::1]' ||\n    // 127.0.0.1/8 is considered localhost for IPv4.\n    window.location.hostname.match(\n      /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\n    )\n);\n\ntype Config = {\n  onSuccess?: (registration: ServiceWorkerRegistration) => void;\n  onUpdate?: (registration: ServiceWorkerRegistration) => void;\n};\n\nexport function register(config?: Config) {\n  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n    // The URL constructor is available in all browsers that support SW.\n    const publicUrl = new URL(\n      (process as { env: { [key: string]: string } }).env.PUBLIC_URL,\n      window.location.href\n    );\n    if (publicUrl.origin !== window.location.origin) {\n      // Our service worker won't work if PUBLIC_URL is on a different origin\n      // from what our page is served on. This might happen if a CDN is used to\n      // serve assets; see https://github.com/facebook/create-react-app/issues/2374\n      return;\n    }\n\n    window.addEventListener('load', () => {\n      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\n\n      if (isLocalhost) {\n        // This is running on localhost. Let's check if a service worker still exists or not.\n        checkValidServiceWorker(swUrl, config);\n\n        // Add some additional logging to localhost, pointing developers to the\n        // service worker/PWA documentation.\n        navigator.serviceWorker.ready.then(() => {\n          console.log(\n            'This web app is being served cache-first by a service ' +\n              'worker. To learn more, visit https://bit.ly/CRA-PWA'\n          );\n        });\n      } else {\n        // Is not localhost. Just register service worker\n        registerValidSW(swUrl, config);\n      }\n    });\n  }\n}\n\nfunction registerValidSW(swUrl: string, config?: Config) {\n  navigator.serviceWorker\n    .register(swUrl)\n    .then(registration => {\n      registration.onupdatefound = () => {\n        const installingWorker = registration.installing;\n        if (installingWorker == null) {\n          return;\n        }\n        installingWorker.onstatechange = () => {\n          if (installingWorker.state === 'installed') {\n            if (navigator.serviceWorker.controller) {\n              // At this point, the updated precached content has been fetched,\n              // but the previous service worker will still serve the older\n              // content until all client tabs are closed.\n              console.log(\n                'New content is available and will be used when all ' +\n                  'tabs for this page are closed. See https://bit.ly/CRA-PWA.'\n              );\n\n              // Execute callback\n              if (config && config.onUpdate) {\n                config.onUpdate(registration);\n              }\n            } else {\n              // At this point, everything has been precached.\n              // It's the perfect time to display a\n              // \"Content is cached for offline use.\" message.\n              console.log('Content is cached for offline use.');\n\n              // Execute callback\n              if (config && config.onSuccess) {\n                config.onSuccess(registration);\n              }\n            }\n          }\n        };\n      };\n    })\n    .catch(error => {\n      console.error('Error during service worker registration:', error);\n    });\n}\n\nfunction checkValidServiceWorker(swUrl: string, config?: Config) {\n  // Check if the service worker can be found. If it can't reload the page.\n  fetch(swUrl)\n    .then(response => {\n      // Ensure service worker exists, and that we really are getting a JS file.\n      const contentType = response.headers.get('content-type');\n      if (\n        response.status === 404 ||\n        (contentType != null && contentType.indexOf('javascript') === -1)\n      ) {\n        // No service worker found. Probably a different app. Reload the page.\n        navigator.serviceWorker.ready.then(registration => {\n          registration.unregister().then(() => {\n            window.location.reload();\n          });\n        });\n      } else {\n        // Service worker found. Proceed as normal.\n        registerValidSW(swUrl, config);\n      }\n    })\n    .catch(() => {\n      console.log(\n        'No internet connection found. App is running in offline mode.'\n      );\n    });\n}\n\nexport function unregister() {\n  if ('serviceWorker' in navigator) {\n    navigator.serviceWorker.ready.then(registration => {\n      registration.unregister();\n    });\n  }\n}\n"
  },
  {
    "path": "src/services/articles-api-client.ts",
    "content": "import { Article } from 'MyModels';\n\nimport * as localStorage from './local-storage-service';\n\nlet articles: Article[] = localStorage.get<Article[]>('articles') || [];\n\nconst TIMEOUT = 750;\n\nexport function loadArticles(): Promise<Article[]> {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      resolve(articles);\n    }, TIMEOUT);\n  });\n}\n\nexport function createArticle(article: Article): Promise<Article[]> {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      articles = articles.concat(article);\n      resolve(articles);\n    }, TIMEOUT);\n  });\n}\n\nexport function updateArticle(article: Article): Promise<Article[]> {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      articles = articles.map(i => (i.id === article.id ? article : i));\n      resolve(articles);\n    }, TIMEOUT);\n  });\n}\n\nexport function deleteArticle(article: Article): Promise<Article[]> {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      articles = articles.filter(i => i.id !== article.id);\n      resolve(articles);\n    }, TIMEOUT);\n  });\n}\n"
  },
  {
    "path": "src/services/index.ts",
    "content": "import * as logger from './logger-service';\nimport * as articles from './articles-api-client';\nimport * as toast from './toast-service';\nimport * as localStorage from './local-storage-service';\n\nexport default {\n  logger,\n  localStorage,\n  toast,\n  api: {\n    articles,\n  },\n};\n"
  },
  {
    "path": "src/services/local-storage-service.ts",
    "content": "const version = process.env.APP_VERSION || 0;\nconst PREFIX = `MY_APP_v${version}::`;\n\nexport function set<T = object>(key: string, value: T): void {\n  if (!localStorage) {\n    return;\n  }\n\n  try {\n    const serializedValue = JSON.stringify(value);\n    localStorage.setItem(PREFIX + key, serializedValue);\n  } catch (error) {\n    throw new Error('store serialization failed');\n  }\n}\n\nexport function get<T = object>(key: string): T | undefined {\n  if (!localStorage) {\n    return;\n  }\n\n  try {\n    const serializedValue = localStorage.getItem(PREFIX + key);\n    if (serializedValue == null) {\n      return;\n    }\n    return JSON.parse(serializedValue);\n  } catch (error) {\n    throw new Error('store deserialization failed');\n  }\n}\n"
  },
  {
    "path": "src/services/logger-service.ts",
    "content": "// TODO: connect external client logging service here (e.g. Sentry SDK)\n// tslint:disable-next-line:no-console\nexport default { log: console.log };\n"
  },
  {
    "path": "src/services/toast-service.ts",
    "content": "import 'react-toastify/dist/ReactToastify.css';\nimport { toast } from 'react-toastify';\ntoast.configure();\n\nconst { info, warn, error, success } = toast;\n\nexport { info, warn, error, success };\n"
  },
  {
    "path": "src/services/types.d.ts",
    "content": "declare module 'MyTypes' {\n  export type Services = typeof import('./index').default;\n}\n"
  },
  {
    "path": "src/store/index.ts",
    "content": "import { RootAction, RootState, Services } from 'MyTypes';\nimport { createStore, applyMiddleware } from 'redux';\nimport { createEpicMiddleware } from 'redux-observable';\nimport { createBrowserHistory } from 'history';\nimport { routerMiddleware } from 'connected-react-router';\n\nimport { composeEnhancers } from './utils';\nimport rootReducer from './root-reducer';\nimport rootEpic from './root-epic';\nimport services from '../services';\n\nexport const epicMiddleware = createEpicMiddleware<\n  RootAction,\n  RootAction,\n  RootState,\n  Services\n>({\n  dependencies: services,\n});\n\n// configure middlewares\nexport const history = createBrowserHistory();\nconst middlewares = [routerMiddleware(history), epicMiddleware];\n// compose enhancers\nconst enhancer = composeEnhancers(applyMiddleware(...middlewares));\n\n// rehydrate state on app start\nconst initialState = {};\n\n// create store\nconst store = createStore(rootReducer(history), initialState, enhancer);\n\nepicMiddleware.run(rootEpic);\n\n// export store singleton instance\nexport default store;\n"
  },
  {
    "path": "src/store/root-action.ts",
    "content": "import { routerActions } from 'connected-react-router';\nimport * as articlesActions from '../features/articles/actions';\n\nexport default {\n  router: routerActions,\n  articles: articlesActions,\n};\n"
  },
  {
    "path": "src/store/root-epic.ts",
    "content": "import { combineEpics } from 'redux-observable';\n\nimport * as app from '../features/app/epics';\nimport * as articles from '../features/articles/epics';\n\nexport default combineEpics(...Object.values(app), ...Object.values(articles));\n"
  },
  {
    "path": "src/store/root-reducer.ts",
    "content": "import { combineReducers } from 'redux';\nimport { connectRouter } from 'connected-react-router';\nimport { History } from 'history';\n\nimport articles from '../features/articles/reducer';\n\nconst rootReducer = (history: History<any>) =>\n  combineReducers({\n    router: connectRouter(history),\n    articles,\n  });\n\nexport default rootReducer;\n"
  },
  {
    "path": "src/store/types.d.ts",
    "content": "import { StateType, ActionType } from 'typesafe-actions';\nimport { Epic } from 'redux-observable';\n\ndeclare module 'MyTypes' {\n  export type Store = StateType<typeof import('./index').default>;\n  export type RootState = StateType<\n    ReturnType<typeof import('./root-reducer').default>\n  >;\n  export type RootAction = ActionType<typeof import('./root-action').default>;\n\n  export type RootEpic = Epic<RootAction, RootAction, RootState, Services>;\n}\n\ndeclare module 'typesafe-actions' {\n  interface Types {\n    RootAction: ActionType<typeof import('./root-action').default>;\n  }\n}\n"
  },
  {
    "path": "src/store/utils.ts",
    "content": "import { compose } from 'redux';\n\nexport const composeEnhancers =\n  (process.env.NODE_ENV === 'development' &&\n    window &&\n    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||\n  compose;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"include\": [\"src\", \"typings\", \"lambdas/src\"],\n  \"exclude\": [\"src/**/*.spec.*\"],\n  \"extends\": \"./node_modules/react-redux-typescript-scripts/tsconfig.json\",\n  \"compilerOptions\": {}\n}\n"
  },
  {
    "path": "tsconfig.test.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"commonjs\"\n  }\n}"
  },
  {
    "path": "typings/augmentations.d.ts",
    "content": "export {};\n\n// Fix incorrect ALBResult type\ndeclare module 'aws-lambda' {\n  export interface ALBResult {\n    statusDescription?: string;\n    isBase64Encoded?: boolean;\n  }\n}\n"
  },
  {
    "path": "typings/globals.d.ts",
    "content": "declare interface Window {\n  __REDUX_DEVTOOLS_EXTENSION__: any;\n  __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any;\n}\n\ndeclare interface NodeModule {\n  hot?: { accept: (path: string, callback: () => void) => void };\n}\n\ndeclare interface System {\n  import<T = any>(module: string): Promise<T>;\n}\ndeclare var System: System;\n"
  },
  {
    "path": "typings/modules.d.ts",
    "content": "declare module '@emotion/styled/macro' {\n  import styled from '@emotion/styled';\n  export default styled;\n}\n"
  }
]