├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .stylelintrc.json ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── capacitor.config.json ├── docs ├── Component Diagram.drawio └── ComponentDiagram.png ├── firebase.json ├── jest ├── jest.json └── jest.preprocessor.js ├── npxcap.js ├── package-lock.json ├── package.json ├── rimraf.js ├── src ├── Common.scss ├── __tests__ │ ├── Render.test.tsx │ ├── Services.test.ts │ ├── __mocks__ │ │ ├── fileMock.js │ │ └── styleMock.js │ └── setupTests.js ├── components │ ├── App.tsx │ ├── Home.tsx │ ├── ServiceStatus.tsx │ ├── database │ │ ├── DatabaseState.ts │ │ └── DatabaseView.tsx │ ├── login │ │ ├── AccountMenu.tsx │ │ ├── Login.tsx │ │ └── LoginState.ts │ └── upload │ │ ├── Upload.tsx │ │ └── UploadState.ts ├── index.html.ejs ├── index.tsx └── services │ ├── Configuration.ts │ ├── Messages.ts │ └── Routing.ts ├── static ├── config │ ├── dev │ │ └── config.json │ ├── prod │ │ └── config.json │ └── test │ │ └── config.json ├── images │ └── favicon.ico └── messages │ └── messages.json ├── tsconfig.json └── webpack ├── common.js ├── dev.js ├── prod.js └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": ["env", "react", "stage-2"], 5 | "plugins": ["transform-export-extensions"], 6 | "only": [ 7 | "./**/*.js", 8 | "node_modules/jest-runtime" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /src/__tests__/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "project": "tsconfig.json", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/class-name-casing": "error", 20 | "@typescript-eslint/indent": "error", 21 | "@typescript-eslint/prefer-namespace-keyword": "error", 22 | "@typescript-eslint/type-annotation-spacing": "error", 23 | "no-eval": "error", 24 | "no-unsafe-finally": "error", 25 | "no-var": "error" 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /build 4 | /plugins 5 | *.log 6 | /www 7 | /cypress/videos 8 | /cypress/screenshots 9 | .vscode 10 | .DS_Store 11 | .firebase 12 | .firebaserc 13 | .stylelintrc 14 | /src/__tests__/__coverage__ 15 | /temp 16 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "block-no-empty": null, 5 | "color-no-invalid-hex": true, 6 | "declaration-colon-space-after": "always", 7 | "indentation": [4, { 8 | "except": ["value"] 9 | }], 10 | "max-empty-lines": 2, 11 | "rule-empty-line-before": [ "always", { 12 | "except": ["first-nested"], 13 | "ignore": ["after-comment"] 14 | } ], 15 | "unit-whitelist": ["em", "rem", "%", "s"] 16 | } 17 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | before_install: 5 | - npm install -g typescript 6 | script: 7 | - npm run build:dev 8 | - npm run test -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at naishtech@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 NaishTech 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 | ## Please note: this repository is no longer maintained, but might still be useful for educational purposes ## 2 | 3 | 4 | # Platform Agnostic TypeScript Template (PATT) 5 | 6 | [![Build Status](https://travis-ci.org/naishtech/platform-agnostic-typescript-template.svg?branch=master)](https://travis-ci.org/naishtech/platform-agnostic-typescript-template) [![Greenkeeper badge](https://badges.greenkeeper.io/naishtech/platform-agnostic-typescript-template.svg)](https://greenkeeper.io/) 7 | 8 | PATT is a template for multi-platform TypeScript applications with serverside authentication, database and storage support. 9 | 10 | PATT can span multiple devices natively including Web, Desktop, Android and iOS. 11 | 12 | PATT has built in services to help you authenticate users, gather analytics, connect to an online database and upload files to online storage. 13 | 14 | PATT is free/open source. Enjoy. 15 | 16 | If you find PATT useful, please consider donating: 17 | 18 | Donate with PayPal 19 | 20 | # Table of contents 21 | 22 | 1. [ Features ](#features) 23 | 2. [ Requirements ](#requirements) 24 | 3. [ Modules ](#dependencies) 25 | 4. [ Setup ](#setup) 26 | 5. [ Getting Started ](#getting-started) 27 | 6. [ Hosting ](#hosting) 28 | 7. [ Sample Project ](#sample-project) 29 | 8. [ Firebase Functions ](#firebase-functions) 30 | 31 | 32 | 33 | # 1. Features: 34 | 35 | - Android, iOS, Desktop and Web support. 36 | - Plain email/password authentication. 37 | - Google Analytics support. 38 | - Local, Test and Production environment configurations. 39 | - Firebase hosting, database and storage support. Firebase Functions with Typescript not included (there's a [template](https://github.com/firebase/functions-samples/tree/master/typescript-getting-started) for that already) 40 | - Multilingual i18n support. 41 | 42 | Components 44 | 45 | 46 | 47 | # 2. Requirements: 48 | 49 | - [NodeJS/npm](https://nodejs.org/en/) 50 | - [Android Studio](https://developer.android.com/studio) (Optional for building Android Apps) 51 | - [XCode](https://developer.apple.com/xcode) (Optional for building iOS Apps). 52 | 53 | 54 | 55 | # 3. Modules: 56 | 57 | - PATT includes the following main dependencies: 58 | 59 | - [Capacitor](https://capacitor.ionicframework.com/) 60 | - [Firebase](https://firebase.google.com/) 61 | - [Google Analytics](https://analytics.google.com/analytics/web/) 62 | - [Webpack](https://webpack.js.org/) 63 | - [Jest](https://jestjs.io/) 64 | - [Mobx](https://github.com/mobxjs/mobx) 65 | - [TypeScript](https://www.typescriptlang.org/) 66 | - [ESLint](https://eslint.org/) 67 | - [Scss](https://sass-lang.com/) 68 | - [Stylelint](https://stylelint.io/) 69 | 70 | 71 | 72 | # 4. Setup: 73 | 74 | ## Install your IDE's: 75 | 76 | 1. Install [VSCode](https://code.visualstudio.com/). 77 | 78 | 2. Install [NodeJS/npm](https://nodejs.org/en/). 79 | 80 | 3. Optionally, for Android support, install [Android Studio](https://developer.android.com/studio). 81 | - Make sure your JAVA_HOME and ANDROID_SDK_ROOT environment variables are set correctly. 82 | - Make sure you setup a Virtual Device (you can do this as part of the android launch, see below). 83 | 84 | 4. Optionally, for iOS support, install [XCode](https://developer.apple.com/xcode/). 85 | 86 | 5. Open a command prompt and change directory to your project's root directory. 87 | 88 | 6. Execute `npm install`. 89 | 90 | 91 | 92 | # 5. Getting Started: 93 | 94 | ### Building the sample web app: 95 | 96 | `npm run build:dev` 97 | 98 | - Notes: 99 | - Compiled files will be in the `dist` directory. 100 | - [Capacitor](https://capacitor.ionicframework.com/) works by copying your web application bundle (HTML / JavaScript / CSS) to other target platforms (IOS/Android/Desktop). The above script will build your web application using the [Webpack](https://webpack.js.org/) build configuration in `/webpack/dev.js`, likewise `npm run build:test` and `npm run build:prod` will build using the [Webpack](https://webpack.js.org/) build configuration in `/webpack/test.js` and `/webpack/prod.js` respectively. 101 | 102 | ### Running tests: 103 | 104 | `npm run test` 105 | 106 | - Notes: 107 | - This template includes Jest as a unit testing tool. A sample test rendering the `` component can be found in `/src/__tests__/App.test.tsx`. 108 | - All tests should be placed under `/src/__tests__/` 109 | - A coverage report will be added to `/src/__coverage__/` 110 | 111 | ### Checking code style: 112 | 113 | `npm run lint:es` 114 | 115 | - Notes: 116 | - Code styles can be configured in `.eslintrc.js` 117 | - Tests are ignored (code styles ignores can be configured in `.eslintignore`) 118 | 119 | 120 | ### Starting the local web development server: 121 | 122 | `npm run start` 123 | 124 | - Notes: 125 | - Once you have built your application, this command will start a local web host at http://localhost:8080 126 | - In order to log in to the sample app provided you will need to create a test Firebase server, see [Hosting](#hosting). After you have set up your test server you can copy the `firebase` settings to your local `static/config/dev/config.json` file. 127 | - In order use the sample shakeout tests you will need to setup the appropriate rules in Firestore and Storage to allow read write access for logged in users. 128 | - The local webpack development server comes with an inbuilt hot loader and will reload as you make changes to your source code. 129 | 130 | ### Starting the local Android emulator: 131 | 132 | `npm run start:android` 133 | 134 | - Notes: 135 | - Make sure you have installed Android Studio as per [Getting Started](#getting-started). 136 | - If this is the first time you have opened the project in Android Studio it will prompt you for import. Just select the defauls and continue. 137 | - Once Android Studio has started the project should automatically build, once built you can execute it via the Run menu. 138 | 139 | ### Starting the local iOS emulator: 140 | 141 | `npm run start:ios` 142 | 143 | - Notes: 144 | - Make sure you have installed XCode as per [Getting Started](#getting-started). 145 | - If this is the first time you have opened the project in XCode it will prompt you for import. Just select the defauls and continue. 146 | - Once XCode has started the project should automatically build, once built you can execute it via the Run button. 147 | 148 | ### Starting the desktop application: 149 | 150 | `npm run start:desktop` 151 | 152 | - Notes: 153 | - Electron support for Capacitor is currently in preview, and lags behind iOS, Android, and Web support. 154 | - First time starting this might take a while, be patient 155 | 156 | 157 | 158 | # 6. Hosting 159 | 160 | ## Deploying to a Firebase test server 161 | 162 | - This template provides configuration for a firebase test server. 163 | 164 | 1. First, create a new [Firebase](https://firebase.google.com) project. 165 | 2. Make sure you have configured your signin method(s) on your Firebase project (under the [firebase console](https://console.firebase.google.com) go to Authentication -> Signin-Method). If you want use Firestore or Storage, make sure you have configured your access rights for them. 166 | 3. Add the project id (found in your Firebase Project Settings) for your test Firebase project under the `test` field in `.firebaserc`: 167 | 168 | 169 | ``` 170 | { 171 | "projects": { 172 | ... 173 | "test": "test-firebase-project", 174 | ... 175 | } 176 | } 177 | ``` 178 | 179 | 4. Get your project settings (under the [firebase console](https://console.firebase.google.com) go to Settings -> Project Settings) and add the relevant configuration to the following file. 180 | 181 | `/static/config/test.json` 182 | 183 | ``` 184 | ... 185 | "firebase" : { 186 | "apiKey": "your_api_key", 187 | "authDomain": "your.firebaseapp.com", 188 | "databaseURL": "https://your.firebaseio.com", 189 | "projectId": "your-project-id", 190 | "storageBucket": "your.appspot.com", 191 | "messagingSenderId": "123456789012" 192 | } 193 | ... 194 | ``` 195 | 196 | 5. Once you have configured as per above, run the following: 197 | 198 | `npm run deploy:test` 199 | 200 | ## Deploying to a Firebase prod server 201 | 202 | - This template provides configuration for a firebase production server. 203 | 204 | 1. First, create a new [Firebase](https://firebase.google.com) project. 205 | 2. Make sure you have configured your signin method(s) on your Firebase project (under the [firebase console](https://console.firebase.google.com) go to Authentication -> Signin-Method). If you want use Firestore or Storage, make sure you have configured your access rights for them. 206 | 3. Add the project id (found in your Firebase Project Settings) for your prod Firebase project under the `prod` field in `.firebaserc`: 207 | 208 | 209 | ``` 210 | { 211 | "projects": { 212 | ... 213 | "prod": "prod-firebase-project", 214 | ... 215 | } 216 | } 217 | ``` 218 | 219 | 4. Get your project settings (under the [firebase console](https://console.firebase.google.com) go to Settings -> Project Settings) and add the relevant configuration to the following file. 220 | 221 | `/static/config/prod.json` 222 | 223 | ``` 224 | ... 225 | "firebase" : { 226 | "apiKey": "your_api_key", 227 | "authDomain": "your.firebaseapp.com", 228 | "databaseURL": "https://your.firebaseio.com", 229 | "projectId": "your-project-id", 230 | "storageBucket": "your.appspot.com", 231 | "messagingSenderId": "123456789012" 232 | } 233 | ... 234 | ``` 235 | 236 | 5. Once you have configured as per above, run the following: 237 | 238 | `npm run deploy:prod` 239 | 240 | 241 | 242 | # 7. Sample Project 243 | 244 | ## Sample Components 245 | 246 | - `src/components/database/DatabaseView.tsx`: Simple data table connected to a Firebase Firestore database. 247 | - `src/components/database/DatabaseState.ts`: Database state. 248 | - `src/components/login/AccountMenu.tsx`: Simple account menu with a login link. Clicking the link will send the user to Login.tsx. 249 | - `src/components/login/Login.tsx`: Contains the FirebaseUI plain email/password login/sign up button. 250 | - `src/components/login/LoginState.ts`: Login state (contains unsubscribe functions). 251 | - `src/components/upload/Upload.tsx`: Simple upload connected to Firebase storage. 252 | - `src/components/upload/Upload.ts`: Upload state. 253 | - `src/App.tsx`: Main container with configured routes (with dev hot loader support). 254 | - `src/Home.tsx`: Simple home screen showing PATT service configuration and shakeout tests. 255 | - `index.tsx`: Index page with dev hot loader support. 256 | 257 | ## Services 258 | 259 | - The included services are under the following directory: `/src/services/`. 260 | - Note: Services are executed in the following order: 261 | 262 | 1. Configuation.ts 263 | 2. Messages.ts 264 | 265 | ### Built in services: 266 | 267 | - Configuration.ts 268 | - Configuration service supporting dev, test and prod configurations under `static/config/`; 269 | - Messages.json are loaded via XHR Request (see Messages.ts) 270 | - The dev, test and prod configuration is deployed with the relative npm script targets `npm deploy:` 271 | - Example usage: 272 | 273 | ``` 274 | import {Configuration} from 'Configuration'; 275 | 276 | /* 277 | Get the storage bucket string from config. 278 | */ 279 | 280 | const storageBucket = Configuration.getConfig('firebase').storageBucket; 281 | 282 | ``` 283 | 284 | - Messages.ts 285 | 286 | - Messages are loaded asynchonously from the server, see Configuration.configure(); 287 | - Example Usage: 288 | 289 | ``` 290 | import {Messages} from 'Messages'; 291 | 292 | /* 293 | Get the message string from messages.json. 294 | */ 295 | 296 | const message = Messages.get("hello") 297 | 298 | /* 299 | Get the message string from messages.json in en US locale 300 | */ 301 | 302 | const message = Messages.get("hello","en_US"); 303 | 304 | /* 305 | Format a string with a variable 306 | */ 307 | 308 | const message = Messages.format("Hello {0} you're {1}", ["PATT","ace"]); 309 | 310 | //returns "Hello PATT you're ace" 311 | 312 | ``` 313 | 314 | 315 | 316 | # 8. Adding firebase functions support 317 | 318 | - Firebase functions should be engineered in a different project/repository. If PATT is of interest we will consider releasing our firebase functions template, until then, here's a decent firebase TypeScript template to get you started: 319 | 320 | https://github.com/firebase/functions-samples/tree/master/typescript-getting-started 321 | 322 | 323 | 324 | 325 | -------------------------------------------------------------------------------- /capacitor.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.example.app", 3 | "appName": "Platform Agnostic TypeScript Template", 4 | "bundledWebRuntime": false, 5 | "npmClient": "npm", 6 | "webDir": "dist" 7 | } 8 | -------------------------------------------------------------------------------- /docs/Component Diagram.drawio: -------------------------------------------------------------------------------- 1 | 7Vxtc9o4EP41zNx1Jhm/23wMkLS9SXPc0btMP2WELcCJbflkkZD++pPwS2yvSEyDgxOgnQmsjV/22efRateipw/D1WeK4sU34uGgpyneqqePepqmGprWE/8V7zG12IadGubU97KdngwT/yfOjEpmXfoeTio7MkIC5sdVo0uiCLusYkOUkofqbjMSVM8aozkGhomLAmi99j22yKyq1X/a8AX780V2akfL7i9E+c7ZnSQL5JGHkkk/7+lDSghL34WrIQ6E83K/pN+72LC1uDCKI9bkC1c3VP/3Z5yoX67IOFDcpf95fKKnR7lHwTK74XGA2IzQMMmumj3mrqBkGXlYHE3p6YOHhc/wJEau2PrAwee2BQsD/knlbwM0xcGYJD7zScRtLr9KTPmGe0yZz/17WduBEXEEFPhz6e5n2YYpYYyEfMOMRGxIAkLXF6c7iviXn3hAqIdpvjkiEb/IQcIoucOy70BXZt4VZ8erkilz7WdMQszoI98l22paZvqVLM5VO4P9oRQ1RmZblAJGNTMjyiJ1Xhz7CUz+JsNTju3jrUUH8V/XsX21vPox+EGTS+ckp1AJ3Auf4ilKMLf+9h0njP89j+59SqKQ3/rvR8i3gTyXtgxx3TYB4romQ7zfFuKQzQBR4BFv/RLe9YOgZJ+tX8KHuW6phUNzJRSWpxARn9A0IcGS4TPqZlq+thaftN243jAqrjckrld1ieuttjxvPEM1K+BnH0wpfzcX7y6WkSsYkHwC6HgoWRR0y4Y0NF1vVapkq6Flqo5xoYOIz2mwQLE4Rriai1H6dO7G2ukCr766goeDmKZvRm5Alt7NLL8+sQlRtoHeNHXrRnaHvueJKy8IHuAZk9C72C/hyuJH88v1biMTEr6/fmX2PLzkEfUsPV4OsyyulFMYV3nolcNKz5IBivng6d9XUwdZrGVnHBM/YqUxpBLUJ2o/P31+CDKbJZiBYC0u/Nfj1zxU5VAkw/SbKoe1hXJMGKEiX+2mbPgBzygIxR9NNszGMXZwsmEfimyoTv9l3dD0N9QNZwvd+BsjPmMOJVtGiAvF+jtdlJTkv+CjiYndOPIOTkxUFYR0F9Vk0yy0DZWRzWs0CfitqYyqbSEz13jKd/1CEtZNOcErhmmEghs/vkGeR3GS4A83yVGbR93BKYwGo/n7Y4wnLvVjGLN5cLkkjHm4Ca/XYa6iqVvtyoOunjYoMspKTq3pgwZLTjzZcN+BM1XL7povnysivTd3Fm7anzthTeMbma6670pA8w74sv9yalbziKJY1nAoGQrTqv+mAbUo9m/sJoB+QSl5IEsW+BHPEvJmoAIziuH6JWsOWI6jDkSuwZMJz8dPt5NlG7l5xFnpZpcVESogqWWWmiQ3iTB7IPQuOY3d3cSJZRj1QDH6krHclESKbrYUKTpsOl0SjjPPEVEogI+mSZzmipxUd3zPEb7HAYlFA4p/mmDKPQCCi7uEVaGW5oNlqDPTi+2lIjWTtbeqDbAWSW/U8iLNtiHnZf1DrbVmEpyOlduHY0q8ZcqCYxPxFWLv2DUSd6GPCPNkOGzufyY+D1CSZGG1QbJ3wUylX0No/+1GHabdx35j56biBY2OU3HoGzjZ6aLEtKMfe2866nBudOw6dkw8jMaBdnjiAZvmH1Q8TLM+x9t/51GHXd9j6/E9KIrVOPoOT1FgO72LitJiAUKiNHvvPuqw7HjsPnZbY5zGcXdwGmPA0ugQCUB4+grFpnNdCUXrXFvCgCXKIYlm/nxJUVqZrIsE8PPGinLZfc/WDNssVu4Ct3o3ybAljFMdCW6t1ReNJvVFCQFuMWOP+Qjq5IZi+ZXRXKeM7RucwGeyUM9tr1SpE63aTbXfSqNMWBgY4XvIo5RmG0GLCNvUUSkTLhtfDAkFqkypKt53sW10Yv0SZ9sLEK0CmQrF0exLhrW2GnEmnCOnK76OUG4LpblnKA044RbdtyOUDaB0usVKA84zv/GJAJrzqcAxWeFwVce9LuQqDR59aTlXaT6nOqhcxYJc+qo6kqT/48vi1hFi6lVZlPHsTYXRgjQ7gtn0QbAqmNrewbRhpeMIZkMw7SqY+t7BNGH2ebZkC37f3NFpleXQ0hRTM7tXVDGbtDBaTVTM5ovbDipRseHTWuch8gOoh2OUJA+ESqZ2ExHFmvIVsu3jiOX28WM49RUxjoSHjgljKk+Adj/2wQeb/sCSntQ7KPDvf0mMDcuRwJEvriSQh3mZE3mbVniy0iuEPdt6zw9FHiW+dxovOIjrFu2mXvDWrEFJnC5imPkrcUU7GbkcBYBsSSijWRLKPKO4r0MZlirPUrcCsD/8OoD674hxjZMlgPbmUXL38MDEwv9zcoDQAO5ofdlwI+NOe+A0KE/xw/hxgkvqtX4ioUGysIORfvtlTfkzBMWQrgEXW5IHBIy2XOzA2exxENoxkQxTRiSJyplGWyjDZwnWTxQdmspZepMByH5mmvYaaKQ/Utrgd0GOi08bLj71cHLHnXCzq0WoRq6OebgoktVrLU0ZpMGigWAZpbd8iFQGC4T7b0nm2z8eV3fnl9cX05OrsfVj6d/+cyIh86digZrAZxnHPKrFUUS9A0WELcR6YIXhMA4Qw/WFxB2Ftbw2sRjN1V71wcqW8c91Mn+MQTadt3czk+Afn37JOq2bPf0euH7+Pw== -------------------------------------------------------------------------------- /docs/ComponentDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naishtech/platform-agnostic-typescript-template/4bd07847a5c8e2af622083bf15500988378985f1/docs/ComponentDiagram.png -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jest/jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "rootDir": "..", 3 | "coverageDirectory": "/src/__tests__/__coverage__/", 4 | "setupFiles": [ 5 | "src/__tests__/setupTests.js" 6 | ], 7 | "roots": [ 8 | "/src/" 9 | ], 10 | "moduleNameMapper": { 11 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/src/__tests__/__mocks__/fileMock.js", 12 | "\\.(css|scss|less)$": "/src/__tests__/__mocks__/styleMock.js" 13 | }, 14 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx"], 15 | "transform": { 16 | "^.+\\.(ts|tsx)$": "/jest/jest.preprocessor.js" 17 | }, 18 | "transformIgnorePatterns": [ 19 | "/node_modules/" 20 | ], 21 | "testRegex": "/__tests__/.*\\.(ts|tsx)$", 22 | "moduleDirectories": [ 23 | "node_modules" 24 | ], 25 | "globals": { 26 | "DEVELOPMENT": false, 27 | "FAKE_SERVER": false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /jest/jest.preprocessor.js: -------------------------------------------------------------------------------- 1 | const tsc = require('typescript'); 2 | const tsConfig = require('./../tsconfig.json'); 3 | 4 | module.exports = { 5 | process(src, path) { 6 | const isTs = path.endsWith('.ts'); 7 | const isTsx = path.endsWith('.tsx'); 8 | const isTypescriptFile = (isTs || isTsx); 9 | 10 | if ( isTypescriptFile ) { 11 | src = tsc.transpileModule(src, { 12 | compilerOptions: tsConfig.compilerOptions, 13 | fileName: path 14 | } 15 | ).outputText; 16 | 17 | // update the path so babel can try and process the output 18 | path = path.substr(0, path.lastIndexOf('.')) + (isTs ? '.js' : '.jsx') || path; 19 | } 20 | 21 | return src; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /npxcap.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { exec } = require('child_process'); 3 | const platform = process.argv[2]; 4 | 5 | const addPlatform = function() { 6 | console.log(`adding ${platform} platform`) 7 | return new Promise(function (resolve, reject) { 8 | exec(`npx cap add ${platform}`, (err, stdout, stderr) => { 9 | if (err) { 10 | console.error(stderr) 11 | reject(err); 12 | return; 13 | } else { 14 | resolve(); 15 | } 16 | console.log(stdout) 17 | }); 18 | }); 19 | 20 | } 21 | 22 | const openIde = function() { 23 | console.log(`opening ${platform} platform`) 24 | exec(`npx cap copy ${platform} && npx cap open ${platform}`, (err, stdout, stderr) => { 25 | console.log(stdout); 26 | if (err) { 27 | console.error(err); 28 | console.error(stderr); 29 | return; 30 | } 31 | }); 32 | } 33 | 34 | if (fs.existsSync(platform)) { 35 | 36 | openIde(); 37 | 38 | } else { 39 | 40 | addPlatform().then(function(){ openIde() }); 41 | 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "platform-agnostic-typescript-template", 3 | "version": "0.0.1", 4 | "description": "Platform Agnostic TypeScript Template (PATT)", 5 | "keywords": [ 6 | "react", 7 | "webpack", 8 | "typescript", 9 | "babel", 10 | "sass", 11 | "hmr", 12 | "starter", 13 | "boilerplate" 14 | ], 15 | "author": "Andrew Naish", 16 | "license": "MIT", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/naishtech/platform-agnostic-typescript-template" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/naishtech/platform-agnostic-typescript-template/issues" 23 | }, 24 | "homepage": "https://www..io", 25 | "scripts": { 26 | "build:prod": "npm run lint:sass && npm run clean-dist && webpack -p --config=webpack/prod.js", 27 | "build:test": "npm run lint:sass && npm run clean-dist && webpack -p --config=webpack/test.js", 28 | "build:dev": "npm run lint:sass && npm run clean-dist && webpack -p --config=webpack/dev.js", 29 | "clean-dist": "node rimraf.js dist", 30 | "lint:es": "eslint ./src --ext .ts,.tsx", 31 | "lint:sass": "stylelint ./src/**/*.scss", 32 | "start": "npm run start:web", 33 | "start:web": "npm run clean-dist && webpack-dev-server --config=webpack/dev.js", 34 | "start:desktop": "npm run build:dev && node npxcap.js electron", 35 | "start:android": "npm run build:dev && node npxcap.js android", 36 | "start:ios": "npm run build:dev && npx cap copy ios && node npxcap.js ios", 37 | "switch:test": "firebase use test", 38 | "deploy:test": "npm run clean-dist && npm run switch:test && npm run build:test && firebase deploy --only hosting", 39 | "switch::prod": "firebase use prod", 40 | "deploy:prod": "npm run clean-dist && npm run switch:prod && npm run build:prod && firebase deploy --only hosting", 41 | "test": "jest --coverage --config=jest/jest.json" 42 | }, 43 | "devDependencies": { 44 | "@types/enzyme": "^3.10.5", 45 | "@types/jest": "^25.1.4", 46 | "@types/react": "^16.9.27", 47 | "@types/react-dom": "^16.9.5", 48 | "@types/react-router-dom": "^5.1.3", 49 | "@typescript-eslint/eslint-plugin": "^2.25.0", 50 | "@typescript-eslint/parser": "^2.25.0", 51 | "awesome-typescript-loader": "^5.2.1", 52 | "babel-cli": "^6.26.0", 53 | "babel-core": "^6.26.3", 54 | "babel-loader": "^8.1.0", 55 | "babel-plugin-transform-export-extensions": "^6.22.0", 56 | "babel-preset-react": "^6.24.1", 57 | "copy-webpack-plugin": "^5.1.1", 58 | "css-loader": "^3.4.2", 59 | "enzyme": "^3.11.0", 60 | "enzyme-adapter-react-16": "^1.15.2", 61 | "enzyme-to-json": "^3.4.4", 62 | "eslint": "^6.8.0", 63 | "eslint-plugin-react": "^7.19.0", 64 | "express": "^4.17.1", 65 | "file-loader": "^6.0.0", 66 | "firebase-tools": "^8.0.0", 67 | "html-webpack-plugin": "^4.0.3", 68 | "image-webpack-loader": "^6.0.0", 69 | "jest": "^24.9.0", 70 | "node-sass": "^4.13.1", 71 | "postcss-loader": "^3.0.0", 72 | "react": "^16.13.1", 73 | "react-addons-test-utils": "^15.6.2", 74 | "react-dom": "^16.13.1", 75 | "react-hot-loader": "^4.12.20", 76 | "react-test-renderer": "^16.13.1", 77 | "rimraf": "^3.0.2", 78 | "sass-loader": "^8.0.2", 79 | "style-loader": "^1.1.3", 80 | "stylelint": "^13.2.1", 81 | "stylelint-config-standard": "^18.3.0", 82 | "stylelint-webpack-plugin": "^2.0.0", 83 | "typescript": "^3.8.3", 84 | "uglifyjs-webpack-plugin": "^2.2.0", 85 | "webpack": "^4.42.1", 86 | "webpack-cli": "^3.3.11", 87 | "webpack-dev-middleware": "^3.7.2", 88 | "webpack-dev-server": "^3.10.3", 89 | "webpack-merge": "^4.2.2" 90 | }, 91 | "dependencies": { 92 | "@capacitor/android": "^2.0.0", 93 | "@capacitor/cli": "^2.0.0", 94 | "@capacitor/core": "^2.0.0", 95 | "@capacitor/ios": "^2.0.0", 96 | "firebase": "^7.13.1", 97 | "html-react-parser": "^0.10.3", 98 | "mobx": "^5.15.4", 99 | "mobx-react": "^6.1.8", 100 | "react-firebaseui": "^4.1.0", 101 | "react-ga": "^2.7.0", 102 | "react-router": "^5.1.2", 103 | "react-router-dom": "^5.1.2" 104 | }, 105 | "postcss": {} 106 | } 107 | -------------------------------------------------------------------------------- /rimraf.js: -------------------------------------------------------------------------------- 1 | const rimraf = require("rimraf"); 2 | 3 | rimraf(process.argv[2], function (error){ 4 | 5 | if(error){ 6 | console.error(error) 7 | } 8 | 9 | }); 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Common.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | max-width: 100%; 4 | overflow-x: hidden; 5 | } 6 | 7 | .visible { 8 | display: inherit; 9 | } 10 | 11 | .hidden { 12 | display: none; 13 | } 14 | -------------------------------------------------------------------------------- /src/__tests__/Render.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { shallow } from "enzyme"; 3 | import App from "../components/App"; 4 | import { Messages } from "../services/Messages"; 5 | import { testMessages, testConfig } from "./Services.test"; 6 | import { Configuration } from "../services/Configuration"; 7 | 8 | /** 9 | * Sample .tsx test 10 | */ 11 | describe("Component Suite", () => { 12 | 13 | beforeAll(() => { 14 | 15 | Configuration.setConfig(testConfig) 16 | Messages.setMessages(testMessages); 17 | 18 | }); 19 | 20 | it("should render App without throwing an error", () => { 21 | shallow(); 22 | }); 23 | 24 | 25 | }); -------------------------------------------------------------------------------- /src/__tests__/Services.test.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from "../services/Configuration"; 2 | import { Messages } from "../services/Messages"; 3 | 4 | export const testConfig = { 5 | config : { 6 | firebase : { 7 | apiKey: "testApiKey", 8 | } 9 | } 10 | }; 11 | 12 | export const testMessages = { 13 | "en-US" : { 14 | "logout" : "Log out", 15 | "login" : "Log in", 16 | "signin-prompt": "Sign in:" 17 | } 18 | }; 19 | 20 | it("Should load configuration", () => { 21 | 22 | Configuration.setConfig(testConfig.config); 23 | 24 | const firebaseKey = Configuration.getConfig("firebase").apiKey; 25 | 26 | expect(firebaseKey).toBe('testApiKey'); 27 | 28 | Messages.setMessages(testMessages); 29 | 30 | }); 31 | 32 | it("Should load messages", () => { 33 | 34 | Messages.setMessages(testMessages); 35 | 36 | const message = Messages.get("logout") 37 | 38 | expect(message).toBe('Log out'); 39 | 40 | }); 41 | 42 | it("Should format messages", () => { 43 | 44 | Messages.setMessages(testMessages); 45 | 46 | const message = Messages.format("Hello {0}",["Patt"]) 47 | 48 | expect(message).toBe("Hello Patt"); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /src/__tests__/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /src/__tests__/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /src/__tests__/setupTests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines the React 16 Adapter for Enzyme. 3 | * 4 | * @link http://airbnb.io/enzyme/docs/installation/#working-with-react-16 5 | * @copyright 2017 Airbnb, Inc. 6 | */ 7 | const enzyme = require("enzyme"); 8 | const Adapter = require("enzyme-adapter-react-16"); 9 | 10 | enzyme.configure({ adapter: new Adapter() }); 11 | 12 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { HashRouter, Route } from "react-router-dom"; 3 | import { observer } from "mobx-react"; 4 | import Login from "./login/Login"; 5 | import Home from "./Home"; 6 | import { Switch } from "react-router-dom"; 7 | import { Configuration } from "../services/Configuration"; 8 | import * as ReactGA from "react-ga"; 9 | import * as firebase from "firebase/app"; 10 | 11 | /** 12 | * Sample component containing routes. 13 | * Initialises firebase and google analytics. 14 | */ 15 | 16 | @observer 17 | export default class App extends React.Component<{}, {}> { 18 | 19 | constructor(props: any){ 20 | super(props); 21 | this.initAnalytics(); 22 | this.initFirebase(); 23 | } 24 | 25 | private initFirebase(){ 26 | const config = Configuration.getConfig("firebase"); 27 | if (config && firebase.apps.length === 0) { 28 | return firebase.initializeApp(config); 29 | } 30 | } 31 | 32 | private initAnalytics(){ 33 | const config = Configuration.getConfig("analytics") 34 | if(config) { 35 | ReactGA.initialize(config.google.config); 36 | window.onhashchange = () => { 37 | let hashPath = window.location.href.split("#"); 38 | let page = hashPath.length === 2 ? hashPath[1] : "/index"; 39 | ReactGA.pageview(page); 40 | }; 41 | } 42 | } 43 | 44 | render() { 45 | return ( 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react"; 3 | import { observer } from "mobx-react"; 4 | import ServiceStatus from "./ServiceStatus"; 5 | import { Messages } from "../services/Messages"; 6 | 7 | /** 8 | * Sample Home Page component. 9 | * Contains ServiceStatus component 10 | */ 11 | 12 | @observer 13 | export default class Home extends React.Component<{}, {}> { 14 | 15 | render() { 16 | return ( 17 |
18 |

