├── .editorconfig
├── .gitignore
├── .nsprc
├── .stylelintrc
├── .vscode
└── launch.json
├── LICENSE
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── @types
│ └── images
│ │ └── index.d.ts
├── actions
│ ├── ActionTypeKeys.ts
│ ├── ActionTypes.ts
│ └── authentication
│ │ ├── authenticationActions.ts
│ │ ├── signin
│ │ └── index.ts
│ │ └── signout
│ │ └── index.ts
├── api
│ ├── ApiError.ts
│ ├── authenticationApi.ts
│ ├── baseUrl.dev.ts
│ ├── baseUrl.prod.ts
│ └── baseUrl.ts
├── components
│ ├── App.scss
│ ├── App.tsx
│ ├── errors
│ │ └── Error404Page.tsx
│ ├── header
│ │ ├── Header.tsx
│ │ ├── navBar
│ │ │ ├── NavBar.scss
│ │ │ ├── NavBar.tsx
│ │ │ └── logo.svg
│ │ └── progress
│ │ │ ├── Progress.scss
│ │ │ └── Progress.tsx
│ ├── home
│ │ └── HomePage.tsx
│ ├── signUp
│ │ └── SignUpPage.tsx
│ └── signedIn
│ │ └── SignedInPage.tsx
├── createBootstrapGlobals.ts
├── index.tsx
├── reducers
│ ├── authenticationReducer.ts
│ ├── initialState.ts
│ ├── pendingActionsReducer.ts
│ └── rootReducer.ts
├── registerServiceWorker.ts
├── routes
│ ├── AuthenticateRoute.tsx
│ ├── RedirectIfAuthenticated.tsx
│ ├── Routes.tsx
│ └── paths.ts
├── selectors
│ └── index.ts
└── store
│ ├── IStoreState.ts
│ ├── configureStore.dev.ts
│ ├── configureStore.prod.ts
│ └── configureStore.ts
├── tsconfig.buildScripts.json
├── tsconfig.json
├── tsconfig.test.json
├── tslint.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = false
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | # Built files
24 | src/**/*.css
--------------------------------------------------------------------------------
/.nsprc:
--------------------------------------------------------------------------------
1 | {
2 | "exceptions": ["https://nodesecurity.io/advisories/535"]
3 | }
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-recommended"
3 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Chrome",
6 | "type": "chrome",
7 | "request": "launch",
8 | "url": "http://localhost:3000",
9 | "webRoot": "${workspaceRoot}/src",
10 | "userDataDir": "${workspaceRoot}/.vscode/chrome",
11 | "sourceMapPathOverrides": {
12 | "webpack:///src/*": "${webRoot}/*"
13 | }
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Jonathan Harrison
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-redux-ts
2 |
3 | This project demonstrates how to use [React](https://reactjs.org/) and [Redux](http://redux.js.org/) in [TypeScript](https://www.typescriptlang.org/).
4 |
5 | See my [blog post](https://medium.com/@jonjam/react-and-redux-with-typescript-da0c37537a79) for more details.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-ts",
3 | "version": "0.0.1",
4 | "private": true,
5 | "dependencies": {
6 | "@types/jest": "^20.0.6",
7 | "@types/node": "^8.0.19",
8 | "@types/react": "^15.6.4",
9 | "@types/react-dom": "^15.5.6",
10 | "bootstrap": "4.0.0-beta",
11 | "font-awesome": "^4.7.0",
12 | "jquery": "^3.2.1",
13 | "npm-run-all": "^4.0.2",
14 | "popper.js": "^1.11.1",
15 | "react": "^15.6.1",
16 | "react-dom": "^15.6.1",
17 | "react-redux": "^5.0.6",
18 | "react-router-dom": "^4.1.2",
19 | "react-scripts-ts": "2.7.0",
20 | "redux": "^3.7.2",
21 | "redux-thunk": "^2.2.0",
22 | "reselect": "^3.0.1"
23 | },
24 | "devDependencies": {
25 | "@types/enzyme": "^2.8.4",
26 | "@types/jquery": "^3.2.12",
27 | "@types/react-redux": "^5.0.1",
28 | "@types/react-router-dom": "^4.0.7",
29 | "@types/redux-immutable-state-invariant": "^2.0.0",
30 | "@types/redux-mock-store": "^0.0.10",
31 | "enzyme": "^2.9.1",
32 | "husky": "^0.14.3",
33 | "lint-staged": "^4.0.3",
34 | "node-sass-chokidar": "^0.0.3",
35 | "nsp": "^2.7.0",
36 | "prettier": "^1.5.3",
37 | "react-test-renderer": "^15.6.1",
38 | "redux-immutable-state-invariant": "^2.0.0",
39 | "redux-mock-store": "^1.2.3",
40 | "stylelint": "^8.0.0",
41 | "stylelint-config-recommended": "^1.0.0",
42 | "tslint-config-prettier": "^1.3.0",
43 | "tslint-immutable": "^4.0.2"
44 | },
45 | "lint-staged": {
46 | "src/**/*.{js,jsx,json,ts,tsx,css,scss}": [
47 | "prettier --write",
48 | "git add"
49 | ]
50 | },
51 | "scripts": {
52 | "precommit": "lint-staged",
53 | "lint-css": "stylelint src/**/*.scss -f verbose --color",
54 | "build-css": "npm run lint-css && node-sass-chokidar src/ -o src/",
55 | "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",
56 | "security-check": "nsp check",
57 | "start-ts": "react-scripts-ts start",
58 | "start": "npm-run-all -p watch-css start-ts",
59 | "build": "npm run security-check && npm run build-css && react-scripts-ts build",
60 | "test": "react-scripts-ts test",
61 | "eject": "react-scripts-ts eject"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JonJam/react-redux-ts/e28b10607ebd05b9c0c168112908621c81148c76/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React / Redux with TypeScript
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/@types/images/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.svg" {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/src/actions/ActionTypeKeys.ts:
--------------------------------------------------------------------------------
1 | export enum ActionTypeStates {
2 | INPROGRESS = "_INPROGRESS",
3 | SUCCESS = "_SUCCESS",
4 | FAIL = "_FAIL"
5 | }
6 |
7 | enum ActionTypeKeys {
8 | SIGNIN_INPROGRESS = "SIGNIN_INPROGRESS",
9 | SIGNIN_SUCCESS = "SIGNIN_SUCCESS",
10 | SIGNIN_FAIL = "SIGNIN_FAIL",
11 | SIGNOUT_INPROGRESS = "SIGNOUT_INPROGRESS",
12 | SIGNOUT_SUCCESS = "SIGNOUT_SUCCESS",
13 | SIGNOUT_FAIL = "SIGNOUT_FAIL"
14 | }
15 |
16 | export default ActionTypeKeys;
17 |
--------------------------------------------------------------------------------
/src/actions/ActionTypes.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ISignInFailAction,
3 | ISignInInProgressAction,
4 | ISignInSuccessAction
5 | } from "./authentication/signin";
6 | import {
7 | ISignOutFailAction,
8 | ISignOutInProgressAction,
9 | ISignOutSuccessAction
10 | } from "./authentication/signout";
11 |
12 | type ActionTypes =
13 | | ISignInFailAction
14 | | ISignInInProgressAction
15 | | ISignInSuccessAction
16 | | ISignOutFailAction
17 | | ISignOutInProgressAction
18 | | ISignOutSuccessAction;
19 |
20 | export default ActionTypes;
21 |
--------------------------------------------------------------------------------
/src/actions/authentication/authenticationActions.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from "redux";
2 | import {
3 | signIn as signInToApi,
4 | signOut as signOutFromApi
5 | } from "../../api/authenticationApi";
6 | import IStoreState from "../../store/IStoreState";
7 | import keys from "../ActionTypeKeys";
8 | import {
9 | ISignInFailAction,
10 | ISignInInProgressAction,
11 | ISignInSuccessAction
12 | } from "./signin";
13 | import {
14 | ISignOutFailAction,
15 | ISignOutInProgressAction,
16 | ISignOutSuccessAction
17 | } from "./signout";
18 |
19 | export function signIn(): (dispatch: Dispatch) => Promise {
20 | return async (dispatch: Dispatch) => {
21 | // Signal work in progress.
22 | dispatch(signInInProgress());
23 |
24 | try {
25 | await signInToApi();
26 |
27 | dispatch(signInSuccess());
28 | } catch (err) {
29 | dispatch(signInFail(err));
30 | }
31 | };
32 | }
33 |
34 | export function signOut(): (dispatch: Dispatch) => Promise {
35 | return async (dispatch: Dispatch) => {
36 | // Signal work in progress.
37 | dispatch(signOutInProgress());
38 |
39 | try {
40 | await signOutFromApi();
41 |
42 | dispatch(signOutSuccess());
43 | } catch (err) {
44 | dispatch(signOutFail(err));
45 | }
46 | };
47 | }
48 |
49 | function signInInProgress(): ISignInInProgressAction {
50 | return {
51 | type: keys.SIGNIN_INPROGRESS
52 | };
53 | }
54 |
55 | function signInSuccess(): ISignInSuccessAction {
56 | return {
57 | type: keys.SIGNIN_SUCCESS
58 | };
59 | }
60 |
61 | function signInFail(error: Error): ISignInFailAction {
62 | return {
63 | payload: {
64 | error
65 | },
66 | type: keys.SIGNIN_FAIL
67 | };
68 | }
69 |
70 | function signOutInProgress(): ISignOutInProgressAction {
71 | return {
72 | type: keys.SIGNOUT_INPROGRESS
73 | };
74 | }
75 |
76 | function signOutSuccess(): ISignOutSuccessAction {
77 | return {
78 | type: keys.SIGNOUT_SUCCESS
79 | };
80 | }
81 |
82 | function signOutFail(error: Error): ISignOutFailAction {
83 | return {
84 | payload: {
85 | error
86 | },
87 | type: keys.SIGNOUT_FAIL
88 | };
89 | }
90 |
--------------------------------------------------------------------------------
/src/actions/authentication/signin/index.ts:
--------------------------------------------------------------------------------
1 | import keys from "../../ActionTypeKeys";
2 |
3 | export interface ISignInSuccessAction {
4 | readonly type: keys.SIGNIN_SUCCESS;
5 | }
6 |
7 | export interface ISignInInProgressAction {
8 | readonly type: keys.SIGNIN_INPROGRESS;
9 | }
10 |
11 | export interface ISignInFailAction {
12 | readonly type: keys.SIGNIN_FAIL;
13 | readonly payload: {
14 | readonly error: Error;
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/src/actions/authentication/signout/index.ts:
--------------------------------------------------------------------------------
1 | import keys from "../../ActionTypeKeys";
2 |
3 | export interface ISignOutSuccessAction {
4 | readonly type: keys.SIGNOUT_SUCCESS;
5 | }
6 |
7 | export interface ISignOutInProgressAction {
8 | readonly type: keys.SIGNOUT_INPROGRESS;
9 | }
10 |
11 | export interface ISignOutFailAction {
12 | readonly type: keys.SIGNOUT_FAIL;
13 | readonly payload: {
14 | readonly error: Error;
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/src/api/ApiError.ts:
--------------------------------------------------------------------------------
1 | export default class ApiError extends Error {
2 | constructor(public readonly status: number, message: string) {
3 | super(message);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/api/authenticationApi.ts:
--------------------------------------------------------------------------------
1 | export function signIn(): Promise<{}> {
2 | return new Promise(resolve => setTimeout(resolve, 100));
3 | }
4 |
5 | export function signOut(): Promise<{}> {
6 | return new Promise(resolve => setTimeout(resolve, 100));
7 | }
8 |
--------------------------------------------------------------------------------
/src/api/baseUrl.dev.ts:
--------------------------------------------------------------------------------
1 | export default "http://localhost:3001/";
2 |
--------------------------------------------------------------------------------
/src/api/baseUrl.prod.ts:
--------------------------------------------------------------------------------
1 | export default "/";
2 |
--------------------------------------------------------------------------------
/src/api/baseUrl.ts:
--------------------------------------------------------------------------------
1 | import baseUrlDev from "./baseUrl.dev";
2 | import baseUrlProd from "./baseUrl.prod";
3 |
4 | const baseUrl =
5 | process.env.NODE_ENV === "production" ? baseUrlProd : baseUrlDev;
6 |
7 | export default baseUrl;
8 |
--------------------------------------------------------------------------------
/src/components/App.scss:
--------------------------------------------------------------------------------
1 | // Global imports
2 | @import "../../node_modules/bootstrap/scss/bootstrap.scss";
3 | @import "../../node_modules/font-awesome/css/font-awesome.css";
4 |
--------------------------------------------------------------------------------
/src/components/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { connect } from "react-redux";
3 | import { RouteComponentProps, withRouter } from "react-router-dom";
4 | import { bindActionCreators, Dispatch } from "redux";
5 | import { signOut as signOutAction } from "../actions/authentication/authenticationActions";
6 | import { homePath } from "../routes/paths";
7 | import Routes from "../routes/Routes";
8 | import { isBusy } from "../selectors";
9 | import IStoreState from "../store/IStoreState";
10 | import Header from "./header/Header";
11 |
12 | import "./App.css";
13 |
14 | interface IAppProps extends RouteComponentProps {
15 | readonly isBusy: boolean;
16 | readonly isAuthenticated: boolean;
17 | signOut: () => (dispatch: Dispatch) => Promise;
18 | }
19 |
20 | class App extends React.Component {
21 | constructor(props: IAppProps) {
22 | super(props);
23 |
24 | this.signOut = this.signOut.bind(this);
25 | }
26 |
27 | public render() {
28 | return (
29 |
30 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
43 | private async signOut(e: React.MouseEvent) {
44 | e.preventDefault();
45 |
46 | await this.props.signOut();
47 |
48 | this.props.history.push(homePath);
49 | }
50 | }
51 |
52 | function mapStateToProps(state: IStoreState) {
53 | return {
54 | isAuthenticated: state.isAuthenticated,
55 | isBusy: isBusy(state)
56 | };
57 | }
58 |
59 | function mapDispatchToProps(dispatch: Dispatch) {
60 | return {
61 | signOut: bindActionCreators(signOutAction, dispatch)
62 | };
63 | }
64 |
65 | // Casting to prevent error where used in index.ts that isBusy is mandatory, since it is being provided by Redux.
66 | export default withRouter(
67 | connect<{}, {}, IAppProps>(mapStateToProps, mapDispatchToProps)(App)
68 | ) as React.ComponentClass<{}>;
69 |
--------------------------------------------------------------------------------
/src/components/errors/Error404Page.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | class Error404Page extends React.Component {
4 | public render() {
5 | return (
6 |
7 |
8 |
9 |
404 - Page not found
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | export default Error404Page;
18 |
--------------------------------------------------------------------------------
/src/components/header/Header.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import NavBar from "./navBar/NavBar";
3 | import Progress from "./progress/Progress";
4 |
5 | interface IHeaderProps {
6 | readonly isBusy: boolean;
7 | readonly isAuthenticated: boolean;
8 | handleSignOut: (e: React.MouseEvent) => void;
9 | }
10 |
11 | export default function Header({ isBusy, ...rest }: IHeaderProps) {
12 | return (
13 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/header/navBar/NavBar.scss:
--------------------------------------------------------------------------------
1 | .navbar-brand {
2 | .brand-logo {
3 | width: 55px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/header/navBar/NavBar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Link } from "react-router-dom";
3 | import { homePath, signUpPath } from "../../../routes/paths";
4 |
5 | import logo from "./logo.svg";
6 |
7 | import "./NavBar.css";
8 |
9 | interface INavBarProps {
10 | readonly isAuthenticated: boolean;
11 | handleSignOut: (e: React.MouseEvent) => void;
12 | }
13 |
14 | export default function NavBar(props: INavBarProps) {
15 | let authenticationLink = (
16 |
17 | {"Sign in / up"}
18 |
19 | );
20 |
21 | if (props.isAuthenticated) {
22 | authenticationLink = (
23 |
24 | {"Sign out"}
25 |
26 | );
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 |
38 | {"React app"}
39 |
40 |
41 |
50 |
51 |
52 |
53 |
54 |
55 | {authenticationLink}
56 |
57 |
58 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/header/navBar/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/header/progress/Progress.scss:
--------------------------------------------------------------------------------
1 | .progress-bar {
2 | width: 100%;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/header/progress/Progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import "./Progress.css";
4 |
5 | interface ILoadingProps {
6 | readonly isBusy: boolean;
7 | }
8 |
9 | function Progress(props: ILoadingProps) {
10 | let progress: JSX.Element | null = null;
11 |
12 | if (props.isBusy) {
13 | progress = (
14 |
20 | );
21 | }
22 |
23 | return progress;
24 | }
25 |
26 | export default Progress;
27 |
--------------------------------------------------------------------------------
/src/components/home/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export default class HomePage extends React.Component {
4 | public render() {
5 | return (
6 |
7 |
{"React app"}
8 |
9 | {
10 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi vel augue turpis. Suspendisse malesuada lacus nec metus pharetra sodales. Nunc tellus quam, mollis a dictum et, luctus maximus libero."
11 | }
12 |
13 |
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/signUp/SignUpPage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { connect } from "react-redux";
3 | import { RouteComponentProps } from "react-router-dom";
4 | import { bindActionCreators, Dispatch } from "redux";
5 | import { signIn as signInAction } from "../../actions/authentication/authenticationActions";
6 | import { signedInPath } from "../../routes/paths";
7 | import IStoreState from "../../store/IStoreState";
8 |
9 | interface ISignUpPageProps extends RouteComponentProps {
10 | signIn: () => (dispatch: Dispatch) => Promise;
11 | }
12 |
13 | class SignUpPage extends React.Component {
14 | constructor(props: ISignUpPageProps) {
15 | super(props);
16 |
17 | this.handleClick = this.handleClick.bind(this);
18 | }
19 |
20 | public render() {
21 | return (
22 |
23 |
24 |
29 | {"Sign up"}
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | private async handleClick() {
37 | await this.props.signIn();
38 |
39 | this.props.history.push(signedInPath);
40 | }
41 | }
42 |
43 | function mapStateToProps() {
44 | return {};
45 | }
46 |
47 | function mapDispatchToProps(dispatch: Dispatch) {
48 | return {
49 | signIn: bindActionCreators(signInAction, dispatch)
50 | };
51 | }
52 |
53 | export default connect(mapStateToProps, mapDispatchToProps)(SignUpPage);
54 |
--------------------------------------------------------------------------------
/src/components/signedIn/SignedInPage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export default class SignedInPage extends React.Component {
4 | public render() {
5 | return (
6 |
7 |
Signed in page
8 | You are signed in.
9 |
10 | );
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/createBootstrapGlobals.ts:
--------------------------------------------------------------------------------
1 | import * as $ from "jquery";
2 | import Popper from "popper.js";
3 |
4 | const win = window as any;
5 |
6 | win.$ = $;
7 | win.jQuery = $;
8 | win.Popper = Popper;
9 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom";
3 | import { Provider } from "react-redux";
4 | import { BrowserRouter } from "react-router-dom";
5 | import "whatwg-fetch";
6 | import App from "../src/components/App";
7 | import configureStore from "../src/store/configureStore";
8 | import registerServiceWorker from "./registerServiceWorker";
9 |
10 | // The following imports have to be in this order for bootstrap to correctly work.
11 | /* tslint:disable ordered-imports */
12 | import "./createBootstrapGlobals";
13 | import "bootstrap";
14 |
15 | const configuredStore = configureStore();
16 |
17 | const app = (
18 |
19 |
20 |
21 |
22 |
23 | );
24 |
25 | ReactDOM.render(app, document.getElementById("root") as HTMLElement);
26 | registerServiceWorker();
27 |
--------------------------------------------------------------------------------
/src/reducers/authenticationReducer.ts:
--------------------------------------------------------------------------------
1 | import ActionTypeKeys from "../actions/ActionTypeKeys";
2 | import ActionTypes from "../actions/ActionTypes";
3 | import initialState from "./initialState";
4 |
5 | export default function authenticationReducer(
6 | state = initialState.isAuthenticated,
7 | action: ActionTypes
8 | ) {
9 | switch (action.type) {
10 | case ActionTypeKeys.SIGNIN_SUCCESS:
11 | return onSignIn();
12 | case ActionTypeKeys.SIGNOUT_SUCCESS:
13 | return onSignOut();
14 | default:
15 | return state;
16 | }
17 | }
18 |
19 | function onSignIn() {
20 | return true;
21 | }
22 |
23 | function onSignOut() {
24 | return false;
25 | }
26 |
--------------------------------------------------------------------------------
/src/reducers/initialState.ts:
--------------------------------------------------------------------------------
1 | import IStoreState from "../store/IStoreState";
2 |
3 | const defaultState: IStoreState = {
4 | isAuthenticated: false,
5 | pendingActions: 0
6 | };
7 |
8 | export default defaultState;
9 |
--------------------------------------------------------------------------------
/src/reducers/pendingActionsReducer.ts:
--------------------------------------------------------------------------------
1 | import ActionTypeKeys, { ActionTypeStates } from "../actions/ActionTypeKeys";
2 | import ActionTypes from "../actions/ActionTypes";
3 | import initialState from "./initialState";
4 |
5 | export default function pendingActionsReducer(
6 | state = initialState.pendingActions,
7 | action: ActionTypes
8 | ) {
9 | if (actionTypeEndsInInProgress(action.type)) {
10 | return onInProgressAction(state);
11 | } else if (
12 | actionTypeEndsInSuccess(action.type) ||
13 | actionTypeEndsInFail(action.type)
14 | ) {
15 | return onSuccessOrFailAction(state);
16 | } else {
17 | return state;
18 | }
19 | }
20 |
21 | function actionTypeEndsInInProgress(type: ActionTypeKeys) {
22 | const inProgress = ActionTypeStates.INPROGRESS;
23 |
24 | return type.substring(type.length - inProgress.length) === inProgress;
25 | }
26 |
27 | function actionTypeEndsInSuccess(type: ActionTypeKeys) {
28 | const success = ActionTypeStates.SUCCESS;
29 |
30 | return type.substring(type.length - success.length) === success;
31 | }
32 |
33 | function actionTypeEndsInFail(type: ActionTypeKeys) {
34 | const fail = ActionTypeStates.FAIL;
35 |
36 | return type.substring(type.length - fail.length) === fail;
37 | }
38 |
39 | function onInProgressAction(state: number) {
40 | return state + 1;
41 | }
42 |
43 | function onSuccessOrFailAction(state: number) {
44 | return state - 1;
45 | }
46 |
--------------------------------------------------------------------------------
/src/reducers/rootReducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import IStoreState from "../store/IStoreState";
3 | import isAuthenticated from "./authenticationReducer";
4 | import pendingActions from "./pendingActionsReducer";
5 |
6 | const rootReducer = combineReducers({
7 | isAuthenticated,
8 | pendingActions
9 | });
10 |
11 | export default rootReducer;
12 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.ts:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | export default function register() {
12 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
13 | window.addEventListener("load", () => {
14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
15 | navigator.serviceWorker
16 | .register(swUrl)
17 | .then(registration => {
18 | registration.onupdatefound = () => {
19 | const installingWorker = registration.installing;
20 | if (!installingWorker) {
21 | return;
22 | }
23 |
24 | installingWorker.onstatechange = () => {
25 | if (installingWorker.state === "installed") {
26 | if (navigator.serviceWorker.controller) {
27 | // At this point, the old content will have been purged and
28 | // the fresh content will have been added to the cache.
29 | // It"s the perfect time to display a "New content is
30 | // available; please refresh." message in your web app.
31 | console.log("New content is available; please refresh."); // tslint:disable-line
32 | } else {
33 | // At this point, everything has been precached.
34 | // It"s the perfect time to display a
35 | // "Content is cached for offline use." message.
36 | console.log("Content is cached for offline use."); // tslint:disable-line
37 | }
38 | }
39 | };
40 | };
41 | })
42 | .catch(error => {
43 | console.error("Error during service worker registration:", error); // tslint:disable-line
44 | });
45 | });
46 | }
47 | }
48 |
49 | export function unregister() {
50 | if ("serviceWorker" in navigator) {
51 | navigator.serviceWorker.ready.then(registration => {
52 | registration.unregister();
53 | });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/routes/AuthenticateRoute.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import {
3 | Redirect,
4 | Route,
5 | RouteComponentProps,
6 | RouteProps
7 | } from "react-router-dom";
8 |
9 | interface IAuthenticateRouteProps extends RouteProps {
10 | readonly isAuthenticated: boolean;
11 | readonly authenticatePath: string;
12 | readonly component: React.ComponentClass | React.StatelessComponent;
13 | }
14 |
15 | export default function AuthenticateRoute({
16 | component,
17 | authenticatePath,
18 | isAuthenticated,
19 | ...rest
20 | }: IAuthenticateRouteProps) {
21 | const Component = component;
22 |
23 | const render = (renderProps: RouteComponentProps) => {
24 | let element = (
25 |
31 | );
32 |
33 | if (isAuthenticated) {
34 | element = ;
35 | }
36 |
37 | return element;
38 | };
39 |
40 | return ;
41 | }
42 |
--------------------------------------------------------------------------------
/src/routes/RedirectIfAuthenticated.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import {
3 | Redirect,
4 | Route,
5 | RouteComponentProps,
6 | RouteProps
7 | } from "react-router-dom";
8 |
9 | interface IRedirectIfAuthenticatedProps extends RouteProps {
10 | readonly isAuthenticated: boolean;
11 | readonly redirectPath: string;
12 | readonly component: React.ComponentClass | React.StatelessComponent;
13 | }
14 |
15 | export default function RedirectIfAuthenticated({
16 | component,
17 | redirectPath,
18 | isAuthenticated,
19 | ...rest
20 | }: IRedirectIfAuthenticatedProps) {
21 | const Component = component;
22 |
23 | const render = (renderProps: RouteComponentProps) => {
24 | let element = ;
25 |
26 | if (isAuthenticated) {
27 | element = (
28 |
34 | );
35 | }
36 |
37 | return element;
38 | };
39 |
40 | return ;
41 | }
42 |
--------------------------------------------------------------------------------
/src/routes/Routes.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Route, Switch } from "react-router-dom";
3 | import Error404Page from "../components/errors/Error404Page";
4 | import HomePage from "../components/home/HomePage";
5 | import SignedInPage from "../components/signedIn/SignedInPage";
6 | import SignUpPage from "../components/signUp/SignUpPage";
7 | import AuthenticateRoute from "./AuthenticateRoute";
8 | import { homePath, signedInPath, signUpPath } from "./paths";
9 | import RedirectIfAuthenticated from "./RedirectIfAuthenticated";
10 |
11 | interface IRoutesProps {
12 | readonly isAuthenticated: boolean;
13 | }
14 |
15 | export default function Routes(props: IRoutesProps) {
16 | return (
17 |
18 |
25 |
26 |
32 |
33 |
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/routes/paths.ts:
--------------------------------------------------------------------------------
1 | export const homePath = "/";
2 | export const signUpPath = "/signup";
3 | export const signedInPath = "/signedin";
4 |
--------------------------------------------------------------------------------
/src/selectors/index.ts:
--------------------------------------------------------------------------------
1 | import { createSelector } from "reselect";
2 | import IStoreState from "../store/IStoreState";
3 |
4 | // Derived data selectors = using reselect
5 | const pendingActionsSelector = (state: IStoreState) => state.pendingActions;
6 |
7 | export const isBusy = createSelector(
8 | [pendingActionsSelector],
9 | pendingActions => pendingActions > 0
10 | );
11 |
--------------------------------------------------------------------------------
/src/store/IStoreState.ts:
--------------------------------------------------------------------------------
1 | export default interface IStoreState {
2 | readonly pendingActions: number;
3 | readonly isAuthenticated: boolean;
4 | };
5 |
--------------------------------------------------------------------------------
/src/store/configureStore.dev.ts:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore } from "redux";
2 | import reduxImmutableStateInvariant from "redux-immutable-state-invariant";
3 | import thunkMiddleware from "redux-thunk";
4 | import rootReducer from "../reducers/rootReducer";
5 | import IStoreState from "./IStoreState";
6 |
7 | export default function configureStore() {
8 | return createStore(
9 | rootReducer,
10 | applyMiddleware(thunkMiddleware, reduxImmutableStateInvariant())
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/store/configureStore.prod.ts:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore } from "redux";
2 | import thunkMiddleware from "redux-thunk";
3 | import rootReducer from "../reducers/rootReducer";
4 | import IStoreState from "./IStoreState";
5 |
6 | export default function configureStore() {
7 | return createStore(
8 | rootReducer,
9 | applyMiddleware(thunkMiddleware)
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/store/configureStore.ts:
--------------------------------------------------------------------------------
1 | import configureStoreDev from "./configureStore.dev";
2 | import configureStoreProd from "./configureStore.prod";
3 |
4 | const configure =
5 | process.env.NODE_ENV === "production"
6 | ? configureStoreProd
7 | : configureStoreDev;
8 |
9 | export default configure;
10 |
--------------------------------------------------------------------------------
/tsconfig.buildScripts.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs" // ts-node does not work with esnext
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "build/dist",
4 | "module": "esnext",
5 | "target": "es5",
6 | "lib": ["es6", "dom", "es2015.promise"],
7 | "sourceMap": true,
8 | "jsx": "react",
9 | "moduleResolution": "node",
10 | "rootDir": "src",
11 | "forceConsistentCasingInFileNames": true,
12 | "noImplicitReturns": true,
13 | "suppressImplicitAnyIndexErrors": true,
14 | "noUnusedLocals": true,
15 | "strict": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "noUnusedParameters": true,
18 | "noEmitOnError": true,
19 | "removeComments": true,
20 | "skipLibCheck": true,
21 | "experimentalDecorators": true
22 | },
23 | "exclude": [
24 | "node_modules",
25 | "build",
26 | "scripts",
27 | "acceptance-tests",
28 | "webpack",
29 | "jest",
30 | "src/setupTests.ts"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier", "tslint-immutable"]
3 | }
4 |
--------------------------------------------------------------------------------