├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ ├── fileTransform.js │ └── typescriptTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── docs ├── asset-manifest.json ├── build │ ├── asset-manifest.json │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ ├── service-worker.js │ └── static │ │ └── js │ │ ├── main.1a06dc19.js │ │ └── main.1a06dc19.js.map ├── favicon.ico ├── index.html ├── manifest.json ├── service-worker.js └── static │ ├── css │ ├── main.b7bf6768.css │ └── main.b7bf6768.css.map │ ├── js │ ├── main.7bb4b72d.js │ ├── main.7bb4b72d.js.map │ ├── main.92c4f680.js │ ├── main.92c4f680.js.map │ ├── main.9c8e191e.js │ ├── main.9c8e191e.js.map │ ├── main.e257de5d.js │ ├── main.e257de5d.js.map │ ├── main.f8502358.js │ └── main.f8502358.js.map │ └── media │ ├── roboto-latin-100.987b8457.woff2 │ ├── roboto-latin-100.e9dbbe8a.woff │ ├── roboto-latin-100italic.6232f43d.woff2 │ ├── roboto-latin-100italic.d704bb3d.woff │ ├── roboto-latin-300.55536c8e.woff2 │ ├── roboto-latin-300.a1471d1d.woff │ ├── roboto-latin-300italic.210a7c78.woff │ ├── roboto-latin-300italic.d69924b9.woff2 │ ├── roboto-latin-400.5d4aeb4e.woff2 │ ├── roboto-latin-400.bafb105b.woff │ ├── roboto-latin-400italic.9680d5a0.woff │ ├── roboto-latin-400italic.d8bcbe72.woff2 │ ├── roboto-latin-500.28546717.woff2 │ ├── roboto-latin-500.de8b7431.woff │ ├── roboto-latin-500italic.510dec37.woff2 │ ├── roboto-latin-500italic.ffcc050b.woff │ ├── roboto-latin-700.037d8304.woff2 │ ├── roboto-latin-700.cf6613d1.woff │ ├── roboto-latin-700italic.010c1aee.woff2 │ ├── roboto-latin-700italic.846d1890.woff │ ├── roboto-latin-900.19b7a0ad.woff2 │ ├── roboto-latin-900.8c2ade50.woff │ ├── roboto-latin-900italic.7b770d6c.woff2 │ └── roboto-latin-900italic.bc833e72.woff ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── start.js └── test.js ├── snippets └── tsx.json ├── src ├── App.tsx ├── Counter.tsx ├── CounterContainer.tsx ├── CounterDisplay.tsx ├── CounterForm.tsx ├── SimplePageLayout.tsx ├── Util.tsx ├── assets │ └── images │ │ └── logo.svg ├── index.tsx ├── registerServiceWorker.ts └── tests │ └── App.test.tsx ├── tsconfig.json ├── tsconfig.test.json └── tslint.json /.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 | .env* 14 | /src/registerServiceWorker.js 15 | 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "typescript", 8 | "tsconfig": "tsconfig.json", 9 | "option": "watch", 10 | "problemMatcher": [ 11 | "$tsc-watch" 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Clemex Technologies 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 | # A Template for TypeScript Projects using React and Redux 2 | 3 | This project is a simple template project developed by [Clemex Technologies](https://github.com/Clemex) for bootstrapping new React/Redux applications using TypeScript. We started from the [TypeScript React Starter](https://github.com/Microsoft/TypeScript-React-Starter) from Microsoft and added several libraries and a more complete example application. 4 | 5 | The project contains the source code for a simple counter application that demonstrates the usage of React, Redux, Redux-Form, Redux-Logger, Material-UI, and React-UI all written in TypeScript. This was inspired by [the Counter project by Leyka](https://github.com/Leyka/learning-react/tree/master/react-redux) a simple app which increments or decrements a counter using React/Redux which in turn was based on [the React/Redux counter example](https://github.com/reactjs/redux/tree/master/examples/counter/src). 6 | 7 | You can [try out this sample project here](https://clemex.github.io/typescript-react-template/). 8 | 9 | # Motivation 10 | 11 | [React](typescriptlang.org) is a very powerful UI creation framework for JavaScript applications, and [Redux](https://redux.js.org/) is a useful and simple state management system for JavaScript applications. 12 | 13 | At [Clemex](https://github.com/Clemex) we have found that React and Redux works very well with [TypeScript](typescriptlang.org), but it is not yet obvious what libraries work best with this stack for production code, and what the best practices are for producing scalable code. This project has served as a test bed for us when developing new features, and experimenting with different techniques. 14 | 15 | There are a couple of pre-existing React/Redux/TypeScript starters and template projects such as the [TypeScript React Starter](https://github.com/Microsoft/TypeScript-React-Starter) we started from, but we wanted to create a starter kit that more closely resembled our development stack and that would make it easier for new team members to onboard. 16 | 17 | # Building and Running the Project 18 | 19 | After cloning the repository from GitHub use the command `npm install` to install all of the tools and dependencies. 20 | 21 | The command `npm run start` will compile and launch the project in your browser at the location `http://localhost:3000/` with a watcher which will rebuild the project whenever any source file is changed. 22 | 23 | You can use `npm run build` to create a production build. For more build options take a look at the `package.json` file. 24 | 25 | # Libraries 26 | 27 | This project demonstrates the use of the following libraries in TypeScript: 28 | 29 | * [React](https://github.com/Clemex/typescript-react-template) - UI Front-end 30 | * [React Intl](https://github.com/yahoo/react-intl) - Application internationalization support 31 | * [Redux](https://redux.js.org/) - Centralized state management 32 | * [Redux Form](https://redux-form.com/7.2.1/) - Form state management via Redux 33 | * [Redux Logger](https://github.com/evgenyrodionov/redux-logger) - Redux logger middleware 34 | * [Material UI](https://www.material-ui.com/) - React components that implement Google's Material Design 35 | 36 | ## Development Tools 37 | 38 | At Clemex we are using the following development tools: 39 | 40 | * [NPM](https://www.npmjs.com/) : Node Package Manager 41 | * [Node](https://nodejs.org) : JavaScript runtime built on Chrome's V8 JavaScript engine 42 | * [TypeScript](https://www.typescriptlang.org/) : Statically typed Javascript that compiles to plain Javascript 43 | * [TS-Lint](https://palantir.github.io/tslint/) : Linter for the TypeScript language 44 | * [Webpack](https://webpack.js.org/) : Let you bundle JavaScript files for usage in a browser (needed for React) 45 | * [Jest](https://facebook.github.io/jest/) : Test all JavaScript code including React applications 46 | * [React Developer Tools for Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) 47 | * [Redux DevTools for Chrome](https://chrome.google.com/webstore/detail/redux-devtools) 48 | * [Visual Studio Code](https://code.visualstudio.com/) : Source code editor by Microsoft for Windows, Linux and macOS 49 | 50 | ## Contributing 51 | 52 | If you find any issues or have ideas for improvements we would love to hear from you. You can either log an [issue/improvement](https://github.com/Clemex/typescript-react-template/issues), or submit a [pull request](https://github.com/Clemex/typescript-react-template/pulls). 53 | 54 | ## Additional Resources 55 | 56 | Here are some useful links for learning specifically more about TypeScript, React, and Redux on the internet. 57 | 58 | * [TypeScript React Starter](https://github.com/Microsoft/TypeScript-React-Starter) from Microsoft 59 | * [TypeScript React Conversion Guide](https://github.com/Microsoft/TypeScript-React-Conversion-Guide) from Microsoft 60 | * [Static Typing in React & Redux using TypeScript](https://github.com/piotrwitek/react-redux-typescript-guide) by Piotr Witek 61 | * [Typesafe Action Creation for Redux](https://github.com/piotrwitek/typesafe-actions) by Piotr Witek 62 | * [TypeScript Guide for Material UI Next](https://material-ui-next.com/guides/typescript/) from Material UI 63 | * [React JavaScript to TypeScript Transform](https://github.com/lyft/react-javascript-to-typescript-transform) from Lyft 64 | * [Getting Started with React and TypeScript](https://javascriptplayground.com/react-typescript/) by Jack Franklin 65 | * [TypeScript, React, and Redux](http://www.mattgreer.org/articles/typescript-react-and-redux/) by Matt Greer 66 | * [TypeScript at Slack](https://slack.engineering/typescript-at-slack-a81307fa288d) by Felix Rieseberg 67 | * [TypeScript at Lyft](https://eng.lyft.com/typescript-at-lyft-64f0702346ea) by Mohsen Azimi 68 | 69 | 70 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. 31 | // https://github.com/motdotla/dotenv 32 | dotenvFiles.forEach(dotenvFile => { 33 | if (fs.existsSync(dotenvFile)) { 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }); 37 | } 38 | }); 39 | 40 | // We support resolving modules according to `NODE_PATH`. 41 | // This lets you use absolute paths in imports inside large monorepos: 42 | // https://github.com/facebookincubator/create-react-app/issues/253. 43 | // It works similar to `NODE_PATH` in Node itself: 44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 48 | // We also resolve them to make sure all tools using them work consistently. 49 | const appDirectory = fs.realpathSync(process.cwd()); 50 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 51 | .split(path.delimiter) 52 | .filter(folder => folder && !path.isAbsolute(folder)) 53 | .map(folder => path.resolve(appDirectory, folder)) 54 | .join(path.delimiter); 55 | 56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 57 | // injected into the application via DefinePlugin in Webpack configuration. 58 | const REACT_APP = /^REACT_APP_/i; 59 | 60 | function getClientEnvironment(publicUrl) { 61 | const raw = Object.keys(process.env) 62 | .filter(key => REACT_APP.test(key)) 63 | .reduce( 64 | (env, key) => { 65 | env[key] = process.env[key]; 66 | return env; 67 | }, 68 | { 69 | // Useful for determining whether we’re running in production mode. 70 | // Most importantly, it switches React into the correct mode. 71 | NODE_ENV: process.env.NODE_ENV || 'development', 72 | // Useful for resolving the correct path to static assets in `public`. 73 | // For example, . 74 | // This should only be used as an escape hatch. Normally you would put 75 | // images into the `src` and `import` them in code to get their paths. 76 | PUBLIC_URL: publicUrl, 77 | } 78 | ); 79 | // Stringify all values so we can feed into Webpack DefinePlugin 80 | const stringified = { 81 | 'process.env': Object.keys(raw).reduce( 82 | (env, key) => { 83 | env[key] = JSON.stringify(raw[key]); 84 | return env; 85 | }, 86 | {} 87 | ), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/jest/typescriptTransform.js: -------------------------------------------------------------------------------- 1 | // Copyright 2004-present Facebook. All Rights Reserved. 2 | 3 | 'use strict'; 4 | 5 | const tsJestPreprocessor = require('ts-jest/preprocessor'); 6 | 7 | module.exports = tsJestPreprocessor; 8 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right -------------------------------------------------------------------------------- /docs/build/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": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /docs/build/service-worker.js: -------------------------------------------------------------------------------- 1 | "use strict";var precacheConfig=[["/index.html","26e4194bc1c4abb3d4c54143f37d9514"],["/static/js/main.1a06dc19.js","1b7f9e94a08167bd1ffba733c2db9c29"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.hash="",n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var r=new Request(n,{credentials:"same-origin"});return fetch(r).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching),r="index.html";(t=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,r),t=urlsToCacheKeys.has(n));var a="/index.html";!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],e.request.url)&&(n=new URL(a,self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}); -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | TypeScript React Template App
-------------------------------------------------------------------------------- /docs/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": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /docs/service-worker.js: -------------------------------------------------------------------------------- 1 | "use strict";var precacheConfig=[["/typescript-react-template/index.html","328ddd30c98f22f580069f86792b2c93"],["/typescript-react-template/static/js/main.e257de5d.js","6b6626e171111eeceff46c97f2f94e70"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.hash="",n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var r=new Request(n,{credentials:"same-origin"});return fetch(r).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching),r="index.html";(t=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,r),t=urlsToCacheKeys.has(n));var a="/typescript-react-template/index.html";!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],e.request.url)&&(n=new URL(a,self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}); -------------------------------------------------------------------------------- /docs/static/css/main.b7bf6768.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Roboto;font-style:normal;font-display:swap;font-weight:100;src:local("Roboto Thin "),local("Roboto-Thin"),url(/typescript-react-template/static/media/roboto-latin-100.987b8457.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-100.e9dbbe8a.woff) format("woff")}@font-face{font-family:Roboto;font-style:italic;font-display:swap;font-weight:100;src:local("Roboto Thin italic"),local("Roboto-Thinitalic"),url(/typescript-react-template/static/media/roboto-latin-100italic.6232f43d.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-100italic.d704bb3d.woff) format("woff")}@font-face{font-family:Roboto;font-style:normal;font-display:swap;font-weight:300;src:local("Roboto Light "),local("Roboto-Light"),url(/typescript-react-template/static/media/roboto-latin-300.55536c8e.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-300.a1471d1d.woff) format("woff")}@font-face{font-family:Roboto;font-style:italic;font-display:swap;font-weight:300;src:local("Roboto Light italic"),local("Roboto-Lightitalic"),url(/typescript-react-template/static/media/roboto-latin-300italic.d69924b9.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-300italic.210a7c78.woff) format("woff")}@font-face{font-family:Roboto;font-style:normal;font-display:swap;font-weight:400;src:local("Roboto Regular "),local("Roboto-Regular"),url(/typescript-react-template/static/media/roboto-latin-400.5d4aeb4e.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-400.bafb105b.woff) format("woff")}@font-face{font-family:Roboto;font-style:italic;font-display:swap;font-weight:400;src:local("Roboto Regular italic"),local("Roboto-Regularitalic"),url(/typescript-react-template/static/media/roboto-latin-400italic.d8bcbe72.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-400italic.9680d5a0.woff) format("woff")}@font-face{font-family:Roboto;font-style:normal;font-display:swap;font-weight:500;src:local("Roboto Medium "),local("Roboto-Medium"),url(/typescript-react-template/static/media/roboto-latin-500.28546717.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-500.de8b7431.woff) format("woff")}@font-face{font-family:Roboto;font-style:italic;font-display:swap;font-weight:500;src:local("Roboto Medium italic"),local("Roboto-Mediumitalic"),url(/typescript-react-template/static/media/roboto-latin-500italic.510dec37.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-500italic.ffcc050b.woff) format("woff")}@font-face{font-family:Roboto;font-style:normal;font-display:swap;font-weight:700;src:local("Roboto Bold "),local("Roboto-Bold"),url(/typescript-react-template/static/media/roboto-latin-700.037d8304.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-700.cf6613d1.woff) format("woff")}@font-face{font-family:Roboto;font-style:italic;font-display:swap;font-weight:700;src:local("Roboto Bold italic"),local("Roboto-Bolditalic"),url(/typescript-react-template/static/media/roboto-latin-700italic.010c1aee.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-700italic.846d1890.woff) format("woff")}@font-face{font-family:Roboto;font-style:normal;font-display:swap;font-weight:900;src:local("Roboto Black "),local("Roboto-Black"),url(/typescript-react-template/static/media/roboto-latin-900.19b7a0ad.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-900.8c2ade50.woff) format("woff")}@font-face{font-family:Roboto;font-style:italic;font-display:swap;font-weight:900;src:local("Roboto Black italic"),local("Roboto-Blackitalic"),url(/typescript-react-template/static/media/roboto-latin-900italic.7b770d6c.woff2) format("woff2"),url(/typescript-react-template/static/media/roboto-latin-900italic.bc833e72.woff) format("woff")} 2 | /*# sourceMappingURL=main.b7bf6768.css.map*/ -------------------------------------------------------------------------------- /docs/static/css/main.b7bf6768.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../node_modules/typeface-roboto/index.css"],"names":[],"mappings":"AACA,WACE,mBACA,kBACA,kBACA,gBACA,yIAIsD,CAIxD,WACE,mBACA,kBACA,kBACA,gBACA,qJAI4D,CAI9D,WACE,mBACA,kBACA,kBACA,gBACA,2IAIsD,CAIxD,WACE,mBACA,kBACA,kBACA,gBACA,uJAI4D,CAI9D,WACE,mBACA,kBACA,kBACA,gBACA,+IAIsD,CAIxD,WACE,mBACA,kBACA,kBACA,gBACA,6JAI4D,CAI9D,WACE,mBACA,kBACA,kBACA,gBACA,+IAIsD,CAIxD,WACE,mBACA,kBACA,kBACA,gBACA,2JAI4D,CAI9D,WACE,mBACA,kBACA,kBACA,gBACA,2IAIsD,CAIxD,WACE,mBACA,kBACA,kBACA,gBACA,uJAI4D,CAI9D,WACE,mBACA,kBACA,kBACA,gBACA,6IAIsD,CAIxD,WACE,mBACA,kBACA,kBACA,gBACA,yJAI4D,CAC7D","file":"static/css/main.b7bf6768.css","sourcesContent":["/* roboto-100normal - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: normal;\n font-display: swap;\n font-weight: 100;\n src:\n local('Roboto Thin '),\n local('Roboto-Thin'),\n url('./files/roboto-latin-100.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-100.woff') format('woff'); /* Modern Browsers */\n}\n\n/* roboto-100italic - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: italic;\n font-display: swap;\n font-weight: 100;\n src:\n local('Roboto Thin italic'),\n local('Roboto-Thinitalic'),\n url('./files/roboto-latin-100italic.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-100italic.woff') format('woff'); /* Modern Browsers */\n}\n\n/* roboto-300normal - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: normal;\n font-display: swap;\n font-weight: 300;\n src:\n local('Roboto Light '),\n local('Roboto-Light'),\n url('./files/roboto-latin-300.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-300.woff') format('woff'); /* Modern Browsers */\n}\n\n/* roboto-300italic - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: italic;\n font-display: swap;\n font-weight: 300;\n src:\n local('Roboto Light italic'),\n local('Roboto-Lightitalic'),\n url('./files/roboto-latin-300italic.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-300italic.woff') format('woff'); /* Modern Browsers */\n}\n\n/* roboto-400normal - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: normal;\n font-display: swap;\n font-weight: 400;\n src:\n local('Roboto Regular '),\n local('Roboto-Regular'),\n url('./files/roboto-latin-400.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-400.woff') format('woff'); /* Modern Browsers */\n}\n\n/* roboto-400italic - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: italic;\n font-display: swap;\n font-weight: 400;\n src:\n local('Roboto Regular italic'),\n local('Roboto-Regularitalic'),\n url('./files/roboto-latin-400italic.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-400italic.woff') format('woff'); /* Modern Browsers */\n}\n\n/* roboto-500normal - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: normal;\n font-display: swap;\n font-weight: 500;\n src:\n local('Roboto Medium '),\n local('Roboto-Medium'),\n url('./files/roboto-latin-500.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-500.woff') format('woff'); /* Modern Browsers */\n}\n\n/* roboto-500italic - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: italic;\n font-display: swap;\n font-weight: 500;\n src:\n local('Roboto Medium italic'),\n local('Roboto-Mediumitalic'),\n url('./files/roboto-latin-500italic.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-500italic.woff') format('woff'); /* Modern Browsers */\n}\n\n/* roboto-700normal - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: normal;\n font-display: swap;\n font-weight: 700;\n src:\n local('Roboto Bold '),\n local('Roboto-Bold'),\n url('./files/roboto-latin-700.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-700.woff') format('woff'); /* Modern Browsers */\n}\n\n/* roboto-700italic - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: italic;\n font-display: swap;\n font-weight: 700;\n src:\n local('Roboto Bold italic'),\n local('Roboto-Bolditalic'),\n url('./files/roboto-latin-700italic.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-700italic.woff') format('woff'); /* Modern Browsers */\n}\n\n/* roboto-900normal - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: normal;\n font-display: swap;\n font-weight: 900;\n src:\n local('Roboto Black '),\n local('Roboto-Black'),\n url('./files/roboto-latin-900.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-900.woff') format('woff'); /* Modern Browsers */\n}\n\n/* roboto-900italic - latin */\n@font-face {\n font-family: 'Roboto';\n font-style: italic;\n font-display: swap;\n font-weight: 900;\n src:\n local('Roboto Black italic'),\n local('Roboto-Blackitalic'),\n url('./files/roboto-latin-900italic.woff2') format('woff2'), /* Super Modern Browsers */\n url('./files/roboto-latin-900italic.woff') format('woff'); /* Modern Browsers */\n}\n\n\n\n\n// WEBPACK FOOTER //\n// ./node_modules/typeface-roboto/index.css"],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-100.987b8457.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-100.987b8457.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-100.e9dbbe8a.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-100.e9dbbe8a.woff -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-100italic.6232f43d.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-100italic.6232f43d.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-100italic.d704bb3d.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-100italic.d704bb3d.woff -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-300.55536c8e.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-300.55536c8e.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-300.a1471d1d.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-300.a1471d1d.woff -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-300italic.210a7c78.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-300italic.210a7c78.woff -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-300italic.d69924b9.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-300italic.d69924b9.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-400.5d4aeb4e.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-400.5d4aeb4e.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-400.bafb105b.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-400.bafb105b.woff -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-400italic.9680d5a0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-400italic.9680d5a0.woff -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-400italic.d8bcbe72.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-400italic.d8bcbe72.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-500.28546717.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-500.28546717.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-500.de8b7431.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-500.de8b7431.woff -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-500italic.510dec37.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-500italic.510dec37.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-500italic.ffcc050b.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-500italic.ffcc050b.woff -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-700.037d8304.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-700.037d8304.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-700.cf6613d1.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-700.cf6613d1.woff -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-700italic.010c1aee.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-700italic.010c1aee.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-700italic.846d1890.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-700italic.846d1890.woff -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-900.19b7a0ad.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-900.19b7a0ad.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-900.8c2ade50.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-900.8c2ade50.woff -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-900italic.7b770d6c.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-900italic.7b770d6c.woff2 -------------------------------------------------------------------------------- /docs/static/media/roboto-latin-900italic.bc833e72.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/docs/static/media/roboto-latin-900italic.bc833e72.woff -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "src/index.tsx", 3 | "name": "template-app", 4 | "version": "1.1.0", 5 | "private": false, 6 | "license": "MIT", 7 | "repository": "github:Clemex/typescript-react-template", 8 | "homepage": "https://clemex.github.io/typescript-react-template", 9 | "description": "A starter template project for bootstrapping new React/Redux applications using TypeScript", 10 | "bugs": "https://github.com/Clemex/typescript-react-template/issues", 11 | "author": "Clemex Technologies (https://github.com/Clemex/)", 12 | "dependencies": { 13 | "@types/react-redux": "^5.0.14", 14 | "@types/redux-form": "^7.0.13", 15 | "autoprefixer": "7.1.6", 16 | "case-sensitive-paths-webpack-plugin": "2.1.1", 17 | "chalk": "1.1.3", 18 | "css-loader": "0.28.7", 19 | "dotenv": "4.0.0", 20 | "extract-text-webpack-plugin": "3.0.2", 21 | "file-loader": "0.11.2", 22 | "fork-ts-checker-webpack-plugin": "^0.2.8", 23 | "fs-extra": "3.0.1", 24 | "html-webpack-plugin": "2.29.0", 25 | "jest": "20.0.4", 26 | "material-ui": "^1.0.0-beta.30", 27 | "object-assign": "4.1.1", 28 | "postcss-flexbugs-fixes": "3.2.0", 29 | "postcss-loader": "2.0.8", 30 | "promise": "8.0.1", 31 | "raf": "3.4.0", 32 | "react": "^16.2.0", 33 | "react-dev-utils": "4.2.1", 34 | "react-dom": "^16.2.0", 35 | "react-intl": "^2.4.0", 36 | "react-redux": "^5.0.6", 37 | "redux": "^3.7.2", 38 | "redux-form": "^7.2.1", 39 | "redux-logger": "^3.0.6", 40 | "redux-thunk": "^2.2.0", 41 | "source-map-loader": "^0.2.1", 42 | "style-loader": "0.19.0", 43 | "sw-precache-webpack-plugin": "0.11.4", 44 | "ts-jest": "^20.0.7", 45 | "ts-loader": "^2.3.7", 46 | "tslint": "^5.7.0", 47 | "tslint-loader": "^3.5.3", 48 | "tslint-react": "^3.2.0", 49 | "typeface-roboto": "0.0.54", 50 | "typescript": "^2.6.2", 51 | "url-loader": "0.6.2", 52 | "webpack": "3.8.1", 53 | "webpack-dev-server": "2.9.4", 54 | "webpack-manifest-plugin": "1.3.2", 55 | "whatwg-fetch": "2.0.3" 56 | }, 57 | "scripts": { 58 | "start": "node scripts/start.js", 59 | "build": "node scripts/build.js", 60 | "test": "node scripts/test.js --env=jsdom", 61 | "gitpages": "cpx build/**/* docs", 62 | "commit": "git add . && git commit -m", 63 | "push": "git push", 64 | "deploy": "npm run build && npm run gitpages && npm run commit -- \"Deploy new version\" && npm run push" 65 | }, 66 | "devDependencies": { 67 | "@types/jest": "^22.0.1", 68 | "@types/node": "^9.3.0", 69 | "@types/react": "^16.0.34", 70 | "@types/react-dom": "^16.0.3", 71 | "@types/redux-logger": "^3.0.5", 72 | "cpx": "^1.5.0" 73 | }, 74 | "jest": { 75 | "collectCoverageFrom": [ 76 | "src/**/*.{js,jsx,ts,tsx}" 77 | ], 78 | "setupFiles": [ 79 | "/config/polyfills.js" 80 | ], 81 | "testMatch": [ 82 | "/src/**/__tests__/**/*.ts?(x)", 83 | "/src/**/?(*.)(spec|test).ts?(x)" 84 | ], 85 | "testEnvironment": "node", 86 | "testURL": "http://localhost", 87 | "transform": { 88 | "^.+\\.tsx?$": "/config/jest/typescriptTransform.js", 89 | "^.+\\.css$": "/config/jest/cssTransform.js", 90 | "^(?!.*\\.(js|jsx|mjs|css|json)$)": "/config/jest/fileTransform.js" 91 | }, 92 | "transformIgnorePatterns": [ 93 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$" 94 | ], 95 | "moduleNameMapper": { 96 | "^react-native$": "react-native-web" 97 | }, 98 | "moduleFileExtensions": [ 99 | "mjs", 100 | "web.ts", 101 | "ts", 102 | "web.tsx", 103 | "tsx", 104 | "web.js", 105 | "js", 106 | "web.jsx", 107 | "jsx", 108 | "json", 109 | "node" 110 | ], 111 | "globals": { 112 | "ts-jest": { 113 | "tsConfigFile": "tsconfig.test.json" 114 | } 115 | } 116 | }, 117 | "babel": { 118 | "presets": [ 119 | "react-app" 120 | ] 121 | }, 122 | "eslintConfig": { 123 | "extends": "react-app" 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clemex/typescript-react-template/012fe1f0152d9d59bf6dbc8495bed4c265b38931/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | TypeScript React Template App 23 | 24 | 25 | 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": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const chalk = require('chalk'); 19 | const fs = require('fs-extra'); 20 | const webpack = require('webpack'); 21 | const config = require('../config/webpack.config.prod'); 22 | const paths = require('../config/paths'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 25 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 26 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 27 | const printBuildError = require('react-dev-utils/printBuildError'); 28 | 29 | const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; 30 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 31 | const useYarn = fs.existsSync(paths.yarnLockFile); 32 | 33 | // These sizes are pretty large. We'll warn for bundles exceeding them. 34 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 35 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 36 | 37 | // Warn and crash if required files are missing 38 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 39 | process.exit(1); 40 | } 41 | 42 | // First, read the current file sizes in build directory. 43 | // This lets us display how much they changed later. 44 | measureFileSizesBeforeBuild(paths.appBuild) 45 | .then(previousFileSizes => { 46 | // Remove all content but keep the directory so that 47 | // if you're in it, you don't end up in Trash 48 | fs.emptyDirSync(paths.appBuild); 49 | // Merge with the public folder 50 | copyPublicFolder(); 51 | // Start the webpack build 52 | return build(previousFileSizes); 53 | }) 54 | .then( 55 | ({ stats, previousFileSizes, warnings }) => { 56 | if (warnings.length) { 57 | console.log(chalk.yellow('Compiled with warnings.\n')); 58 | console.log(warnings.join('\n\n')); 59 | console.log( 60 | '\nSearch for the ' + 61 | chalk.underline(chalk.yellow('keywords')) + 62 | ' to learn more about each warning.' 63 | ); 64 | console.log( 65 | 'To ignore, add ' + 66 | chalk.cyan('// eslint-disable-next-line') + 67 | ' to the line before.\n' 68 | ); 69 | } else { 70 | console.log(chalk.green('Compiled successfully.\n')); 71 | } 72 | 73 | console.log('File sizes after gzip:\n'); 74 | printFileSizesAfterBuild( 75 | stats, 76 | previousFileSizes, 77 | paths.appBuild, 78 | WARN_AFTER_BUNDLE_GZIP_SIZE, 79 | WARN_AFTER_CHUNK_GZIP_SIZE 80 | ); 81 | console.log(); 82 | 83 | const appPackage = require(paths.appPackageJson); 84 | const publicUrl = paths.publicUrl; 85 | const publicPath = config.output.publicPath; 86 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 87 | printHostingInstructions( 88 | appPackage, 89 | publicUrl, 90 | publicPath, 91 | buildFolder, 92 | useYarn 93 | ); 94 | }, 95 | err => { 96 | console.log(chalk.red('Failed to compile.\n')); 97 | printBuildError(err); 98 | process.exit(1); 99 | } 100 | ); 101 | 102 | // Create the production build and print the deployment instructions. 103 | function build(previousFileSizes) { 104 | console.log('Creating an optimized production build...'); 105 | 106 | let compiler = webpack(config); 107 | return new Promise((resolve, reject) => { 108 | compiler.run((err, stats) => { 109 | if (err) { 110 | return reject(err); 111 | } 112 | const messages = formatWebpackMessages(stats.toJson({}, true)); 113 | if (messages.errors.length) { 114 | // Only keep the first error. Others are often indicative 115 | // of the same problem, but confuse the reader with noise. 116 | if (messages.errors.length > 1) { 117 | messages.errors.length = 1; 118 | } 119 | return reject(new Error(messages.errors.join('\n\n'))); 120 | } 121 | if ( 122 | process.env.CI && 123 | (typeof process.env.CI !== 'string' || 124 | process.env.CI.toLowerCase() !== 'false') && 125 | messages.warnings.length 126 | ) { 127 | console.log( 128 | chalk.yellow( 129 | '\nTreating warnings as errors because process.env.CI = true.\n' + 130 | 'Most CI servers set it automatically.\n' 131 | ) 132 | ); 133 | return reject(new Error(messages.warnings.join('\n\n'))); 134 | } 135 | return resolve({ 136 | stats, 137 | previousFileSizes, 138 | warnings: messages.warnings, 139 | }); 140 | }); 141 | }); 142 | } 143 | 144 | function copyPublicFolder() { 145 | fs.copySync(paths.appPublic, paths.appBuild, { 146 | dereference: true, 147 | filter: file => file !== paths.appHtml, 148 | }); 149 | } 150 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const fs = require('fs'); 18 | const chalk = require('chalk'); 19 | const webpack = require('webpack'); 20 | const WebpackDevServer = require('webpack-dev-server'); 21 | const clearConsole = require('react-dev-utils/clearConsole'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const { 24 | choosePort, 25 | createCompiler, 26 | prepareProxy, 27 | prepareUrls, 28 | } = require('react-dev-utils/WebpackDevServerUtils'); 29 | const openBrowser = require('react-dev-utils/openBrowser'); 30 | const paths = require('../config/paths'); 31 | const config = require('../config/webpack.config.dev'); 32 | const createDevServerConfig = require('../config/webpackDevServer.config'); 33 | 34 | const useYarn = fs.existsSync(paths.yarnLockFile); 35 | const isInteractive = process.stdout.isTTY; 36 | 37 | // Warn and crash if required files are missing 38 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 39 | process.exit(1); 40 | } 41 | 42 | // Tools like Cloud9 rely on this. 43 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 44 | const HOST = process.env.HOST || '0.0.0.0'; 45 | 46 | // We attempt to use the default port but if it is busy, we offer the user to 47 | // run on a different port. `detect()` Promise resolves to the next free port. 48 | choosePort(HOST, DEFAULT_PORT) 49 | .then(port => { 50 | if (port == null) { 51 | // We have not found a port. 52 | return; 53 | } 54 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 55 | const appName = require(paths.appPackageJson).name; 56 | const urls = prepareUrls(protocol, HOST, port); 57 | // Create a webpack compiler that is configured with custom messages. 58 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 59 | // Load proxy config 60 | const proxySetting = require(paths.appPackageJson).proxy; 61 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 62 | // Serve webpack assets generated by the compiler over a web sever. 63 | const serverConfig = createDevServerConfig( 64 | proxyConfig, 65 | urls.lanUrlForConfig 66 | ); 67 | const devServer = new WebpackDevServer(compiler, serverConfig); 68 | // Launch WebpackDevServer. 69 | devServer.listen(port, HOST, err => { 70 | if (err) { 71 | return console.log(err); 72 | } 73 | if (isInteractive) { 74 | clearConsole(); 75 | } 76 | console.log(chalk.cyan('Starting the development server...\n')); 77 | openBrowser(urls.localUrlForBrowser); 78 | }); 79 | 80 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 81 | process.on(sig, function() { 82 | devServer.close(); 83 | process.exit(); 84 | }); 85 | }); 86 | }) 87 | .catch(err => { 88 | if (err && err.message) { 89 | console.log(err.message); 90 | } 91 | process.exit(1); 92 | }); 93 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /snippets/tsx.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": { 3 | "prefix": "Component", 4 | "description": "Create a pure React component with a state type and a properties type", 5 | "body": [ 6 | "export type ${1:name}Properties = {", 7 | "};", 8 | "", 9 | "export type ${1:name}State = {", 10 | "};", 11 | "", 12 | "export class ${1:name} extends React.PureComponent<${1:name}Properties> {", 13 | " state: ${1:name}State = {};", 14 | "", 15 | " constructor(p: ${1:name}Properties) {", 16 | " super(p);", 17 | " }", 18 | "", 19 | " render(): React.ReactNode {", 20 | " }", 21 | "}", 22 | "" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the main entry point of your React application. 3 | * The React application is a React component like any other react components. 4 | */ 5 | import * as React from 'react'; 6 | import { Provider } from 'react-redux'; 7 | import { IntlProvider, FormattedMessage, defineMessages } from 'react-intl'; 8 | import { createLogger } from 'redux-logger'; 9 | import { reducer as formReducer } from 'redux-form'; 10 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 11 | import { createMuiTheme, List, ListItem, ListItemText, Typography, Grid, Toolbar, Button, Paper } from 'material-ui'; 12 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 13 | import { Theme } from 'material-ui/styles'; 14 | import { blue } from 'material-ui/colors'; 15 | import { SimplePageLayout } from './SimplePageLayout'; 16 | import { CounterForm } from './CounterForm'; 17 | import { CounterContainer, counterReducer } from './CounterContainer'; 18 | 19 | /** Localizable strings defined using react-intl. */ 20 | const messages = defineMessages({ 21 | headline: { 22 | id: "headline", 23 | defaultMessage: "TypeScript React Template Application" , 24 | }, 25 | subheading: { 26 | id: "subheading", 27 | defaultMessage: "brought to you by" 28 | }, 29 | forkme: { 30 | id: "forkme", 31 | defaultMessage: "Fork me on GitHub!" 32 | }, 33 | light: { 34 | id: "light", 35 | defaultMessage: "Light" 36 | }, 37 | dark : { 38 | id: "dark", 39 | defaultMessage: "Dark" 40 | } 41 | }); 42 | 43 | /** 44 | * Reducers define how to update the redux store: an immutable object that contains most of the application data. 45 | */ 46 | const rootReducer = combineReducers({ 47 | counter: counterReducer, 48 | form: formReducer, 49 | }) 50 | 51 | /** 52 | * An example of using overrides to control the style of Material UI component 53 | */ 54 | const themeOverrides = { 55 | MuiPaper: { 56 | root: { 57 | padding: '10px' 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * Defines the style and palette settings for the light theme. 64 | */ 65 | const lightTheme = createMuiTheme({ 66 | palette: { type: 'light', primary: blue }, 67 | overrides: themeOverrides, 68 | }); 69 | 70 | /** 71 | * Defines the style and palette settings for the dark theme. 72 | */ 73 | const darkTheme = createMuiTheme({ 74 | palette: { type: 'dark', primary: blue }, 75 | overrides: themeOverrides, 76 | }); 77 | 78 | /** 79 | * Create the Redux middleware (https://redux.js.org/docs/advanced/Middleware.html). 80 | * Redux middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer 81 | * Note: logger must be the last middleware in chain, otherwise it will log thunk and promise, not actual actions 82 | */ 83 | const middleware = applyMiddleware(createLogger({})); 84 | 85 | /** 86 | * This is the globabl redux store. This is an immutable object where most of the application data is stored. 87 | * Updating the redux store is done by triggering "reducers" that are launched by actions. 88 | * For additional data see the AppState object. 89 | */ 90 | const store = createStore(rootReducer, middleware); 91 | 92 | /** 93 | * There are no properties in the main application: nobody passes it anything. 94 | */ 95 | export type AppProperties = { } 96 | 97 | /** 98 | * This is a state object for the whole Theme information is stored in app state. Optionally this could have been added to the store and added it 99 | * as its own reducer, but it would have added a lot of additional complexity for a simple string value. 100 | */ 101 | export type AppState = { theme: Theme }; 102 | 103 | /** 104 | * The entry point of the application. 105 | */ 106 | export class App extends React.PureComponent 107 | { 108 | state: AppState = { theme: lightTheme } 109 | 110 | setLightTheme = () => { this.setState({ theme: lightTheme }); } 111 | setDarkTheme = () => { this.setState({ theme: darkTheme }); } 112 | 113 | render(): React.ReactNode 114 | { 115 | const header = ( 116 | 117 |
118 | 119 | 120 | 121 | 122 | Clemex 123 | 124 |
125 | 126 | 127 | 128 |
129 | ); 130 | 131 | const sidebar = ( 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | ); 140 | 141 | const main = ( 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | ); 150 | 151 | return ( 152 | 153 | 154 | 155 | 160 | 161 | 162 | 163 | ); 164 | } 165 | } -------------------------------------------------------------------------------- /src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { WithStyles, withStyles, Paper } from 'material-ui'; 3 | import { NumberInput, LabeledButton } from "./Util"; 4 | import { StyleRules } from 'material-ui/styles'; 5 | import { defineMessages } from 'react-intl'; 6 | import { CounterDisplay } from './CounterDisplay'; 7 | 8 | /** 9 | * Localizable strings 10 | */ 11 | const messages = defineMessages({ 12 | current_counter_label: { 13 | id: "current_counter_label", 14 | defaultMessage: "The current counter is: {value}" , 15 | }, 16 | counter_label: { 17 | id: "counter_label", 18 | defaultMessage: "counter", 19 | }, 20 | increment: { 21 | id: "increment", 22 | defaultMessage: "increment", 23 | }, 24 | decrement: { 25 | id: "decrement", 26 | defaultMessage: "decrement", 27 | }, 28 | undo: { 29 | id: "undo", 30 | defaultMessage: "undo", 31 | }, 32 | redo: { 33 | id: "decrement", 34 | defaultMessage: "redo", 35 | } 36 | }); 37 | 38 | type ClassKeys = 'root'; 39 | 40 | const styles: StyleRules = { 41 | root: { 42 | backgroundColor: 'red', 43 | }, 44 | }; 45 | 46 | export interface CounterProps { 47 | value: number; 48 | onChange(value: number): void; 49 | onUndo(): void; 50 | onRedo(): void; 51 | } 52 | 53 | // Main presentation component: styles have to be passed explicitly. 54 | export class CounterBase 55 | extends React.PureComponent> 56 | { 57 | // Increment and decrement arrow functions. 58 | // These are defined outside of the render function as readonly properties of the object 59 | // so that they don't trigger unnecessary re-renders (as opposed to defining them in the render function) 60 | readonly inc = () => this.props.onChange(this.props.value + 1); 61 | readonly dec = () => this.props.onChange(this.props.value - 1); 62 | 63 | render() { 64 | return ( 65 | 66 |
67 | 68 | 69 |
70 |
71 | 72 | 73 |
74 |
75 | 76 | 77 |
78 |
79 | ); 80 | } 81 | } 82 | 83 | // Applies the specified styles 84 | export const Counter = withStyles(styles)(CounterBase); 85 | -------------------------------------------------------------------------------- /src/CounterContainer.tsx: -------------------------------------------------------------------------------- 1 | import { Counter } from './Counter'; 2 | import { connect } from 'react-redux'; 3 | 4 | /** 5 | * The state of the counter. Contains the current value, a history and an index for tracking undo/redo. 6 | * Making state objects as classes is useful because they can be initialized. 7 | */ 8 | export class CounterState { 9 | readonly value: number = 0; 10 | readonly history: number[] = [0]; 11 | readonly undoIndex: number = 0; 12 | } 13 | 14 | /** Generic type for actions with or without payloads. */ 15 | export type Action = { readonly type: T; readonly payload: P; } 16 | 17 | /** Counter specific type of an undo action */ 18 | export type UndoActionType = Action<'undo'>; 19 | 20 | /** Counter specific type of a redo action */ 21 | export type RedoActionType = Action<'redo'>; 22 | 23 | /** Counter specific type of a replace action */ 24 | export type ReplaceActionType = Action<'replace', number>; 25 | 26 | /** Creator functions for the different types of actions.*/ 27 | export const CounterActionCreators = { 28 | undo: () => ({ type: 'undo' } as UndoActionType), 29 | redo: () => ({ type: 'redo' } as RedoActionType), 30 | replace: (payload: number) => ({ type: 'replace', payload } as ReplaceActionType), 31 | } 32 | 33 | /** One of the different supported actions. */ 34 | export type CounterActionType = UndoActionType | RedoActionType | ReplaceActionType; 35 | 36 | /** 37 | * This is the redux reducer for the counter component. Given an action 38 | * it will create a new state from the previous state. 39 | */ 40 | export function counterReducer(state: CounterState = new CounterState(), 41 | action: CounterActionType): CounterState 42 | { 43 | switch (action.type) { 44 | case 'replace': 45 | return state.value !== action.payload ? { 46 | value: action.payload, 47 | history: [...state.history.slice(0, state.undoIndex+1), action.payload], 48 | undoIndex: state.undoIndex+1 49 | } : state; 50 | case 'undo': 51 | return state.undoIndex > 0 ? { 52 | value: state.history[state.undoIndex - 1], 53 | history: state.history, 54 | undoIndex: state.undoIndex-1, 55 | } : state; 56 | case 'redo': 57 | return state.undoIndex < state.history.length - 1 ? { 58 | value: state.history[state.undoIndex+1], 59 | history: state.history, 60 | undoIndex: state.undoIndex+1, 61 | } : state; 62 | default : 63 | return state; 64 | } 65 | }; 66 | 67 | /** 68 | * Function used to map Redux state to prperties in the counter. 69 | * Notice, we do not specify types, instead we let TypeScript infer the correct type. 70 | * This is useful for getting the correct type out of the connect HOC function 71 | */ 72 | function mapStateToProps(state) { 73 | return { 74 | value: (state['counter'] as CounterState).value 75 | } 76 | } 77 | 78 | /** 79 | * Mapping callback properties to an action dispatcher. 80 | * Notice, we do not specify the return type, instead we let TypeScript infer the types. 81 | */ 82 | function mapDispatchToProps(dispatch) { 83 | return { 84 | onChange: (value: number) => dispatch(CounterActionCreators.replace(value)), 85 | onUndo: () => dispatch(CounterActionCreators.undo()), 86 | onRedo: () => dispatch(CounterActionCreators.redo()), 87 | } 88 | } 89 | 90 | /** 91 | * The 'connect' function take a component and returns a high order component. 92 | * No types are needed, it is inferred automatically. 93 | */ 94 | export const CounterContainer = connect(mapStateToProps, mapDispatchToProps)(Counter); 95 | -------------------------------------------------------------------------------- /src/CounterDisplay.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * An example of a simple TypeScript react component that is styled using Material UI. 3 | */ 4 | import React from 'react'; 5 | import { FormattedMessage } from 'react-intl'; 6 | import { WithStyles, Typography } from 'material-ui'; 7 | import { StyleRules, withStyles } from 'material-ui/styles'; 8 | 9 | /** 10 | * This specifies the programmatic names of the different classes used to define styling rules. 11 | * The convention is to use one name that is 'root'. Multiple names are separated using the '|' 12 | * type union operator. 13 | */ 14 | type ClassKeys = 'root'; 15 | 16 | /** These are the styling rules used. */ 17 | export const styles: StyleRules = { 18 | root: { 19 | backgroundColor: 'lightGray', 20 | fontWeight: 'bold', 21 | padding: 5 22 | } 23 | } 24 | 25 | /** 26 | * Contains the minimum information to display the counter value: a label and a value. 27 | * Notice The label of the counter is a Message descriptor, not a string. 28 | * This makes internationalization of your application easier, and it makes finding or changing strings easier. 29 | */ 30 | export interface CounterDisplayProps { 31 | value: number; 32 | label: FormattedMessage.MessageDescriptor; 33 | } 34 | 35 | /** 36 | * This displays 37 | */ 38 | export class CounterDisplayBase extends React.PureComponent> { 39 | render(): React.ReactNode { 40 | return ( 41 | 42 | 43 | 44 | ); 45 | } 46 | } 47 | 48 | export const CounterDisplay = withStyles(styles)(CounterDisplayBase); -------------------------------------------------------------------------------- /src/CounterForm.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This module demostrates using Redux-form to encapsulate a form that has its own state separate from the main 3 | * application but still using the redux store. It submits changes to the main application when the input is valid. 4 | */ 5 | import React from 'react'; 6 | import { defineMessages, FormattedMessage, InjectedIntlProps, injectIntl } from 'react-intl'; 7 | import { Field, reduxForm, Form, InjectedFormProps, FormSubmitHandler, DecoratedComponentClass } from 'redux-form' 8 | import { connect } from 'react-redux'; 9 | import { NumberInput } from "./Util"; 10 | import { CounterState, CounterActionCreators } from "./CounterContainer"; 11 | import { Paper, Typography } from 'material-ui'; 12 | 13 | /** Localizable strings defined using react-intl. */ 14 | const messages = defineMessages({ 15 | counter_form_submit: { 16 | id: "counter_form_submit", 17 | defaultMessage: "submit" , 18 | }, 19 | counter_form_clear: { 20 | id: "counter_form_clear", 21 | defaultMessage: "clear" 22 | }, 23 | counter_form_title: { 24 | id: "counter_form_title", 25 | defaultMessage: "A Redux-Form with Validation (Max 100)" 26 | }, 27 | input_value : { 28 | id: "input_value", 29 | defaultMessage: "Input value" 30 | } 31 | }); 32 | 33 | /** Validates that the passed value is a number and less than or equal to a hundred */ 34 | function isValidValue(value) { 35 | return typeof(value) !== 'number' 36 | ? "Only numbers plz" 37 | : value > 100 38 | ? "This number is too darn high!" 39 | : undefined; 40 | } 41 | 42 | /** A wrapper around the Material UI Number input used with Redux-form. */ 43 | const NumberInputForm = (props) => (
44 | {props.meta.error}) || (props.meta.warning && {props.meta.warning})) } 49 | {...props} 50 | /> 51 |
52 | ); 53 | 54 | /** The type of the data managed by Redux-form. Not necessarily the same as the properties. */ 55 | export interface CounterFormData { 56 | value: number; 57 | } 58 | 59 | /** The type of the properties exposed to clients of this form. */ 60 | export interface CounterFormProps { 61 | value: number; 62 | } 63 | 64 | /** 65 | * The form component definition. Notice that we separate the form injected properties and the internationalization context. 66 | * Also the form automatically communicates its data to the Redux store. 67 | */ 68 | export class BaseCounterForm 69 | extends React.PureComponent> 70 | { 71 | /** Called by redux-form when the submit button is pressed. */ 72 | readonly onSubmit: FormSubmitHandler = 73 | ({value}, dispatch) => { 74 | if (value !== undefined) 75 | dispatch(CounterActionCreators.replace(value)); 76 | } 77 | 78 | /** Renders the form in all its glory. */ 79 | render() { 80 | const { 81 | invalid, pristine, submitting, reset, handleSubmit, intl 82 | } = this.props; 83 | 84 | const inputLabel = intl.formatMessage(messages.input_value); 85 | 86 | return ( 87 | 88 | 89 | 90 | 91 |
92 |
93 | 99 |
100 | 103 | 106 |
107 |
108 |
109 |
110 | ); 111 | } 112 | } 113 | 114 | /** Add the intl context which is needed explicitly. */ 115 | export const CounterFormWithIntl = injectIntl(BaseCounterForm); 116 | 117 | /** A Redux-ready version of the form. */ 118 | export const ReduxCounterForm: DecoratedComponentClass 119 | = reduxForm({ form: 'CounterForm', touchOnChange: true })(CounterFormWithIntl); 120 | 121 | /** Retrieves the value of the from the redux store. */ 122 | function mapStateToProps(state) { 123 | return { 124 | value: (state.counter as CounterState).value 125 | } 126 | }; 127 | 128 | /** The wrapper around the redux-connected version of the form. */ 129 | export const CounterForm = connect(mapStateToProps)(ReduxCounterForm); 130 | -------------------------------------------------------------------------------- /src/SimplePageLayout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Grid, AppBar } from 'material-ui'; 3 | 4 | /** Used for passing sidebar, header, and main formatted content. */ 5 | export interface SimplePageLayoutProperties { 6 | sidebar: React.ReactNode; 7 | header: React.ReactNode; 8 | main: React.ReactNode; 9 | } 10 | 11 | /** A simple predefined layout with a header, sidebar, and main body. 12 | * This demonstrates how to pass render elements as properties. 13 | */ 14 | export class SimplePageLayout extends React.PureComponent { 15 | render(): React.ReactNode { 16 | return ( 17 |
18 | 19 | {this.props.header} 20 | 21 | 22 | 23 | {this.props.sidebar} 24 | 25 | 26 | {this.props.main} 27 | 28 | 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Util.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from 'react-intl' 3 | import { Button, TextField, WithStyles, withStyles } from 'material-ui'; 4 | 5 | /** Represents either plain text string or a message descriptor */ 6 | export type StringOrMessage = string | FormattedMessage.MessageDescriptor; 7 | 8 | /** Properties of a number input component */ 9 | export interface NumberInputProps { 10 | label?: StringOrMessage; 11 | value: number; 12 | change: (value: number) => void; 13 | errorText?: string; 14 | } 15 | 16 | /** The input field definition, without the styles information. */ 17 | export class BaseNumberInput extends React.PureComponent> { 18 | render(): React.ReactNode { 19 | const label = FormattedMessageOrText(this.props.label); 20 | return ( 21 | this.props.change(+e.target.value)} 25 | type="number" 26 | className={(this.props.classes || {})['textField']} 27 | InputLabelProps={{ shrink: true, }} 28 | margin="normal" 29 | error={this.props.errorText !== undefined} 30 | helperText={this.props.errorText} 31 | /> 32 | ); 33 | } 34 | } 35 | 36 | /** The input field definition, with the styles information. */ 37 | export const NumberInput = withStyles({})(BaseNumberInput); 38 | 39 | /** Either returns the string, or a properly formatted string if given a message */ 40 | export function FormattedMessageOrText(input?: StringOrMessage): React.ReactNode { 41 | if (!input) { 42 | return ""; 43 | } 44 | else if (typeof (input) === 'string') { 45 | return input; 46 | } 47 | else { 48 | return ; 49 | } 50 | } 51 | 52 | /** This is a helper function that facilitates providing redux field components with the correct type. */ 53 | export function wrapForReduxFormField(Component) { 54 | return (p) => () 57 | } 58 | 59 | /** Properties of a button component */ 60 | export interface ButtonProperties { 61 | label?: StringOrMessage; 62 | click: () => void; 63 | } 64 | 65 | /** Labeled button input component */ 66 | export class LabeledButton extends React.PureComponent { 67 | render(): React.ReactNode { 68 | return ( 69 | 72 | ); 73 | } 74 | } -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { App } from './App'; 4 | import registerServiceWorker from './registerServiceWorker'; 5 | 6 | ReactDOM.render( 7 | , 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | registerServiceWorker(); 11 | -------------------------------------------------------------------------------- /src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-console 2 | // In production, we register a service worker to serve assets from local cache. 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 the 'N+1' visit to a page, since previously 7 | // cached resources are updated in the background. 8 | 9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 10 | // This link also includes instructions on opting out of this behavior. 11 | 12 | const isLocalhost = Boolean( 13 | window.location.hostname === 'localhost' || 14 | // [::1] is the IPv6 localhost address. 15 | window.location.hostname === '[::1]' || 16 | // 127.0.0.1/8 is considered localhost for IPv4. 17 | window.location.hostname.match( 18 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 19 | ) 20 | ); 21 | 22 | export default function register() { 23 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 24 | // The URL constructor is available in all browsers that support SW. 25 | const publicUrl = new URL( 26 | process.env.PUBLIC_URL!, 27 | window.location.toString() 28 | ); 29 | if (publicUrl.origin !== window.location.origin) { 30 | // Our service worker won't work if PUBLIC_URL is on a different origin 31 | // from what our page is served on. This might happen if a CDN is used to 32 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 33 | return; 34 | } 35 | 36 | window.addEventListener('load', () => { 37 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 38 | 39 | if (!isLocalhost) { 40 | // Is not local host. Just register service worker 41 | registerValidSW(swUrl); 42 | } else { 43 | // This is running on localhost. Lets check if a service worker still exists or not. 44 | checkValidServiceWorker(swUrl); 45 | } 46 | }); 47 | } 48 | } 49 | 50 | function registerValidSW(swUrl: string) { 51 | navigator.serviceWorker 52 | .register(swUrl) 53 | .then(registration => { 54 | registration.onupdatefound = () => { 55 | const installingWorker = registration.installing; 56 | if (installingWorker) { 57 | installingWorker.onstatechange = () => { 58 | if (installingWorker.state === 'installed') { 59 | if (navigator.serviceWorker.controller) { 60 | // At this point, the old content will have been purged and 61 | // the fresh content will have been added to the cache. 62 | // It's the perfect time to display a 'New content is 63 | // available; please refresh.' message in your web app. 64 | console.log('New content is available; please refresh.'); 65 | } else { 66 | // At this point, everything has been precached. 67 | // It's the perfect time to display a 68 | // 'Content is cached for offline use.' message. 69 | console.log('Content is cached for offline use.'); 70 | } 71 | } 72 | }; 73 | } 74 | }; 75 | }) 76 | .catch(error => { 77 | console.error('Error during service worker registration:', error); 78 | }); 79 | } 80 | 81 | function checkValidServiceWorker(swUrl: string) { 82 | // Check if the service worker can be found. If it can't reload the page. 83 | fetch(swUrl) 84 | .then(response => { 85 | // Ensure service worker exists, and that we really are getting a JS file. 86 | if ( 87 | response.status === 404 || 88 | response.headers.get('content-type')!.indexOf('javascript') === -1 89 | ) { 90 | // No service worker found. Probably a different app. Reload the page. 91 | navigator.serviceWorker.ready.then(registration => { 92 | registration.unregister().then(() => { 93 | window.location.reload(); 94 | }); 95 | }); 96 | } else { 97 | // Service worker found. Proceed as normal. 98 | registerValidSW(swUrl); 99 | } 100 | }) 101 | .catch(() => { 102 | console.log( 103 | 'No internet connection found. App is running in offline mode.' 104 | ); 105 | }); 106 | } 107 | 108 | export function unregister() { 109 | if ('serviceWorker' in navigator) { 110 | navigator.serviceWorker.ready.then(registration => { 111 | registration.unregister(); 112 | }); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/tests/App.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as 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 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": ["es2017", "dom"], 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "jsx": "react", 10 | "moduleResolution": "node", 11 | "rootDir": "src", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noImplicitAny": false, 16 | "strictNullChecks": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "noUnusedLocals": false, 19 | "noUnusedParameters": false, 20 | "allowSyntheticDefaultImports": true 21 | }, 22 | "exclude": [ 23 | "node_modules", 24 | "build", 25 | "scripts", 26 | "acceptance-tests", 27 | "webpack", 28 | "jest", 29 | "src/setupTests.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | --------------------------------------------------------------------------------