Repository: piotrwitek/react-redux-typescript-realworld-app Branch: master Commit: f92c56a61265 Files: 61 Total size: 49.5 KB Directory structure: gitextract_6ug6f53y/ ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .vscode/ │ └── settings.json ├── CODE_OF_CONDUCT.md ├── README.md ├── lambdas/ │ ├── .babelrc │ ├── build/ │ │ └── hello.js │ └── src/ │ └── hello.ts ├── netlify.toml ├── package.json ├── public/ │ ├── index.html │ └── manifest.json ├── server/ │ ├── package.json │ ├── src/ │ │ └── env.ts │ └── tsconfig.json ├── src/ │ ├── App.test.tsx │ ├── App.tsx │ ├── components/ │ │ ├── BackLink.tsx │ │ ├── FlexBox.tsx │ │ ├── FlexColumn.tsx │ │ └── FlexRow.tsx │ ├── features/ │ │ ├── app/ │ │ │ └── epics.ts │ │ └── articles/ │ │ ├── actions.ts │ │ ├── components/ │ │ │ ├── ArticleActionsMenu.tsx │ │ │ ├── ArticleForm.tsx │ │ │ ├── ArticleList.tsx │ │ │ ├── ArticleListItem.tsx │ │ │ └── ArticleView.tsx │ │ ├── epics.ts │ │ ├── reducer.ts │ │ ├── selectors.ts │ │ └── types.d.ts │ ├── index.css │ ├── index.tsx │ ├── layouts/ │ │ ├── Main.css │ │ └── Main.tsx │ ├── react-app-env.d.ts │ ├── router-paths.ts │ ├── routes/ │ │ ├── AddArticle.tsx │ │ ├── EditArticle.tsx │ │ ├── Home.tsx │ │ └── ViewArticle.tsx │ ├── serviceWorker.ts │ ├── services/ │ │ ├── articles-api-client.ts │ │ ├── index.ts │ │ ├── local-storage-service.ts │ │ ├── logger-service.ts │ │ ├── toast-service.ts │ │ └── types.d.ts │ └── store/ │ ├── index.ts │ ├── root-action.ts │ ├── root-epic.ts │ ├── root-reducer.ts │ ├── types.d.ts │ └── utils.ts ├── tsconfig.json ├── tsconfig.test.json └── typings/ ├── augmentations.d.ts ├── globals.d.ts └── modules.d.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc ================================================ { "extends": [ "react-app", "./node_modules/react-redux-typescript-scripts/eslint.js" ] } ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /server/node_modules /.pnp .pnp.js # testing /coverage # production /build /server/build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: .prettierrc ================================================ { "printWidth": 80, "semi": true, "singleQuote": true, "trailingComma": "es5" } ================================================ FILE: .vscode/settings.json ================================================ { "typescript.tsdk": "node_modules/typescript/lib" } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at piotrek.witek@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: README.md ================================================
# React, Redux, TypeScript - RealWorld App ## 🚧🚧🚧 UNDER CONSTRUCTION 🚧🚧🚧 ### **LIVE DEMO: [LINK](https://react-redux-typescript-realworld-app.netlify.com/)** _Reference implementation of RealWorld [JAMStack](https://jamstack.org/) Application based on ["React, Redux, TypeScript Guide"](https://github.com/piotrwitek/react-redux-typescript-guide) and [Create React App v3.0](https://facebook.github.io/create-react-app/)._
--- ## Features Roadmap: - [x] Routing with React-Router - [ ] User Identity - [ ] External providers (Google, Github, Bitbucket) - [ ] Registration / Authentication - [x] Cross-cutting Application Services - [x] Local Storage - [x] Client Logger - [x] Toasts - [ ] Analytics - [x] Feature Folders - [x] `/articles` - Articles listing with CRUD Operations - [ ] `/realtime-monitoring` - Realtime monitoring of connected users using Websockets - [x] REST API Integration (API Client) - [ ] WebSockets Integration - [ ] Serverless Lambda Functions (Netlify Functions) - [ ] Utilities (HOC, Hooks, Media Queries...) - [ ] Typesafe Styling/Theming with CSSinJS (`Emotion`) - [ ] ... --- ## Available Scripts ### `npm start` Runs the app in the development modeat [http://localhost:3000](http://localhost:3000) ### `npm test` Launches the test runner in the interactive watch mode.
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. ### `npm run build` Builds the app for production to the `build` folder.
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. ## Learn More This 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). ================================================ FILE: lambdas/.babelrc ================================================ // lambda build config { "presets": ["@babel/preset-typescript", "@babel/preset-env"], "plugins": [ "@babel/transform-runtime", "@babel/plugin-proposal-class-properties", "@babel/plugin-transform-object-assign", "@babel/plugin-proposal-object-rest-spread" ] } ================================================ FILE: lambdas/build/hello.js ================================================ !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=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=0;--e){var o=this.tryEntries[e];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev=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)}}])); ================================================ FILE: lambdas/src/hello.ts ================================================ import { ALBHandler } from 'aws-lambda'; import querystring from 'querystring'; export const handler: ALBHandler = async (event, context) => { switch (event.httpMethod) { case 'GET': { return { statusCode: 200, body: `Hello, World!`, }; } case 'POST': { const params = querystring.parse(event.body!); const name = params.name || 'World!'; return { statusCode: 200, body: `Hello, ${name}`, }; } default: return { statusCode: 405, body: 'Method Not Allowed' }; } }; ================================================ FILE: netlify.toml ================================================ [build] functions = "lambdas/build" # netlify-lambda reads this for local dev server ================================================ FILE: package.json ================================================ { "name": "react-redux-typescript-realworld-app", "description": "RealWorld App implementation based on \"react-redux-typescript-guide\"", "version": "0.1.0", "private": true, "author": "Piotr Witek (http://piotrwitek.github.io/)", "repository": "https://github.com/piotrwitek/react-redux-typescript-realworld-app.git", "homepage": "https://react-redux-typescript-realworld-app.netlify.com/", "license": "MIT", "main": "src/index.tsx", "scripts": { "start:client": "react-scripts start", "start:lambdas": "netlify-lambda serve lambdas/src", "start": "concurrently 'npm run start:client' 'npm run start:lambdas'", "build:client": "react-scripts build", "build:lambdas": "netlify-lambda build lambdas/src", "build": "concurrently 'npm run build:client' 'npm run build:lambdas'", "test": "react-scripts test", "eject": "react-scripts eject", "reinstall": "rm -rf ./node_modules && npm install", "ci-check": "npm run prettier && npm run tsc && npm run test", "prettier": "prettier --list-different 'src/**/*.ts' || (echo '\nPlease run the following command to fix:\nnpm run prettier:fix\n'; exit 1)", "prettier:fix": "prettier --write 'src/**/*.ts'", "tsc": "tsc -p ./ --noEmit", "tsc:watch": "tsc -p ./ --noEmit -w", "deploy": "openode deploy" }, "dependencies": { "@babel/polyfill": "7.4.3", "@emotion/core": "10.0.10", "@emotion/styled": "10.0.10", "@types/aws-lambda": "8.10.24", "@types/jest": "24.0.11", "@types/node": "11.13.7", "@types/prop-types": "15.7.1", "@types/react": "16.8.14", "@types/react-dom": "16.8.4", "@types/react-redux": "7.0.8", "@types/react-router-dom": "4.3.2", "axios": "0.18.0", "connected-react-router": "6.4.0", "cuid": "2.1.6", "fast-deep-equal": "2.0.1", "formik": "1.5.2", "netlify-lambda": "1.4.5", "prettier": "1.17.0", "prop-types": "15.7.2", "react": "16.8.6", "react-dom": "16.8.6", "react-redux": "7.0.2", "react-redux-typescript-scripts": "1.5.0", "react-router-dom": "5.0.0", "react-scripts": "3.0.0", "react-testing-library": "6.1.2", "react-toastify": "5.1.0", "redux": "4.0.1", "redux-observable": "1.1.0", "reselect": "4.0.0", "rxjs": "6.5.1", "tslib": "1.9.3", "typesafe-actions": "4.1.2", "typescript": "3.4.5", "utility-types": "3.5.0", "yup": "0.27.0" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "devDependencies": { "@types/yup": "0.26.12", "concurrently": "4.1.0" } } ================================================ FILE: public/index.html ================================================ React App
================================================ FILE: public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: server/package.json ================================================ { "name": "server", "version": "0.1.0", "private": true, "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "...", "build": "...", "deploy": "openode deploy" }, "author": "Piotrek Witek", "license": "MIT", "devDependencies": { "openode": "2.0.3", "react-redux-typescript-scripts": "1.5.0", "typescript": "3.4.5" }, "dependencies": {} } ================================================ FILE: server/src/env.ts ================================================ export const DB_HOST = process.env.DB_HOST; ================================================ FILE: server/tsconfig.json ================================================ { "include": ["src", "typings"], "exclude": ["src/**/*.spec.*"], "extends": "./node_modules/react-redux-typescript-scripts/tsconfig.json", "compilerOptions": {} } ================================================ FILE: src/App.test.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; it('renders without crashing', () => { const div = document.createElement('div'); ReactDOM.render(, div); ReactDOM.unmountComponentAtNode(div); }); ================================================ FILE: src/App.tsx ================================================ import React, { Component } from 'react'; import { Provider } from 'react-redux'; import { ConnectedRouter } from 'connected-react-router'; import { Switch, Route } from 'react-router'; import store, { history } from './store'; import Home from './routes/Home'; import { getPath } from './router-paths'; import AddArticle from './routes/AddArticle'; import EditArticle from './routes/EditArticle'; import ViewArticle from './routes/ViewArticle'; class App extends Component { render() { return ( } /> } />
Page not found!
} />
); } } export default App; ================================================ FILE: src/components/BackLink.tsx ================================================ import React from 'react'; import areEqual from 'fast-deep-equal'; import { Link } from 'react-router-dom'; interface Props {} const BackLink = React.memo(() => { return ( {'< Back'} ); }, areEqual); export default BackLink; ================================================ FILE: src/components/FlexBox.tsx ================================================ import styled from '@emotion/styled/macro'; import { CSSObject } from '@emotion/core'; type Props = { className?: string; style?: React.CSSProperties; /* @description will add spacing between children, work dependinng on row/column layout */ itemsSpacing?: number; direction?: CSSObject['flexDirection']; wrap?: CSSObject['flexWrap']; justify?: CSSObject['justifyContent']; align?: CSSObject['alignItems']; grow?: CSSObject['flexGrow']; shrink?: CSSObject['flexShrink']; }; const FlexBox = styled('div')( ({ itemsSpacing, direction: flexDirection, justify: justifyContent, wrap: flexWrap, align: alignItems, grow: flexGrow, shrink: flexShrink, }) => ({ display: 'flex', ...(itemsSpacing != null && { '> * + *': { [flexDirection === 'row' ? 'marginLeft' : 'marginTop']: itemsSpacing, }, }), flexDirection, flexWrap, justifyContent, alignItems, flexGrow, flexShrink, }) ); export default FlexBox as React.FC; ================================================ FILE: src/components/FlexColumn.tsx ================================================ import React from 'react'; import { CSSObject } from '@emotion/core'; import FlexBox from './FlexBox'; type Props = React.ComponentProps & { direction?: CSSObject['flexDirection']; }; export default (props: Props) => ; ================================================ FILE: src/components/FlexRow.tsx ================================================ import React from 'react'; import { CSSObject } from '@emotion/core'; import FlexBox from './FlexBox'; type Props = React.ComponentProps & { direction?: CSSObject['flexDirection']; }; export default (props: Props) => ; ================================================ FILE: src/features/app/epics.ts ================================================ import { RootEpic } from 'MyTypes'; import { tap, ignoreElements, filter, first, map } from 'rxjs/operators'; import { isActionOf } from 'typesafe-actions'; import { loadArticlesAsync, createArticleAsync, updateArticleAsync, deleteArticleAsync, } from '../articles/actions'; export const persistArticlesInLocalStorage: RootEpic = ( action$, store, { localStorage } ) => action$.pipe( filter( isActionOf([ loadArticlesAsync.success, createArticleAsync.success, updateArticleAsync.success, deleteArticleAsync.success, ]) ), tap(_ => { // handle side-effects localStorage.set('articles', store.value.articles.articles); }), ignoreElements() ); export const loadDataOnAppStart: RootEpic = (action$, store, { api }) => action$.pipe( first(), map(loadArticlesAsync.request) ); ================================================ FILE: src/features/articles/actions.ts ================================================ import { Article } from 'MyModels'; import { createAsyncAction } from 'typesafe-actions'; export const loadArticlesAsync = createAsyncAction( 'LOAD_ARTICLES_REQUEST', 'LOAD_ARTICLES_SUCCESS', 'LOAD_ARTICLES_FAILURE' )(); export const createArticleAsync = createAsyncAction( 'CREATE_ARTICLE_REQUEST', 'CREATE_ARTICLE_SUCCESS', 'CREATE_ARTICLE_FAILURE' )(); export const updateArticleAsync = createAsyncAction( 'UPDATE_ARTICLE_REQUEST', 'UPDATE_ARTICLE_SUCCESS', 'UPDATE_ARTICLE_FAILURE' )(); export const deleteArticleAsync = createAsyncAction( 'DELETE_ARTICLE_REQUEST', 'DELETE_ARTICLE_SUCCESS', 'DELETE_ARTICLE_FAILURE' )(); ================================================ FILE: src/features/articles/components/ArticleActionsMenu.tsx ================================================ import { RootState } from 'MyTypes'; import React from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { getPath } from '../../../router-paths'; const mapStateToProps = (state: RootState) => ({}); const dispatchProps = {}; type Props = ReturnType & typeof dispatchProps; type State = {}; class ArticleActionsMenu extends React.Component { render() { return (
Create article
); } } export default connect( mapStateToProps, dispatchProps )(ArticleActionsMenu); ================================================ FILE: src/features/articles/components/ArticleForm.tsx ================================================ import React from 'react'; import cuid from 'cuid'; import { Form, FormikProps, Field, withFormik, ErrorMessage } from 'formik'; import { Article } from 'MyModels'; import { compose } from 'redux'; import { connect } from 'react-redux'; import { push } from 'connected-react-router'; import { createArticleAsync, updateArticleAsync } from '../actions'; // import { getPath } from '../../../router-paths'; type FormValues = Pick & {}; const dispatchProps = { createArticle: (values: FormValues) => createArticleAsync.request({ id: cuid(), ...values, }), updateArticle: (values: Article) => updateArticleAsync.request({ ...values, }), redirectToListing: () => push('/'), }; type Props = typeof dispatchProps & { article?: Article; }; const InnerForm: React.FC> = props => { const { isSubmitting, dirty } = props; return (


); }; export default compose( connect( null, dispatchProps ), withFormik({ enableReinitialize: true, // initialize values mapPropsToValues: ({ article: data }) => ({ title: (data && data.title) || '', content: (data && data.content) || '', }), handleSubmit: (values, form) => { if (form.props.article != null) { form.props.updateArticle({ ...form.props.article, ...values }); } else { form.props.createArticle(values); } form.props.redirectToListing(); form.setSubmitting(false); }, }) )(InnerForm); ================================================ FILE: src/features/articles/components/ArticleList.tsx ================================================ import { RootState } from 'MyTypes'; import React from 'react'; import { connect } from 'react-redux'; import * as selectors from '../selectors'; import ArticleListItem from './ArticleListItem'; const mapStateToProps = (state: RootState) => ({ isLoading: state.articles.isLoadingArticles, articles: selectors.getArticles(state), }); const dispatchProps = {}; type Props = ReturnType & typeof dispatchProps; const ArticleList: React.FC = ({ isLoading, articles: articles = [], }) => { if (isLoading) { return

Loading articles...

; } if (articles.length === 0) { return (

No articles yet, please create new...

); } return (
    {articles.map(article => (
  • ))}
); }; const getStyle = (): React.CSSProperties => ({ textAlign: 'left', margin: 'auto', maxWidth: 500, }); export default connect( mapStateToProps, dispatchProps )(ArticleList); ================================================ FILE: src/features/articles/components/ArticleListItem.tsx ================================================ import { Article } from 'MyModels'; import React from 'react'; import areEqual from 'fast-deep-equal'; import { connect } from 'react-redux'; import { deleteArticleAsync } from '../actions'; import { getPath } from '../../../router-paths'; import FlexRow from '../../../components/FlexRow'; import { Link } from 'react-router-dom'; const dispatchProps = { deleteArticle: deleteArticleAsync.request, }; type Props = typeof dispatchProps & { article: Article; }; const ArticleListItem = React.memo(({ article, deleteArticle }) => { return (
{article.title}
View Edit
deleteArticle(article)} style={{ color: 'darkred' }} > Delete
); }, areEqual); const getStyle = (): React.CSSProperties => ({ overflowX: 'hidden', textOverflow: 'ellipsis', width: '300px', }); export default connect( null, dispatchProps )(ArticleListItem); ================================================ FILE: src/features/articles/components/ArticleView.tsx ================================================ import React from 'react'; import { Article } from 'MyModels'; type Props = { article: Article; }; const ArticleView: React.FC = ({ article }) => { return (

{article.title}

{article.content}

); }; export default ArticleView; ================================================ FILE: src/features/articles/epics.ts ================================================ import { RootEpic } from 'MyTypes'; import { from, of } from 'rxjs'; import { filter, switchMap, map, catchError } from 'rxjs/operators'; import { isActionOf } from 'typesafe-actions'; import { loadArticlesAsync, createArticleAsync, updateArticleAsync, deleteArticleAsync, } from './actions'; export const loadArticlesEpic: RootEpic = (action$, state$, { api }) => action$.pipe( filter(isActionOf(loadArticlesAsync.request)), switchMap(() => from(api.articles.loadArticles()).pipe( map(loadArticlesAsync.success), catchError(message => of(loadArticlesAsync.failure(message))) ) ) ); export const createArticlesEpic: RootEpic = (action$, state$, { api }) => action$.pipe( filter(isActionOf(createArticleAsync.request)), switchMap(action => from(api.articles.createArticle(action.payload)).pipe( map(createArticleAsync.success), catchError(message => of(createArticleAsync.failure(message))) ) ) ); export const updateArticlesEpic: RootEpic = (action$, state$, { api }) => action$.pipe( filter(isActionOf(updateArticleAsync.request)), switchMap(action => from(api.articles.updateArticle(action.payload)).pipe( map(updateArticleAsync.success), catchError(message => of(updateArticleAsync.failure(message))) ) ) ); export const deleteArticlesEpic: RootEpic = (action$, state$, { api, toast }) => action$.pipe( filter(isActionOf(deleteArticleAsync.request)), switchMap(action => from(api.articles.deleteArticle(action.payload)).pipe( map(deleteArticleAsync.success), catchError(message => { toast.error(message); return of(deleteArticleAsync.failure(action.payload)); }) ) ) ); ================================================ FILE: src/features/articles/reducer.ts ================================================ import { Article } from 'MyModels'; import { combineReducers } from 'redux'; import { createReducer } from 'typesafe-actions'; import { loadArticlesAsync, createArticleAsync, updateArticleAsync, deleteArticleAsync, } from './actions'; const reducer = combineReducers({ isLoadingArticles: createReducer(false as boolean) .handleAction([loadArticlesAsync.request], (state, action) => true) .handleAction( [loadArticlesAsync.success, loadArticlesAsync.failure], (state, action) => false ), articles: createReducer([] as Article[]) .handleAction( [ loadArticlesAsync.success, createArticleAsync.success, updateArticleAsync.success, deleteArticleAsync.success, ], (state, action) => action.payload ) .handleAction(createArticleAsync.request, (state, action) => [ ...state, action.payload, ]) .handleAction(updateArticleAsync.request, (state, action) => state.map(i => (i.id === action.payload.id ? action.payload : i)) ) .handleAction(deleteArticleAsync.request, (state, action) => state.filter(i => i.id !== action.payload.id) ) .handleAction(deleteArticleAsync.failure, (state, action) => state.concat(action.payload) ), }); export default reducer; ================================================ FILE: src/features/articles/selectors.ts ================================================ import { RootState } from 'MyTypes'; // import { createSelector } from 'reselect'; export const getArticles = (state: RootState) => state.articles.articles; ================================================ FILE: src/features/articles/types.d.ts ================================================ declare module 'MyModels' { export type Article = { id: string; title: string; content: string; }; } ================================================ FILE: src/index.css ================================================ body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } a, .link { color: #61dafb; cursor: pointer; text-decoration: none; } a:hover, .link:hover { text-decoration: underline; } ================================================ FILE: src/index.tsx ================================================ import '@babel/polyfill'; import 'tslib'; import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import * as serviceWorker from './serviceWorker'; import App from './App'; ReactDOM.render(, document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister(); ================================================ FILE: src/layouts/Main.css ================================================ .App { min-width: 500px; } .App-header { background-color: #282c34; min-height: 80px; display: flex; flex-direction: row; align-items: center; justify-content: center; font-size: 24px; color: white; } .App-main { margin: 0 auto; width: 500px; overflow-x: hidden; } .App-logo { animation: App-logo-spin infinite 20s linear; height: 40px; pointer-events: none; } @keyframes App-logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ================================================ FILE: src/layouts/Main.tsx ================================================ import React, { FC } from 'react'; import { Link } from 'react-router-dom'; import './Main.css'; import logo from '../assets/logo.svg'; import FlexRow from '../components/FlexRow'; type Props = { renderActionsMenu?: () => JSX.Element; }; const Main: FC = ({ children, renderActionsMenu }) => (
logo Demo App {renderActionsMenu && renderActionsMenu()}
{children}
); export default Main; ================================================ FILE: src/react-app-env.d.ts ================================================ /// ================================================ FILE: src/router-paths.ts ================================================ const pathsMap = { home: () => '/', addArticle: () => '/add-article', viewArticle: (articleId: string) => `/articles/${articleId}`, editArticle: (articleId: string) => `/articles/${articleId}/edit`, }; type PathsMap = typeof pathsMap; export const getPath = ( route: TRoute, ...params: Parameters ) => { const pathCb: (...args: any[]) => string = pathsMap[route]; return pathCb(...params); }; ================================================ FILE: src/routes/AddArticle.tsx ================================================ import React from 'react'; import ArticleForm from '../features/articles/components/ArticleForm'; import Main from '../layouts/Main'; import BackLink from '../components/BackLink'; export default () => (
}>
); ================================================ FILE: src/routes/EditArticle.tsx ================================================ import { RootState } from 'MyTypes'; import React from 'react'; import { match } from 'react-router'; import ArticleForm from '../features/articles/components/ArticleForm'; import Main from '../layouts/Main'; import BackLink from '../components/BackLink'; import { connect } from 'react-redux'; type OwnProps = { match: match<{ articleId: string }>; }; const mapStateToProps = (state: RootState, ownProps: OwnProps) => ({ article: state.articles.articles.find( i => i.id === ownProps.match.params.articleId ), }); type Props = ReturnType; const EditArticle = ({ article }: Props) => { return (
}>
); }; export default connect(mapStateToProps)(EditArticle); ================================================ FILE: src/routes/Home.tsx ================================================ import React from 'react'; import ArticleList from '../features/articles/components/ArticleList'; import ArticleActionsMenu from '../features/articles/components/ArticleActionsMenu'; import Main from '../layouts/Main'; export default () => (
}>
); ================================================ FILE: src/routes/ViewArticle.tsx ================================================ import { RootState } from 'MyTypes'; import React from 'react'; import { connect } from 'react-redux'; import { match } from 'react-router'; import ArticleView from '../features/articles/components/ArticleView'; import Main from '../layouts/Main'; import BackLink from '../components/BackLink'; type OwnProps = { match: match<{ articleId: string }>; }; const mapStateToProps = (state: RootState, ownProps: OwnProps) => ({ article: state.articles.articles.find( i => i.id === ownProps.match.params.articleId ), }); type Props = ReturnType; const ViewArticle = ({ article }: Props) => { if (!article) { return
'Article doesn\'t exist'
; } return (
}>
); }; export default connect(mapStateToProps)(ViewArticle); ================================================ FILE: src/serviceWorker.ts ================================================ // This optional code is used to register a service worker. // register() is not called by default. // This lets the app load faster on subsequent visits in production, and gives // it offline capabilities. However, it also means that developers (and users) // will only see deployed updates on subsequent visits to a page, after all the // existing tabs open on the page have been closed, since previously cached // resources are updated in the background. // To learn more about the benefits of this model and instructions on how to // opt-in, read https://bit.ly/CRA-PWA const isLocalhost = Boolean( window.location.hostname === 'localhost' || // [::1] is the IPv6 localhost address. window.location.hostname === '[::1]' || // 127.0.0.1/8 is considered localhost for IPv4. window.location.hostname.match( /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ ) ); type Config = { onSuccess?: (registration: ServiceWorkerRegistration) => void; onUpdate?: (registration: ServiceWorkerRegistration) => void; }; export function register(config?: Config) { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { // The URL constructor is available in all browsers that support SW. const publicUrl = new URL( (process as { env: { [key: string]: string } }).env.PUBLIC_URL, window.location.href ); if (publicUrl.origin !== window.location.origin) { // Our service worker won't work if PUBLIC_URL is on a different origin // from what our page is served on. This might happen if a CDN is used to // serve assets; see https://github.com/facebook/create-react-app/issues/2374 return; } window.addEventListener('load', () => { const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; if (isLocalhost) { // This is running on localhost. Let's check if a service worker still exists or not. checkValidServiceWorker(swUrl, config); // Add some additional logging to localhost, pointing developers to the // service worker/PWA documentation. navigator.serviceWorker.ready.then(() => { console.log( 'This web app is being served cache-first by a service ' + 'worker. To learn more, visit https://bit.ly/CRA-PWA' ); }); } else { // Is not localhost. Just register service worker registerValidSW(swUrl, config); } }); } } function registerValidSW(swUrl: string, config?: Config) { navigator.serviceWorker .register(swUrl) .then(registration => { registration.onupdatefound = () => { const installingWorker = registration.installing; if (installingWorker == null) { return; } installingWorker.onstatechange = () => { if (installingWorker.state === 'installed') { if (navigator.serviceWorker.controller) { // At this point, the updated precached content has been fetched, // but the previous service worker will still serve the older // content until all client tabs are closed. console.log( 'New content is available and will be used when all ' + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' ); // Execute callback if (config && config.onUpdate) { config.onUpdate(registration); } } else { // At this point, everything has been precached. // It's the perfect time to display a // "Content is cached for offline use." message. console.log('Content is cached for offline use.'); // Execute callback if (config && config.onSuccess) { config.onSuccess(registration); } } } }; }; }) .catch(error => { console.error('Error during service worker registration:', error); }); } function checkValidServiceWorker(swUrl: string, config?: Config) { // Check if the service worker can be found. If it can't reload the page. fetch(swUrl) .then(response => { // Ensure service worker exists, and that we really are getting a JS file. const contentType = response.headers.get('content-type'); if ( response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1) ) { // No service worker found. Probably a different app. Reload the page. navigator.serviceWorker.ready.then(registration => { registration.unregister().then(() => { window.location.reload(); }); }); } else { // Service worker found. Proceed as normal. registerValidSW(swUrl, config); } }) .catch(() => { console.log( 'No internet connection found. App is running in offline mode.' ); }); } export function unregister() { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(registration => { registration.unregister(); }); } } ================================================ FILE: src/services/articles-api-client.ts ================================================ import { Article } from 'MyModels'; import * as localStorage from './local-storage-service'; let articles: Article[] = localStorage.get('articles') || []; const TIMEOUT = 750; export function loadArticles(): Promise { return new Promise((resolve, reject) => { setTimeout(() => { resolve(articles); }, TIMEOUT); }); } export function createArticle(article: Article): Promise { return new Promise((resolve, reject) => { setTimeout(() => { articles = articles.concat(article); resolve(articles); }, TIMEOUT); }); } export function updateArticle(article: Article): Promise { return new Promise((resolve, reject) => { setTimeout(() => { articles = articles.map(i => (i.id === article.id ? article : i)); resolve(articles); }, TIMEOUT); }); } export function deleteArticle(article: Article): Promise { return new Promise((resolve, reject) => { setTimeout(() => { articles = articles.filter(i => i.id !== article.id); resolve(articles); }, TIMEOUT); }); } ================================================ FILE: src/services/index.ts ================================================ import * as logger from './logger-service'; import * as articles from './articles-api-client'; import * as toast from './toast-service'; import * as localStorage from './local-storage-service'; export default { logger, localStorage, toast, api: { articles, }, }; ================================================ FILE: src/services/local-storage-service.ts ================================================ const version = process.env.APP_VERSION || 0; const PREFIX = `MY_APP_v${version}::`; export function set(key: string, value: T): void { if (!localStorage) { return; } try { const serializedValue = JSON.stringify(value); localStorage.setItem(PREFIX + key, serializedValue); } catch (error) { throw new Error('store serialization failed'); } } export function get(key: string): T | undefined { if (!localStorage) { return; } try { const serializedValue = localStorage.getItem(PREFIX + key); if (serializedValue == null) { return; } return JSON.parse(serializedValue); } catch (error) { throw new Error('store deserialization failed'); } } ================================================ FILE: src/services/logger-service.ts ================================================ // TODO: connect external client logging service here (e.g. Sentry SDK) // tslint:disable-next-line:no-console export default { log: console.log }; ================================================ FILE: src/services/toast-service.ts ================================================ import 'react-toastify/dist/ReactToastify.css'; import { toast } from 'react-toastify'; toast.configure(); const { info, warn, error, success } = toast; export { info, warn, error, success }; ================================================ FILE: src/services/types.d.ts ================================================ declare module 'MyTypes' { export type Services = typeof import('./index').default; } ================================================ FILE: src/store/index.ts ================================================ import { RootAction, RootState, Services } from 'MyTypes'; import { createStore, applyMiddleware } from 'redux'; import { createEpicMiddleware } from 'redux-observable'; import { createBrowserHistory } from 'history'; import { routerMiddleware } from 'connected-react-router'; import { composeEnhancers } from './utils'; import rootReducer from './root-reducer'; import rootEpic from './root-epic'; import services from '../services'; export const epicMiddleware = createEpicMiddleware< RootAction, RootAction, RootState, Services >({ dependencies: services, }); // configure middlewares export const history = createBrowserHistory(); const middlewares = [routerMiddleware(history), epicMiddleware]; // compose enhancers const enhancer = composeEnhancers(applyMiddleware(...middlewares)); // rehydrate state on app start const initialState = {}; // create store const store = createStore(rootReducer(history), initialState, enhancer); epicMiddleware.run(rootEpic); // export store singleton instance export default store; ================================================ FILE: src/store/root-action.ts ================================================ import { routerActions } from 'connected-react-router'; import * as articlesActions from '../features/articles/actions'; export default { router: routerActions, articles: articlesActions, }; ================================================ FILE: src/store/root-epic.ts ================================================ import { combineEpics } from 'redux-observable'; import * as app from '../features/app/epics'; import * as articles from '../features/articles/epics'; export default combineEpics(...Object.values(app), ...Object.values(articles)); ================================================ FILE: src/store/root-reducer.ts ================================================ import { combineReducers } from 'redux'; import { connectRouter } from 'connected-react-router'; import { History } from 'history'; import articles from '../features/articles/reducer'; const rootReducer = (history: History) => combineReducers({ router: connectRouter(history), articles, }); export default rootReducer; ================================================ FILE: src/store/types.d.ts ================================================ import { StateType, ActionType } from 'typesafe-actions'; import { Epic } from 'redux-observable'; declare module 'MyTypes' { export type Store = StateType; export type RootState = StateType< ReturnType >; export type RootAction = ActionType; export type RootEpic = Epic; } declare module 'typesafe-actions' { interface Types { RootAction: ActionType; } } ================================================ FILE: src/store/utils.ts ================================================ import { compose } from 'redux'; export const composeEnhancers = (process.env.NODE_ENV === 'development' && window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose; ================================================ FILE: tsconfig.json ================================================ { "include": ["src", "typings", "lambdas/src"], "exclude": ["src/**/*.spec.*"], "extends": "./node_modules/react-redux-typescript-scripts/tsconfig.json", "compilerOptions": {} } ================================================ FILE: tsconfig.test.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "module": "commonjs" } } ================================================ FILE: typings/augmentations.d.ts ================================================ export {}; // Fix incorrect ALBResult type declare module 'aws-lambda' { export interface ALBResult { statusDescription?: string; isBase64Encoded?: boolean; } } ================================================ FILE: typings/globals.d.ts ================================================ declare interface Window { __REDUX_DEVTOOLS_EXTENSION__: any; __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any; } declare interface NodeModule { hot?: { accept: (path: string, callback: () => void) => void }; } declare interface System { import(module: string): Promise; } declare var System: System; ================================================ FILE: typings/modules.d.ts ================================================ declare module '@emotion/styled/macro' { import styled from '@emotion/styled'; export default styled; }