{Messages.get("home-title")}

19 | 20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ServiceStatus.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react"; 3 | import { observer } from "mobx-react"; 4 | import AccountMenu from "./login/AccountMenu"; 5 | import Upload from "./upload/Upload"; 6 | import { LoginState } from "./login/LoginState"; 7 | import DatabaseView from "./database/DatabaseView"; 8 | import firebase = require("firebase"); 9 | 10 | @observer 11 | export default class ServiceStatus extends React.Component<{}, {}> { 12 | 13 | render() { 14 | 15 | return ( 16 |
17 | {firebase.app.length > 0 ? : null} 18 | {firebase.app.length > 0 && LoginState.user ? 19 |
    20 |
  • 21 | {LoginState.user ? : null} 22 |
  • 23 |
  • 24 | {LoginState.user ? : null} 25 |
  • 26 |
27 | : null} 28 |
29 | ); 30 | } 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/components/database/DatabaseState.ts: -------------------------------------------------------------------------------- 1 | import { observable } from "mobx"; 2 | 3 | /** 4 | * Sample Database state 5 | */ 6 | export class DatabaseStore { 7 | 8 | @observable public rows: any; 9 | public key: string; 10 | public val: string; 11 | 12 | } 13 | 14 | export const DatabaseState = new DatabaseStore(); -------------------------------------------------------------------------------- /src/components/database/DatabaseView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { Messages } from "../../services/Messages"; 4 | import { DatabaseState } from "./DatabaseState"; 5 | import * as firebase from "firebase/app"; 6 | import "firebase/firestore"; 7 | import { LoginState } from "../login/LoginState"; 8 | 9 | /** 10 | * Sample Database view 11 | */ 12 | 13 | const colectionName = "shakeout-tests"; 14 | const docName = "rows"; 15 | 16 | @observer 17 | export default class DatabaseView extends React.Component<{}, {}> { 18 | 19 | private key: string; 20 | private val: string; 21 | 22 | constructor(props: any) { 23 | super(props); 24 | this.getValues(); 25 | } 26 | 27 | /** 28 | * Gets the "shakeout-tests" collection and "rows" object from firestore 29 | */ 30 | private async deleteValues() { 31 | 32 | firebase.firestore() 33 | .collection(colectionName) 34 | .doc(docName).delete(); 35 | 36 | } 37 | 38 | /** 39 | * Gets the "shakeout-tests" collection and "rows" object from firestore 40 | */ 41 | private async getValues() { 42 | 43 | const unsubscribe = firebase.firestore() 44 | .collection(colectionName) 45 | .doc(docName) 46 | .onSnapshot(snapShot => DatabaseState.rows = snapShot.data()); 47 | 48 | //save the unsub function and execute it before logging out 49 | LoginState.subscriptions.push(unsubscribe); 50 | 51 | } 52 | 53 | 54 | /** 55 | * Updates the "rows" object in the collection "shakeout-tests" 56 | */ 57 | private async setValue() { 58 | const update = {}; 59 | update[this.key] = this.val; 60 | firebase.firestore() 61 | .collection(colectionName).doc(docName) 62 | .set(update, { merge: true }); 63 | } 64 | 65 | private onAddButtonClicked() { 66 | 67 | if (this.key && this.val) { 68 | this.setValue(); 69 | } 70 | 71 | } 72 | 73 | private onKeyChange(evt: any) { 74 | 75 | this.key = evt.target.value; 76 | 77 | } 78 | 79 | private onValChange(evt: any) { 80 | 81 | this.val = evt.target.value; 82 | 83 | } 84 | 85 | render() { 86 | 87 | const rows = []; 88 | for (let key in DatabaseState.rows) { 89 | rows.push(
  • {key} | {DatabaseState.rows[key]}
  • ); 90 | } 91 | 92 | return ( 93 |
    94 | {Messages.get("shakeout-test-database")} 95 |
      96 |
    • {Messages.get("shakeout-test-key")}
    • 97 |
    • {Messages.get("shakeout-test-val")}
    • 98 |
    • 99 |
    • 100 | {rows} 101 |
    102 |
    103 | ); 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/components/login/AccountMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { Messages } from "../../services/Messages"; 4 | import { LoginState } from "./LoginState"; 5 | import { Routing } from "../../services/Routing"; 6 | import "../../Common.scss"; 7 | import * as firebase from "firebase/app"; 8 | import { Redirect } from "react-router"; 9 | 10 | /** 11 | * Sample login / log out component 12 | */ 13 | 14 | @observer 15 | export default class AccountMenu extends React.Component<{}, {}> { 16 | 17 | constructor(props: any) { 18 | 19 | super(props); 20 | this.listenForLogin(); 21 | 22 | } 23 | 24 | /** 25 | * Listesn for login via the FirebaseUI (see Login.tsx) 26 | */ 27 | private listenForLogin() { 28 | 29 | firebase.app().auth().onAuthStateChanged(user => LoginState.user = user); 30 | 31 | } 32 | 33 | /** 34 | * Clear firebase references and signs the user out when the logout link is clicked 35 | */ 36 | 37 | private onClickLogout() { 38 | 39 | LoginState.subscriptions.filter(unsub => !!unsub).forEach(unsub => unsub()); 40 | 41 | firebase.auth().signOut().then(() => { 42 | LoginState.user = null; 43 | Routing.redirect = Routing.HOME; 44 | }, (error) => { 45 | console.error(error); 46 | }); 47 | 48 | } 49 | 50 | private onClickLogin() { 51 | 52 | Routing.redirect = Routing.LOGIN; 53 | 54 | } 55 | 56 | render() { 57 | return ( 58 |
    59 | {Messages.get("shakeout-test-auth")} 60 | 63 | {Messages.get("shakeout-test-logout")} 64 | 65 | 68 | {Messages.get("shakeout-test-login")} 69 | 70 | {LoginState.user ? Messages.get("welcome") + " " + LoginState.user.email : ""} 71 | {Routing.redirect ? : null} 72 |
    73 | ); 74 | } 75 | } -------------------------------------------------------------------------------- /src/components/login/Login.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import StyledFirebaseAuth from "react-firebaseui/StyledFirebaseAuth"; 3 | import { observer } from "mobx-react"; 4 | import * as firebase from "firebase/app"; 5 | import 'firebase/auth'; 6 | 7 | // Configure FirebaseUI. 8 | const uiConfig = { 9 | // Popup signin flow rather than redirect flow. 10 | signInFlow: "popup", 11 | // Redirect to / after sign in is successful. Alternatively you can provide a callbacks.signInSuccess function. 12 | signInSuccessUrl: "/", 13 | // We will display Google and Facebook as auth providers. 14 | signInOptions: [ 15 | firebase.auth.EmailAuthProvider.PROVIDER_ID 16 | ] 17 | }; 18 | 19 | /** 20 | * Sample Firebase login component 21 | */ 22 | @observer 23 | export default class Login extends React.Component { 24 | 25 | render() { 26 | return ( 27 | 28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /src/components/login/LoginState.ts: -------------------------------------------------------------------------------- 1 | import { observable } from "mobx"; 2 | 3 | /** 4 | * Sample Login State 5 | */ 6 | export class LoginStore { 7 | 8 | @observable public user: firebase.User; 9 | public subscriptions: any[] = []; 10 | 11 | } 12 | 13 | export const LoginState = new LoginStore(); -------------------------------------------------------------------------------- /src/components/upload/Upload.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { Messages } from "../../services/Messages"; 4 | import { UploadState } from "./UploadState"; 5 | import * as firebase from "firebase/app"; 6 | import "firebase/storage"; 7 | 8 | /** 9 | * Sample Upload / download / delete 10 | */ 11 | 12 | declare const window: { 13 | File: any 14 | FileReader: any 15 | FileList: any 16 | Blob: any 17 | }; 18 | 19 | const uploadFolder = "shakeout-tests"; 20 | 21 | @observer 22 | export default class Upload extends React.Component<{}, {}> { 23 | 24 | constructor(props: any) { 25 | super(props); 26 | this.getImages(); 27 | } 28 | 29 | /** 30 | * Uploads a File[] to a folder 31 | * @param folder folder to up load to 32 | * @param uploads files to upload 33 | */ 34 | private uploadFiles(folder: string, uploads: File[]): Promise { 35 | return Promise.all(Array.from(uploads).map(file => this.uploadFile(folder, file))); 36 | } 37 | 38 | /** 39 | * Uploads a File to a folder 40 | * @param folder folder to up load to 41 | * @param file file to upload 42 | */ 43 | 44 | private uploadFile(folder: string, file: File): Promise { 45 | 46 | return new Promise((resolve, reject) => { 47 | const folderRef = firebase.app().storage().ref(folder); 48 | const fileRef = folderRef.child(file.name); 49 | const uploadTask = fileRef.put(file); 50 | uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED, null, reject, resolve); 51 | }); 52 | 53 | } 54 | 55 | private async getFiles(): Promise { 56 | 57 | UploadState.urls.splice(0, UploadState.urls.length); 58 | return await firebase.app().storage().ref(uploadFolder).listAll(); 59 | 60 | } 61 | 62 | private async deleteImages() { 63 | 64 | this.getFiles().then(result => result.items.forEach(async item => await item.delete())); 65 | 66 | } 67 | 68 | private async getImages() { 69 | 70 | this.getFiles().then(result => 71 | result.items.forEach(item => item.getDownloadURL().then(url => 72 | UploadState.urls.push(url)))); 73 | 74 | } 75 | 76 | private onUploadButtonClicked(evt: any) { 77 | 78 | if (window.File && window.FileReader && window.FileList && window.Blob) { 79 | const files = evt.target.files; // FileList object 80 | this.uploadFiles(uploadFolder, files).then(this.getImages.bind(this)); 81 | } 82 | 83 | } 84 | 85 | render() { 86 | return ( 87 |
    88 | {Messages.get("shakeout-test-upload")} 89 |
      90 |
    • 95 |
    • 96 | {UploadState.imageUrls.map(url =>
    • )} 97 |
    98 |
    99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/components/upload/UploadState.ts: -------------------------------------------------------------------------------- 1 | import {observable, computed} from "mobx"; 2 | 3 | /** 4 | * Sample upload state 5 | */ 6 | export class UploadStore { 7 | 8 | @observable public urls: string[] = []; 9 | 10 | @computed get imageUrls() { 11 | return this.urls; 12 | } 13 | 14 | } 15 | 16 | export const UploadState = new UploadStore(); -------------------------------------------------------------------------------- /src/index.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { render } from "react-dom"; 3 | import { AppContainer } from "react-hot-loader"; 4 | import App from "./components/App"; 5 | import { Configuration } from "./services/Configuration"; 6 | 7 | declare const require: (path: string) => { default: any; }; 8 | 9 | /** 10 | * Sample index compnent with live hot swap 11 | */ 12 | 13 | const rootEl = document.getElementById("root"); 14 | 15 | Configuration.configure("config.json") 16 | .then(() => render( 17 | 18 | 19 | , 20 | rootEl)); 21 | 22 | // Hot Module Replacement API 23 | declare let module: { hot: any }; 24 | 25 | if (module.hot) { 26 | module.hot.accept("./components/App", () => { 27 | const NewApp = require("./components/App").default; 28 | Configuration.configure("config.json") 29 | .then(() => render( 30 | 31 | 32 | , 33 | rootEl)); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/services/Configuration.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from "./Messages"; 2 | 3 | /** 4 | * Configuration service 5 | */ 6 | class ConfigurationService { 7 | 8 | private static configuration = {}; 9 | 10 | /** 11 | * Retrieves configuration object for a configured field 12 | * @param field field to retrieve 13 | */ 14 | public getConfig(field: string): any { 15 | 16 | return ConfigurationService.configuration[field]; 17 | 18 | } 19 | 20 | /** 21 | * Sets the configuration object 22 | * @param config configuration object eg: https://github.com/naishtech/platform-agnostic-typescript-template/blob/master/static/config/dev/config.json 23 | */ 24 | public setConfig(config: any): void { 25 | 26 | ConfigurationService.configuration = config; 27 | 28 | } 29 | 30 | /** 31 | * Reads local configuration and fetches messages from server. 32 | * @param url 33 | */ 34 | public configure(url: string): Promise { 35 | 36 | return ConfigurationService.fetchSameOrigin(url) 37 | .then(ConfigurationService.onConfigResponse) 38 | .then(() => ConfigurationService.fetchReferencedConfigurations()); 39 | 40 | } 41 | 42 | private static getConfiguration(): any { 43 | 44 | return ConfigurationService.configuration; 45 | 46 | } 47 | 48 | private static fetchReferencedConfigurations(): Promise { 49 | 50 | return ConfigurationService.fetchSameOrigin(ConfigurationService.getConfiguration()["messages"]) 51 | .then(Messages.onMessagesResponse); 52 | 53 | } 54 | 55 | private static fetchSameOrigin(url: string) { 56 | 57 | return new Promise((resolve, reject) => { 58 | const xhr = new XMLHttpRequest(); 59 | xhr.onload = function () { 60 | resolve(new Response(xhr.responseText, { status: xhr.status })); 61 | }; 62 | xhr.onerror = function () { 63 | reject(new TypeError("Same origin request failed")); 64 | }; 65 | xhr.open("GET", url); 66 | xhr.send(null); 67 | }); 68 | 69 | } 70 | 71 | private static onConfigResponse(response: Response): Promise { 72 | 73 | return response.json().then(ConfigurationService.onConfigParsed); 74 | 75 | } 76 | 77 | private static onConfigParsed(result: any): void { 78 | 79 | ConfigurationService.configuration = result["config"]; 80 | 81 | } 82 | 83 | } 84 | 85 | export const Configuration = new ConfigurationService(); -------------------------------------------------------------------------------- /src/services/Messages.ts: -------------------------------------------------------------------------------- 1 | class I18nService { 2 | 3 | private static messages: any; 4 | 5 | /** 6 | * Sets a new message configuration 7 | * @param messagesConfig new message config eg: https://github.com/naishtech/platform-agnostic-typescript-template/blob/master/static/messages/messages.json 8 | */ 9 | public setMessages(messagesConfig: any): void { 10 | 11 | I18nService.messages = messagesConfig; 12 | 13 | } 14 | 15 | /** 16 | * @param key message key 17 | * @param country country as configured in messages.config (eg: en) 18 | * @param locale country as configured in messages.config (eg: US) 19 | */ 20 | public get(key: string, country?: string, locale?: string): string { 21 | 22 | let message = this.getMessage(key, country, locale); 23 | return message ? message : key; 24 | 25 | } 26 | 27 | private getMessage(key: string, country?: string, locale?: string) { 28 | 29 | country = country ? country : "en"; 30 | locale = locale ? locale : "US"; 31 | return I18nService.messages[country + "-" + locale][key]; 32 | 33 | } 34 | 35 | private static configure(messagesConfig: any): void { 36 | 37 | I18nService.messages = messagesConfig; 38 | 39 | } 40 | 41 | public onMessagesResponse(response: Response): Promise { 42 | 43 | return response.json().then(I18nService.onMessagesParsed); 44 | 45 | } 46 | 47 | private static onMessagesParsed(result: any): void { 48 | 49 | I18nService.configure(result["messages"]); 50 | 51 | } 52 | 53 | public format(key: string, values: any[], country?: string, locale?: string) { 54 | 55 | return this.get(key, country, locale).replace(/{(\d+)}/g, (match, num) => { 56 | return typeof values[num] !== "undefined" 57 | ? values[num] 58 | : match; 59 | }); 60 | 61 | } 62 | 63 | } 64 | 65 | export const Messages = new I18nService(); 66 | -------------------------------------------------------------------------------- /src/services/Routing.ts: -------------------------------------------------------------------------------- 1 | import { observable } from "mobx"; 2 | 3 | /** 4 | * Route state 5 | */ 6 | class RoutingService { 7 | 8 | @observable redirect: string; 9 | public HOME: string = "/"; 10 | public LOGIN: string = "/login"; 11 | 12 | } 13 | 14 | export const Routing = new RoutingService(); -------------------------------------------------------------------------------- /static/config/dev/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "config" : { 3 | "firebase" : { 4 | "apiKey": "your_api_key", 5 | "authDomain": "your.firebaseapp.com", 6 | "databaseURL": "https://your.firebaseio.com", 7 | "projectId": "your-project-id", 8 | "storageBucket": "your.appspot.com", 9 | "messagingSenderId": "123456789012" 10 | }, 11 | "analytics":{ 12 | "google":{ 13 | "config":"your_ga_key" 14 | } 15 | }, 16 | "messages" : "messages.json" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /static/config/prod/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "config" : { 3 | "firebase" : { 4 | "apiKey": "your_api_key", 5 | "authDomain": "your.firebaseapp.com", 6 | "databaseURL": "https://your.firebaseio.com", 7 | "projectId": "your-project-id", 8 | "storageBucket": "your.appspot.com", 9 | "messagingSenderId": "123456789012" 10 | }, 11 | "analytics":{ 12 | "google":{ 13 | "config":"your_ga_key" 14 | } 15 | }, 16 | "messages" : "messages.json" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /static/config/test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "config" : { 3 | "firebase" : { 4 | "apiKey": "your_api_key", 5 | "authDomain": "your.firebaseapp.com", 6 | "databaseURL": "https://your.firebaseio.com", 7 | "projectId": "your-project-id", 8 | "storageBucket": "your.appspot.com", 9 | "messagingSenderId": "123456789012" 10 | }, 11 | "analytics":{ 12 | "google":{ 13 | "config":"your_ga_key" 14 | } 15 | }, 16 | "messages" : "messages.json" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naishtech/platform-agnostic-typescript-template/4bd07847a5c8e2af622083bf15500988378985f1/static/images/favicon.ico -------------------------------------------------------------------------------- /static/messages/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": { 3 | "en-US" : { 4 | "home-title":"Platform Agnostic TypeScript Template (PATT)", 5 | "welcome" : "Welcome", 6 | "shakeout-test-auth":"Firebase Authentication:", 7 | "shakeout-test-database":"Firebase Realtime Database:", 8 | "shakeout-test-key":"Test Key:", 9 | "shakeout-test-val":"Test Value:", 10 | "shakeout-test-add":"Add", 11 | "shakeout-test-delete":"Delete all", 12 | "shakeout-test-upload":"Firebase Storage Upload:", 13 | "shakeout-test-logout" : "Test Log out", 14 | "shakeout-test-login" : "Test Log in" 15 | } 16 | } 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": false, 6 | "module": "commonjs", 7 | "target": "es6", 8 | "jsx": "react", 9 | "lib": ["es5", "es6", "dom"], 10 | "experimentalDecorators": true, 11 | "esModuleInterop": true 12 | }, 13 | "include": [ 14 | "./src/**/*", "webpack", "jest" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /webpack/common.js: -------------------------------------------------------------------------------- 1 | // shared config (dev and prod) 2 | const {resolve} = require('path'); 3 | const {CheckerPlugin} = require('awesome-typescript-loader'); 4 | const StyleLintPlugin = require('stylelint-webpack-plugin'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | module.exports = { 8 | resolve: { 9 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 10 | }, 11 | context: resolve(__dirname, '../src'), 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | use: ['babel-loader', 'source-map-loader'], 17 | exclude: /node_modules/, 18 | }, 19 | { 20 | test: /\.tsx?$/, 21 | use: ['babel-loader', 'awesome-typescript-loader'], 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 1 } }, 'postcss-loader',], 26 | }, 27 | { 28 | test: /\.scss$/, 29 | loaders: [ 30 | 'style-loader', 31 | { loader: 'css-loader', options: { importLoaders: 1 } }, 32 | 'postcss-loader', 33 | 'sass-loader', 34 | ], 35 | }, 36 | { 37 | test: /\.(jpe?g|png|gif|svg)$/i, 38 | loaders: [ 39 | 'file-loader?hash=sha512&digest=hex&name=img/[hash].[ext]', 40 | 'image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false', 41 | ], 42 | }, 43 | ], 44 | }, 45 | plugins: [ 46 | new CheckerPlugin(), 47 | new HtmlWebpackPlugin( 48 | { 49 | template: 'index.html.ejs', 50 | favicon: '../static/images/favicon.ico', 51 | title:"Platform Agnostic TypeScript Template", 52 | meta: { 53 | "charset": "UTF-8", 54 | "viewport": "width=device-width, initial-scale=1, shrink-to-fit=no", 55 | "name":"Platform Agnostic TypeScript Template", 56 | "description":"Template for making full stack mulit-platform applications in TypeScript", 57 | "keywords":"Platform Agnostic TypeScript Template", 58 | "title":"Platform Agnostic TypeScript Template" 59 | }, 60 | } 61 | ), 62 | ], 63 | externals: { 64 | 'react': 'React', 65 | 'react-dom': 'ReactDOM', 66 | }, 67 | performance: { 68 | hints: false, 69 | }, 70 | }; 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /webpack/dev.js: -------------------------------------------------------------------------------- 1 | // development config 2 | const merge = require('webpack-merge'); 3 | const webpack = require('webpack'); 4 | const commonConfig = require('./common'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | 7 | module.exports = merge(commonConfig, { 8 | mode: 'development', 9 | entry: [ 10 | 'react-hot-loader/patch', // activate HMR for React 11 | 'webpack-dev-server/client?http://localhost:8080',// bundle the client for webpack-dev-server and connect to the provided endpoint 12 | 'webpack/hot/only-dev-server', // bundle the client for hot reloading, only- means to only hot reload for successful updates 13 | './index.tsx' // the entry point of our app 14 | ], 15 | devServer: { 16 | hot: true, // enable HMR on the server 17 | }, 18 | devtool: 'cheap-module-eval-source-map', 19 | plugins: [ 20 | new webpack.HotModuleReplacementPlugin(), // enable HMR globally 21 | new webpack.NamedModulesPlugin(), // prints more readable module names in the browser console on HMR updates 22 | new CopyWebpackPlugin([ 23 | { from: '../static/config/dev' }, 24 | { from: '../static/images' }, 25 | { from: '../static/messages' } 26 | ]), 27 | new webpack.DefinePlugin({ 28 | "process.env": { 29 | NODE_ENV: JSON.stringify("development") 30 | } 31 | }) 32 | ], 33 | }); 34 | -------------------------------------------------------------------------------- /webpack/prod.js: -------------------------------------------------------------------------------- 1 | // production config 2 | const merge = require('webpack-merge'); 3 | const {resolve} = require('path'); 4 | const commonConfig = require('./common'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | 7 | 8 | module.exports = merge(commonConfig, { 9 | mode: 'production', 10 | entry: './index.tsx', 11 | output: { 12 | filename: 'js/bundle.[hash].min.js', 13 | path: resolve(__dirname, '../dist'), 14 | publicPath: '/', 15 | }, 16 | devtool: 'source-map', 17 | plugins: [ 18 | new CopyWebpackPlugin([ 19 | { from: '../static/config/prod' }, 20 | { from: '../static/images' }, 21 | { from: '../static/messages' }, 22 | ]) 23 | ], 24 | }); 25 | -------------------------------------------------------------------------------- /webpack/test.js: -------------------------------------------------------------------------------- 1 | // test config 2 | const merge = require('webpack-merge'); 3 | const {resolve} = require('path'); 4 | const commonConfig = require('./common'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | 7 | 8 | module.exports = merge(commonConfig, { 9 | mode: 'production', 10 | entry: './index.tsx', 11 | output: { 12 | filename: 'js/bundle.[hash].min.js', 13 | path: resolve(__dirname, '../dist'), 14 | publicPath: '/', 15 | }, 16 | devtool: 'source-map', 17 | plugins: [ 18 | new CopyWebpackPlugin([ 19 | { from: '../static/config/test' }, 20 | { from: '../static/images' }, 21 | { from: '../static/messages' }, 22 | ]) 23 | ], 24 | }); 25 | --------------------------------------------------------------------------------