├── server ├── .env ├── src │ └── env.ts ├── tsconfig.json └── package.json ├── src ├── react-app-env.d.ts ├── services │ ├── types.d.ts │ ├── logger-service.ts │ ├── toast-service.ts │ ├── index.ts │ ├── local-storage-service.ts │ └── articles-api-client.ts ├── features │ ├── articles │ │ ├── types.d.ts │ │ ├── selectors.ts │ │ ├── components │ │ │ ├── ArticleView.tsx │ │ │ ├── ArticleActionsMenu.tsx │ │ │ ├── ArticleList.tsx │ │ │ ├── ArticleListItem.tsx │ │ │ └── ArticleForm.tsx │ │ ├── actions.ts │ │ ├── reducer.ts │ │ └── epics.ts │ └── app │ │ └── epics.ts ├── store │ ├── utils.ts │ ├── root-action.ts │ ├── root-epic.ts │ ├── root-reducer.ts │ ├── types.d.ts │ └── index.ts ├── App.test.tsx ├── components │ ├── FlexRow.tsx │ ├── FlexColumn.tsx │ ├── BackLink.tsx │ └── FlexBox.tsx ├── routes │ ├── AddArticle.tsx │ ├── Home.tsx │ ├── EditArticle.tsx │ └── ViewArticle.tsx ├── router-paths.ts ├── index.tsx ├── index.css ├── layouts │ ├── Main.css │ └── Main.tsx ├── App.tsx ├── assets │ └── logo.svg └── serviceWorker.ts ├── .vscode └── settings.json ├── netlify.toml ├── .prettierrc ├── public ├── favicon.ico ├── manifest.json └── index.html ├── tsconfig.test.json ├── .eslintrc ├── typings ├── modules.d.ts ├── augmentations.d.ts └── globals.d.ts ├── tsconfig.json ├── lambdas ├── .babelrc ├── src │ └── hello.ts └── build │ └── hello.js ├── .gitignore ├── README.md ├── package.json └── CODE_OF_CONDUCT.md /server/.env: -------------------------------------------------------------------------------- 1 | DB_HOST=localhost 2 | -------------------------------------------------------------------------------- /server/src/env.ts: -------------------------------------------------------------------------------- 1 | export const DB_HOST = process.env.DB_HOST; 2 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "lambdas/build" # netlify-lambda reads this for local dev server 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrwitek/react-redux-typescript-realworld-app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/services/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'MyTypes' { 2 | export type Services = typeof import('./index').default; 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "react-app", 4 | "./node_modules/react-redux-typescript-scripts/eslint.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /typings/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@emotion/styled/macro' { 2 | import styled from '@emotion/styled'; 3 | export default styled; 4 | } 5 | -------------------------------------------------------------------------------- /src/features/articles/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'MyModels' { 2 | export type Article = { 3 | id: string; 4 | title: string; 5 | content: string; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/services/logger-service.ts: -------------------------------------------------------------------------------- 1 | // TODO: connect external client logging service here (e.g. Sentry SDK) 2 | // tslint:disable-next-line:no-console 3 | export default { log: console.log }; 4 | -------------------------------------------------------------------------------- /src/features/articles/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from 'MyTypes'; 2 | // import { createSelector } from 'reselect'; 3 | 4 | export const getArticles = (state: RootState) => state.articles.articles; 5 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "typings"], 3 | "exclude": ["src/**/*.spec.*"], 4 | "extends": "./node_modules/react-redux-typescript-scripts/tsconfig.json", 5 | "compilerOptions": {} 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "typings", "lambdas/src"], 3 | "exclude": ["src/**/*.spec.*"], 4 | "extends": "./node_modules/react-redux-typescript-scripts/tsconfig.json", 5 | "compilerOptions": {} 6 | } 7 | -------------------------------------------------------------------------------- /src/store/utils.ts: -------------------------------------------------------------------------------- 1 | import { compose } from 'redux'; 2 | 3 | export const composeEnhancers = 4 | (process.env.NODE_ENV === 'development' && 5 | window && 6 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || 7 | compose; 8 | -------------------------------------------------------------------------------- /src/store/root-action.ts: -------------------------------------------------------------------------------- 1 | import { routerActions } from 'connected-react-router'; 2 | import * as articlesActions from '../features/articles/actions'; 3 | 4 | export default { 5 | router: routerActions, 6 | articles: articlesActions, 7 | }; 8 | -------------------------------------------------------------------------------- /typings/augmentations.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | // Fix incorrect ALBResult type 4 | declare module 'aws-lambda' { 5 | export interface ALBResult { 6 | statusDescription?: string; 7 | isBase64Encoded?: boolean; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/services/toast-service.ts: -------------------------------------------------------------------------------- 1 | import 'react-toastify/dist/ReactToastify.css'; 2 | import { toast } from 'react-toastify'; 3 | toast.configure(); 4 | 5 | const { info, warn, error, success } = toast; 6 | 7 | export { info, warn, error, success }; 8 | -------------------------------------------------------------------------------- /src/store/root-epic.ts: -------------------------------------------------------------------------------- 1 | import { combineEpics } from 'redux-observable'; 2 | 3 | import * as app from '../features/app/epics'; 4 | import * as articles from '../features/articles/epics'; 5 | 6 | export default combineEpics(...Object.values(app), ...Object.values(articles)); 7 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /lambdas/.babelrc: -------------------------------------------------------------------------------- 1 | // lambda build config 2 | { 3 | "presets": ["@babel/preset-typescript", "@babel/preset-env"], 4 | "plugins": [ 5 | "@babel/transform-runtime", 6 | "@babel/plugin-proposal-class-properties", 7 | "@babel/plugin-transform-object-assign", 8 | "@babel/plugin-proposal-object-rest-spread" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/components/FlexRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CSSObject } from '@emotion/core'; 3 | 4 | import FlexBox from './FlexBox'; 5 | 6 | type Props = React.ComponentProps & { 7 | direction?: CSSObject['flexDirection']; 8 | }; 9 | 10 | export default (props: Props) => ; 11 | -------------------------------------------------------------------------------- /src/components/FlexColumn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CSSObject } from '@emotion/core'; 3 | 4 | import FlexBox from './FlexBox'; 5 | 6 | type Props = React.ComponentProps & { 7 | direction?: CSSObject['flexDirection']; 8 | }; 9 | 10 | export default (props: Props) => ; 11 | -------------------------------------------------------------------------------- /src/routes/AddArticle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ArticleForm from '../features/articles/components/ArticleForm'; 4 | import Main from '../layouts/Main'; 5 | import BackLink from '../components/BackLink'; 6 | 7 | export default () => ( 8 |
}> 9 | 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import * as logger from './logger-service'; 2 | import * as articles from './articles-api-client'; 3 | import * as toast from './toast-service'; 4 | import * as localStorage from './local-storage-service'; 5 | 6 | export default { 7 | logger, 8 | localStorage, 9 | toast, 10 | api: { 11 | articles, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/BackLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import areEqual from 'fast-deep-equal'; 3 | 4 | import { Link } from 'react-router-dom'; 5 | 6 | interface Props {} 7 | 8 | const BackLink = React.memo(() => { 9 | return ( 10 | 11 | {'< Back'} 12 | 13 | ); 14 | }, areEqual); 15 | 16 | export default BackLink; 17 | -------------------------------------------------------------------------------- /typings/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | __REDUX_DEVTOOLS_EXTENSION__: any; 3 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any; 4 | } 5 | 6 | declare interface NodeModule { 7 | hot?: { accept: (path: string, callback: () => void) => void }; 8 | } 9 | 10 | declare interface System { 11 | import(module: string): Promise; 12 | } 13 | declare var System: System; 14 | -------------------------------------------------------------------------------- /src/routes/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ArticleList from '../features/articles/components/ArticleList'; 4 | import ArticleActionsMenu from '../features/articles/components/ArticleActionsMenu'; 5 | import Main from '../layouts/Main'; 6 | 7 | export default () => ( 8 |
}> 9 | 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /src/store/root-reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { connectRouter } from 'connected-react-router'; 3 | import { History } from 'history'; 4 | 5 | import articles from '../features/articles/reducer'; 6 | 7 | const rootReducer = (history: History) => 8 | combineReducers({ 9 | router: connectRouter(history), 10 | articles, 11 | }); 12 | 13 | export default rootReducer; 14 | -------------------------------------------------------------------------------- /src/features/articles/components/ArticleView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Article } from 'MyModels'; 4 | 5 | type Props = { 6 | article: Article; 7 | }; 8 | 9 | const ArticleView: React.FC = ({ article }) => { 10 | return ( 11 |
12 |

{article.title}

13 | 14 |

{article.content}

15 |
16 | ); 17 | }; 18 | 19 | export default ArticleView; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /server/node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | /server/build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /src/router-paths.ts: -------------------------------------------------------------------------------- 1 | const pathsMap = { 2 | home: () => '/', 3 | addArticle: () => '/add-article', 4 | viewArticle: (articleId: string) => `/articles/${articleId}`, 5 | editArticle: (articleId: string) => `/articles/${articleId}/edit`, 6 | }; 7 | type PathsMap = typeof pathsMap; 8 | 9 | export const getPath = ( 10 | route: TRoute, 11 | ...params: Parameters 12 | ) => { 13 | const pathCb: (...args: any[]) => string = pathsMap[route]; 14 | 15 | return pathCb(...params); 16 | }; 17 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "...", 10 | "build": "...", 11 | "deploy": "openode deploy" 12 | }, 13 | "author": "Piotrek Witek", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "openode": "2.0.3", 17 | "react-redux-typescript-scripts": "1.5.0", 18 | "typescript": "3.4.5" 19 | }, 20 | "dependencies": {} 21 | } 22 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill'; 2 | import 'tslib'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import './index.css'; 7 | import * as serviceWorker from './serviceWorker'; 8 | import App from './App'; 9 | 10 | ReactDOM.render(, document.getElementById('root')); 11 | 12 | // If you want your app to work offline and load faster, you can change 13 | // unregister() to register() below. Note this comes with some pitfalls. 14 | // Learn more about service workers: https://bit.ly/CRA-PWA 15 | serviceWorker.unregister(); 16 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | 16 | a, 17 | .link { 18 | color: #61dafb; 19 | cursor: pointer; 20 | text-decoration: none; 21 | } 22 | 23 | a:hover, 24 | .link:hover { 25 | text-decoration: underline; 26 | } 27 | -------------------------------------------------------------------------------- /src/store/types.d.ts: -------------------------------------------------------------------------------- 1 | import { StateType, ActionType } from 'typesafe-actions'; 2 | import { Epic } from 'redux-observable'; 3 | 4 | declare module 'MyTypes' { 5 | export type Store = StateType; 6 | export type RootState = StateType< 7 | ReturnType 8 | >; 9 | export type RootAction = ActionType; 10 | 11 | export type RootEpic = Epic; 12 | } 13 | 14 | declare module 'typesafe-actions' { 15 | interface Types { 16 | RootAction: ActionType; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/layouts/Main.css: -------------------------------------------------------------------------------- 1 | .App { 2 | min-width: 500px; 3 | } 4 | 5 | .App-header { 6 | background-color: #282c34; 7 | min-height: 80px; 8 | display: flex; 9 | flex-direction: row; 10 | align-items: center; 11 | justify-content: center; 12 | font-size: 24px; 13 | color: white; 14 | } 15 | 16 | .App-main { 17 | margin: 0 auto; 18 | width: 500px; 19 | overflow-x: hidden; 20 | } 21 | 22 | .App-logo { 23 | animation: App-logo-spin infinite 20s linear; 24 | height: 40px; 25 | pointer-events: none; 26 | } 27 | 28 | @keyframes App-logo-spin { 29 | from { 30 | transform: rotate(0deg); 31 | } 32 | to { 33 | transform: rotate(360deg); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lambdas/src/hello.ts: -------------------------------------------------------------------------------- 1 | import { ALBHandler } from 'aws-lambda'; 2 | import querystring from 'querystring'; 3 | 4 | export const handler: ALBHandler = async (event, context) => { 5 | switch (event.httpMethod) { 6 | case 'GET': { 7 | return { 8 | statusCode: 200, 9 | body: `Hello, World!`, 10 | }; 11 | } 12 | 13 | case 'POST': { 14 | const params = querystring.parse(event.body!); 15 | const name = params.name || 'World!'; 16 | 17 | return { 18 | statusCode: 200, 19 | body: `Hello, ${name}`, 20 | }; 21 | } 22 | 23 | default: 24 | return { statusCode: 405, body: 'Method Not Allowed' }; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/features/articles/components/ArticleActionsMenu.tsx: -------------------------------------------------------------------------------- 1 | import { RootState } from 'MyTypes'; 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | import { getPath } from '../../../router-paths'; 7 | 8 | const mapStateToProps = (state: RootState) => ({}); 9 | const dispatchProps = {}; 10 | 11 | type Props = ReturnType & typeof dispatchProps; 12 | 13 | type State = {}; 14 | 15 | class ArticleActionsMenu extends React.Component { 16 | render() { 17 | return ( 18 |
19 | Create article 20 |
21 | ); 22 | } 23 | } 24 | 25 | export default connect( 26 | mapStateToProps, 27 | dispatchProps 28 | )(ArticleActionsMenu); 29 | -------------------------------------------------------------------------------- /src/services/local-storage-service.ts: -------------------------------------------------------------------------------- 1 | const version = process.env.APP_VERSION || 0; 2 | const PREFIX = `MY_APP_v${version}::`; 3 | 4 | export function set(key: string, value: T): void { 5 | if (!localStorage) { 6 | return; 7 | } 8 | 9 | try { 10 | const serializedValue = JSON.stringify(value); 11 | localStorage.setItem(PREFIX + key, serializedValue); 12 | } catch (error) { 13 | throw new Error('store serialization failed'); 14 | } 15 | } 16 | 17 | export function get(key: string): T | undefined { 18 | if (!localStorage) { 19 | return; 20 | } 21 | 22 | try { 23 | const serializedValue = localStorage.getItem(PREFIX + key); 24 | if (serializedValue == null) { 25 | return; 26 | } 27 | return JSON.parse(serializedValue); 28 | } catch (error) { 29 | throw new Error('store deserialization failed'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/features/articles/actions.ts: -------------------------------------------------------------------------------- 1 | import { Article } from 'MyModels'; 2 | import { createAsyncAction } from 'typesafe-actions'; 3 | 4 | export const loadArticlesAsync = createAsyncAction( 5 | 'LOAD_ARTICLES_REQUEST', 6 | 'LOAD_ARTICLES_SUCCESS', 7 | 'LOAD_ARTICLES_FAILURE' 8 | )(); 9 | 10 | export const createArticleAsync = createAsyncAction( 11 | 'CREATE_ARTICLE_REQUEST', 12 | 'CREATE_ARTICLE_SUCCESS', 13 | 'CREATE_ARTICLE_FAILURE' 14 | )(); 15 | 16 | export const updateArticleAsync = createAsyncAction( 17 | 'UPDATE_ARTICLE_REQUEST', 18 | 'UPDATE_ARTICLE_SUCCESS', 19 | 'UPDATE_ARTICLE_FAILURE' 20 | )(); 21 | 22 | export const deleteArticleAsync = createAsyncAction( 23 | 'DELETE_ARTICLE_REQUEST', 24 | 'DELETE_ARTICLE_SUCCESS', 25 | 'DELETE_ARTICLE_FAILURE' 26 | )(); 27 | -------------------------------------------------------------------------------- /src/routes/EditArticle.tsx: -------------------------------------------------------------------------------- 1 | import { RootState } from 'MyTypes'; 2 | import React from 'react'; 3 | import { match } from 'react-router'; 4 | 5 | import ArticleForm from '../features/articles/components/ArticleForm'; 6 | import Main from '../layouts/Main'; 7 | import BackLink from '../components/BackLink'; 8 | import { connect } from 'react-redux'; 9 | 10 | type OwnProps = { 11 | match: match<{ articleId: string }>; 12 | }; 13 | 14 | const mapStateToProps = (state: RootState, ownProps: OwnProps) => ({ 15 | article: state.articles.articles.find( 16 | i => i.id === ownProps.match.params.articleId 17 | ), 18 | }); 19 | 20 | type Props = ReturnType; 21 | 22 | const EditArticle = ({ article }: Props) => { 23 | return ( 24 |
}> 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default connect(mapStateToProps)(EditArticle); 31 | -------------------------------------------------------------------------------- /src/layouts/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import './Main.css'; 5 | import logo from '../assets/logo.svg'; 6 | import FlexRow from '../components/FlexRow'; 7 | 8 | type Props = { 9 | renderActionsMenu?: () => JSX.Element; 10 | }; 11 | 12 | const Main: FC = ({ children, renderActionsMenu }) => ( 13 |
14 |
15 | 21 | 22 | logo 23 | 24 | Demo App 25 | 26 | 27 | {renderActionsMenu && renderActionsMenu()} 28 | 29 |
30 |
{children}
31 |
32 | ); 33 | 34 | export default Main; 35 | -------------------------------------------------------------------------------- /src/routes/ViewArticle.tsx: -------------------------------------------------------------------------------- 1 | import { RootState } from 'MyTypes'; 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { match } from 'react-router'; 5 | 6 | import ArticleView from '../features/articles/components/ArticleView'; 7 | import Main from '../layouts/Main'; 8 | import BackLink from '../components/BackLink'; 9 | 10 | type OwnProps = { 11 | match: match<{ articleId: string }>; 12 | }; 13 | 14 | const mapStateToProps = (state: RootState, ownProps: OwnProps) => ({ 15 | article: state.articles.articles.find( 16 | i => i.id === ownProps.match.params.articleId 17 | ), 18 | }); 19 | 20 | type Props = ReturnType; 21 | 22 | const ViewArticle = ({ article }: Props) => { 23 | if (!article) { 24 | return
'Article doesn\'t exist'
; 25 | } 26 | 27 | return ( 28 |
}> 29 | 30 |
31 | ); 32 | }; 33 | 34 | export default connect(mapStateToProps)(ViewArticle); 35 | -------------------------------------------------------------------------------- /src/features/app/epics.ts: -------------------------------------------------------------------------------- 1 | import { RootEpic } from 'MyTypes'; 2 | import { tap, ignoreElements, filter, first, map } from 'rxjs/operators'; 3 | 4 | import { isActionOf } from 'typesafe-actions'; 5 | import { 6 | loadArticlesAsync, 7 | createArticleAsync, 8 | updateArticleAsync, 9 | deleteArticleAsync, 10 | } from '../articles/actions'; 11 | 12 | export const persistArticlesInLocalStorage: RootEpic = ( 13 | action$, 14 | store, 15 | { localStorage } 16 | ) => 17 | action$.pipe( 18 | filter( 19 | isActionOf([ 20 | loadArticlesAsync.success, 21 | createArticleAsync.success, 22 | updateArticleAsync.success, 23 | deleteArticleAsync.success, 24 | ]) 25 | ), 26 | tap(_ => { 27 | // handle side-effects 28 | localStorage.set('articles', store.value.articles.articles); 29 | }), 30 | ignoreElements() 31 | ); 32 | 33 | export const loadDataOnAppStart: RootEpic = (action$, store, { api }) => 34 | action$.pipe( 35 | first(), 36 | map(loadArticlesAsync.request) 37 | ); 38 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { RootAction, RootState, Services } from 'MyTypes'; 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import { createEpicMiddleware } from 'redux-observable'; 4 | import { createBrowserHistory } from 'history'; 5 | import { routerMiddleware } from 'connected-react-router'; 6 | 7 | import { composeEnhancers } from './utils'; 8 | import rootReducer from './root-reducer'; 9 | import rootEpic from './root-epic'; 10 | import services from '../services'; 11 | 12 | export const epicMiddleware = createEpicMiddleware< 13 | RootAction, 14 | RootAction, 15 | RootState, 16 | Services 17 | >({ 18 | dependencies: services, 19 | }); 20 | 21 | // configure middlewares 22 | export const history = createBrowserHistory(); 23 | const middlewares = [routerMiddleware(history), epicMiddleware]; 24 | // compose enhancers 25 | const enhancer = composeEnhancers(applyMiddleware(...middlewares)); 26 | 27 | // rehydrate state on app start 28 | const initialState = {}; 29 | 30 | // create store 31 | const store = createStore(rootReducer(history), initialState, enhancer); 32 | 33 | epicMiddleware.run(rootEpic); 34 | 35 | // export store singleton instance 36 | export default store; 37 | -------------------------------------------------------------------------------- /src/components/FlexBox.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled/macro'; 2 | import { CSSObject } from '@emotion/core'; 3 | 4 | type Props = { 5 | className?: string; 6 | style?: React.CSSProperties; 7 | /* @description will add spacing between children, work dependinng on row/column layout */ 8 | itemsSpacing?: number; 9 | direction?: CSSObject['flexDirection']; 10 | wrap?: CSSObject['flexWrap']; 11 | justify?: CSSObject['justifyContent']; 12 | align?: CSSObject['alignItems']; 13 | grow?: CSSObject['flexGrow']; 14 | shrink?: CSSObject['flexShrink']; 15 | }; 16 | 17 | const FlexBox = styled('div')( 18 | ({ 19 | itemsSpacing, 20 | direction: flexDirection, 21 | justify: justifyContent, 22 | wrap: flexWrap, 23 | align: alignItems, 24 | grow: flexGrow, 25 | shrink: flexShrink, 26 | }) => ({ 27 | display: 'flex', 28 | ...(itemsSpacing != null && { 29 | '> * + *': { 30 | [flexDirection === 'row' ? 'marginLeft' : 'marginTop']: itemsSpacing, 31 | }, 32 | }), 33 | flexDirection, 34 | flexWrap, 35 | justifyContent, 36 | alignItems, 37 | flexGrow, 38 | flexShrink, 39 | }) 40 | ); 41 | 42 | export default FlexBox as React.FC; 43 | -------------------------------------------------------------------------------- /src/services/articles-api-client.ts: -------------------------------------------------------------------------------- 1 | import { Article } from 'MyModels'; 2 | 3 | import * as localStorage from './local-storage-service'; 4 | 5 | let articles: Article[] = localStorage.get('articles') || []; 6 | 7 | const TIMEOUT = 750; 8 | 9 | export function loadArticles(): Promise { 10 | return new Promise((resolve, reject) => { 11 | setTimeout(() => { 12 | resolve(articles); 13 | }, TIMEOUT); 14 | }); 15 | } 16 | 17 | export function createArticle(article: Article): Promise { 18 | return new Promise((resolve, reject) => { 19 | setTimeout(() => { 20 | articles = articles.concat(article); 21 | resolve(articles); 22 | }, TIMEOUT); 23 | }); 24 | } 25 | 26 | export function updateArticle(article: Article): Promise { 27 | return new Promise((resolve, reject) => { 28 | setTimeout(() => { 29 | articles = articles.map(i => (i.id === article.id ? article : i)); 30 | resolve(articles); 31 | }, TIMEOUT); 32 | }); 33 | } 34 | 35 | export function deleteArticle(article: Article): Promise { 36 | return new Promise((resolve, reject) => { 37 | setTimeout(() => { 38 | articles = articles.filter(i => i.id !== article.id); 39 | resolve(articles); 40 | }, TIMEOUT); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { ConnectedRouter } from 'connected-react-router'; 4 | import { Switch, Route } from 'react-router'; 5 | 6 | import store, { history } from './store'; 7 | import Home from './routes/Home'; 8 | import { getPath } from './router-paths'; 9 | import AddArticle from './routes/AddArticle'; 10 | import EditArticle from './routes/EditArticle'; 11 | import ViewArticle from './routes/ViewArticle'; 12 | 13 | class App extends Component { 14 | render() { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | } 25 | /> 26 | } 30 | /> 31 |
Page not found!
} /> 32 |
33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /src/features/articles/components/ArticleList.tsx: -------------------------------------------------------------------------------- 1 | import { RootState } from 'MyTypes'; 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | 5 | import * as selectors from '../selectors'; 6 | 7 | import ArticleListItem from './ArticleListItem'; 8 | 9 | const mapStateToProps = (state: RootState) => ({ 10 | isLoading: state.articles.isLoadingArticles, 11 | articles: selectors.getArticles(state), 12 | }); 13 | const dispatchProps = {}; 14 | 15 | type Props = ReturnType & typeof dispatchProps; 16 | 17 | const ArticleList: React.FC = ({ 18 | isLoading, 19 | articles: articles = [], 20 | }) => { 21 | if (isLoading) { 22 | return

Loading articles...

; 23 | } 24 | 25 | if (articles.length === 0) { 26 | return ( 27 |

28 | No articles yet, please create new... 29 |

30 | ); 31 | } 32 | 33 | return ( 34 |
    35 | {articles.map(article => ( 36 |
  • 37 | 38 |
  • 39 | ))} 40 |
41 | ); 42 | }; 43 | 44 | const getStyle = (): React.CSSProperties => ({ 45 | textAlign: 'left', 46 | margin: 'auto', 47 | maxWidth: 500, 48 | }); 49 | 50 | export default connect( 51 | mapStateToProps, 52 | dispatchProps 53 | )(ArticleList); 54 | -------------------------------------------------------------------------------- /src/features/articles/components/ArticleListItem.tsx: -------------------------------------------------------------------------------- 1 | import { Article } from 'MyModels'; 2 | import React from 'react'; 3 | import areEqual from 'fast-deep-equal'; 4 | import { connect } from 'react-redux'; 5 | 6 | import { deleteArticleAsync } from '../actions'; 7 | import { getPath } from '../../../router-paths'; 8 | import FlexRow from '../../../components/FlexRow'; 9 | import { Link } from 'react-router-dom'; 10 | 11 | const dispatchProps = { 12 | deleteArticle: deleteArticleAsync.request, 13 | }; 14 | 15 | type Props = typeof dispatchProps & { 16 | article: Article; 17 | }; 18 | 19 | const ArticleListItem = React.memo(({ article, deleteArticle }) => { 20 | return ( 21 | 22 |
{article.title}
23 | 24 | View 25 | Edit 26 |
deleteArticle(article)} 29 | style={{ color: 'darkred' }} 30 | > 31 | Delete 32 |
33 |
34 |
35 | ); 36 | }, areEqual); 37 | 38 | const getStyle = (): React.CSSProperties => ({ 39 | overflowX: 'hidden', 40 | textOverflow: 'ellipsis', 41 | width: '300px', 42 | }); 43 | 44 | export default connect( 45 | null, 46 | dispatchProps 47 | )(ArticleListItem); 48 | -------------------------------------------------------------------------------- /src/features/articles/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Article } from 'MyModels'; 2 | import { combineReducers } from 'redux'; 3 | import { createReducer } from 'typesafe-actions'; 4 | 5 | import { 6 | loadArticlesAsync, 7 | createArticleAsync, 8 | updateArticleAsync, 9 | deleteArticleAsync, 10 | } from './actions'; 11 | 12 | const reducer = combineReducers({ 13 | isLoadingArticles: createReducer(false as boolean) 14 | .handleAction([loadArticlesAsync.request], (state, action) => true) 15 | .handleAction( 16 | [loadArticlesAsync.success, loadArticlesAsync.failure], 17 | (state, action) => false 18 | ), 19 | articles: createReducer([] as Article[]) 20 | .handleAction( 21 | [ 22 | loadArticlesAsync.success, 23 | createArticleAsync.success, 24 | updateArticleAsync.success, 25 | deleteArticleAsync.success, 26 | ], 27 | (state, action) => action.payload 28 | ) 29 | .handleAction(createArticleAsync.request, (state, action) => [ 30 | ...state, 31 | action.payload, 32 | ]) 33 | .handleAction(updateArticleAsync.request, (state, action) => 34 | state.map(i => (i.id === action.payload.id ? action.payload : i)) 35 | ) 36 | .handleAction(deleteArticleAsync.request, (state, action) => 37 | state.filter(i => i.id !== action.payload.id) 38 | ) 39 | .handleAction(deleteArticleAsync.failure, (state, action) => 40 | state.concat(action.payload) 41 | ), 42 | }); 43 | 44 | export default reducer; 45 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/features/articles/epics.ts: -------------------------------------------------------------------------------- 1 | import { RootEpic } from 'MyTypes'; 2 | import { from, of } from 'rxjs'; 3 | import { filter, switchMap, map, catchError } from 'rxjs/operators'; 4 | import { isActionOf } from 'typesafe-actions'; 5 | 6 | import { 7 | loadArticlesAsync, 8 | createArticleAsync, 9 | updateArticleAsync, 10 | deleteArticleAsync, 11 | } from './actions'; 12 | 13 | export const loadArticlesEpic: RootEpic = (action$, state$, { api }) => 14 | action$.pipe( 15 | filter(isActionOf(loadArticlesAsync.request)), 16 | switchMap(() => 17 | from(api.articles.loadArticles()).pipe( 18 | map(loadArticlesAsync.success), 19 | catchError(message => of(loadArticlesAsync.failure(message))) 20 | ) 21 | ) 22 | ); 23 | 24 | export const createArticlesEpic: RootEpic = (action$, state$, { api }) => 25 | action$.pipe( 26 | filter(isActionOf(createArticleAsync.request)), 27 | switchMap(action => 28 | from(api.articles.createArticle(action.payload)).pipe( 29 | map(createArticleAsync.success), 30 | catchError(message => of(createArticleAsync.failure(message))) 31 | ) 32 | ) 33 | ); 34 | 35 | export const updateArticlesEpic: RootEpic = (action$, state$, { api }) => 36 | action$.pipe( 37 | filter(isActionOf(updateArticleAsync.request)), 38 | switchMap(action => 39 | from(api.articles.updateArticle(action.payload)).pipe( 40 | map(updateArticleAsync.success), 41 | catchError(message => of(updateArticleAsync.failure(message))) 42 | ) 43 | ) 44 | ); 45 | 46 | export const deleteArticlesEpic: RootEpic = (action$, state$, { api, toast }) => 47 | action$.pipe( 48 | filter(isActionOf(deleteArticleAsync.request)), 49 | switchMap(action => 50 | from(api.articles.deleteArticle(action.payload)).pipe( 51 | map(deleteArticleAsync.success), 52 | catchError(message => { 53 | toast.error(message); 54 | return of(deleteArticleAsync.failure(action.payload)); 55 | }) 56 | ) 57 | ) 58 | ); 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # React, Redux, TypeScript - RealWorld App 4 | 5 | ## 🚧🚧🚧 UNDER CONSTRUCTION 🚧🚧🚧 6 | 7 | ### **LIVE DEMO: [LINK](https://react-redux-typescript-realworld-app.netlify.com/)** 8 | 9 | _Reference implementation of RealWorld [JAMStack](https://jamstack.org/) Application based on ["React, Redux, TypeScript Guide"](https://github.com/piotrwitek/react-redux-typescript-guide) 10 | and [Create React App v3.0](https://facebook.github.io/create-react-app/)._ 11 | 12 |
13 | 14 | --- 15 | 16 | ## Features Roadmap: 17 | - [x] Routing with React-Router 18 | - [ ] User Identity 19 | - [ ] External providers (Google, Github, Bitbucket) 20 | - [ ] Registration / Authentication 21 | - [x] Cross-cutting Application Services 22 | - [x] Local Storage 23 | - [x] Client Logger 24 | - [x] Toasts 25 | - [ ] Analytics 26 | - [x] Feature Folders 27 | - [x] `/articles` - Articles listing with CRUD Operations 28 | - [ ] `/realtime-monitoring` - Realtime monitoring of connected users using Websockets 29 | - [x] REST API Integration (API Client) 30 | - [ ] WebSockets Integration 31 | - [ ] Serverless Lambda Functions (Netlify Functions) 32 | - [ ] Utilities (HOC, Hooks, Media Queries...) 33 | - [ ] Typesafe Styling/Theming with CSSinJS (`Emotion`) 34 | - [ ] ... 35 | 36 | --- 37 | 38 | ## Available Scripts 39 | 40 | ### `npm start` 41 | 42 | Runs the app in the development modeat [http://localhost:3000](http://localhost:3000) 43 | 44 | ### `npm test` 45 | 46 | Launches the test runner in the interactive watch mode.
47 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 48 | 49 | ### `npm run build` 50 | 51 | Builds the app for production to the `build` folder.
52 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 53 | 54 | ## Learn More 55 | 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). 56 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/features/articles/components/ArticleForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cuid from 'cuid'; 3 | import { Form, FormikProps, Field, withFormik, ErrorMessage } from 'formik'; 4 | import { Article } from 'MyModels'; 5 | import { compose } from 'redux'; 6 | import { connect } from 'react-redux'; 7 | import { push } from 'connected-react-router'; 8 | 9 | import { createArticleAsync, updateArticleAsync } from '../actions'; 10 | // import { getPath } from '../../../router-paths'; 11 | 12 | type FormValues = Pick & {}; 13 | 14 | const dispatchProps = { 15 | createArticle: (values: FormValues) => 16 | createArticleAsync.request({ 17 | id: cuid(), 18 | ...values, 19 | }), 20 | updateArticle: (values: Article) => 21 | updateArticleAsync.request({ 22 | ...values, 23 | }), 24 | redirectToListing: () => push('/'), 25 | }; 26 | 27 | type Props = typeof dispatchProps & { 28 | article?: Article; 29 | }; 30 | 31 | const InnerForm: React.FC> = props => { 32 | const { isSubmitting, dirty } = props; 33 | return ( 34 |
35 |
36 | 37 |
38 | 46 | 47 |
48 | 49 |
50 | 51 |
52 | 59 | 60 |
61 | 62 | 65 |
66 | ); 67 | }; 68 | 69 | export default compose( 70 | connect( 71 | null, 72 | dispatchProps 73 | ), 74 | withFormik({ 75 | enableReinitialize: true, 76 | // initialize values 77 | mapPropsToValues: ({ article: data }) => ({ 78 | title: (data && data.title) || '', 79 | content: (data && data.content) || '', 80 | }), 81 | handleSubmit: (values, form) => { 82 | if (form.props.article != null) { 83 | form.props.updateArticle({ ...form.props.article, ...values }); 84 | } else { 85 | form.props.createArticle(values); 86 | } 87 | 88 | form.props.redirectToListing(); 89 | form.setSubmitting(false); 90 | }, 91 | }) 92 | )(InnerForm); 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-typescript-realworld-app", 3 | "description": "RealWorld App implementation based on \"react-redux-typescript-guide\"", 4 | "version": "0.1.0", 5 | "private": true, 6 | "author": "Piotr Witek (http://piotrwitek.github.io/)", 7 | "repository": "https://github.com/piotrwitek/react-redux-typescript-realworld-app.git", 8 | "homepage": "https://react-redux-typescript-realworld-app.netlify.com/", 9 | "license": "MIT", 10 | "main": "src/index.tsx", 11 | "scripts": { 12 | "start:client": "react-scripts start", 13 | "start:lambdas": "netlify-lambda serve lambdas/src", 14 | "start": "concurrently 'npm run start:client' 'npm run start:lambdas'", 15 | "build:client": "react-scripts build", 16 | "build:lambdas": "netlify-lambda build lambdas/src", 17 | "build": "concurrently 'npm run build:client' 'npm run build:lambdas'", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject", 20 | "reinstall": "rm -rf ./node_modules && npm install", 21 | "ci-check": "npm run prettier && npm run tsc && npm run test", 22 | "prettier": "prettier --list-different 'src/**/*.ts' || (echo '\nPlease run the following command to fix:\nnpm run prettier:fix\n'; exit 1)", 23 | "prettier:fix": "prettier --write 'src/**/*.ts'", 24 | "tsc": "tsc -p ./ --noEmit", 25 | "tsc:watch": "tsc -p ./ --noEmit -w", 26 | "deploy": "openode deploy" 27 | }, 28 | "dependencies": { 29 | "@babel/polyfill": "7.4.3", 30 | "@emotion/core": "10.0.10", 31 | "@emotion/styled": "10.0.10", 32 | "@types/aws-lambda": "8.10.24", 33 | "@types/jest": "24.0.11", 34 | "@types/node": "11.13.7", 35 | "@types/prop-types": "15.7.1", 36 | "@types/react": "16.8.14", 37 | "@types/react-dom": "16.8.4", 38 | "@types/react-redux": "7.0.8", 39 | "@types/react-router-dom": "4.3.2", 40 | "axios": "0.18.0", 41 | "connected-react-router": "6.4.0", 42 | "cuid": "2.1.6", 43 | "fast-deep-equal": "2.0.1", 44 | "formik": "1.5.2", 45 | "netlify-lambda": "1.4.5", 46 | "prettier": "1.17.0", 47 | "prop-types": "15.7.2", 48 | "react": "16.8.6", 49 | "react-dom": "16.8.6", 50 | "react-redux": "7.0.2", 51 | "react-redux-typescript-scripts": "1.5.0", 52 | "react-router-dom": "5.0.0", 53 | "react-scripts": "3.0.0", 54 | "react-testing-library": "6.1.2", 55 | "react-toastify": "5.1.0", 56 | "redux": "4.0.1", 57 | "redux-observable": "1.1.0", 58 | "reselect": "4.0.0", 59 | "rxjs": "6.5.1", 60 | "tslib": "1.9.3", 61 | "typesafe-actions": "4.1.2", 62 | "typescript": "3.4.5", 63 | "utility-types": "3.5.0", 64 | "yup": "0.27.0" 65 | }, 66 | "browserslist": { 67 | "production": [ 68 | ">0.2%", 69 | "not dead", 70 | "not op_mini all" 71 | ], 72 | "development": [ 73 | "last 1 chrome version", 74 | "last 1 firefox version", 75 | "last 1 safari version" 76 | ] 77 | }, 78 | "devDependencies": { 79 | "@types/yup": "0.26.12", 80 | "concurrently": "4.1.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at piotrek.witek@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lambdas/build/hello.js: -------------------------------------------------------------------------------- 1 | !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)}}])); --------------------------------------------------------------------------------