├── Chapter01 ├── .gitignore ├── package.json ├── public │ ├── assets │ │ ├── app.css │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── index.html │ └── secrets.js └── yarn.lock ├── Chapter02 ├── .gitignore ├── package.json ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── bundle.js │ └── index.html ├── scripts │ └── copy_assets.js ├── src │ ├── App.js │ ├── app.css │ ├── components │ │ ├── Component1.js │ │ ├── Component2.js │ │ └── Component3.js │ └── index.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock ├── Chapter03 ├── .gitignore ├── package.json ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── bundle.js │ └── index.html ├── scripts │ └── copy_assets.js ├── src │ ├── components │ │ ├── App.js │ │ ├── Header.js │ │ ├── LoginContainer.js │ │ └── app.css │ └── index.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock ├── Chapter04 ├── .firebaserc ├── .gitignore ├── firebase.json ├── functions │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package.json ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── bundle.js │ └── index.html ├── scripts │ └── copy_assets.js ├── src │ ├── components │ │ ├── App.js │ │ ├── Header.js │ │ ├── LoginContainer.js │ │ └── app.css │ └── index.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock ├── Chapter05 ├── .firebaserc ├── .gitignore ├── firebase.json ├── functions │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package.json ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── bundle.js │ └── index.html ├── scripts │ └── copy_assets.js ├── src │ ├── components │ │ ├── App.js │ │ ├── ChatContainer.js │ │ ├── Header.js │ │ ├── LoginContainer.js │ │ ├── UserContainer.js │ │ └── app.css │ └── index.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock ├── Chapter06 ├── .firebaserc ├── .gitignore ├── firebase.json ├── functions │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package.json ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── bundle.js │ └── index.html ├── scripts │ └── copy_assets.js ├── src │ ├── components │ │ ├── App.js │ │ ├── ChatContainer.js │ │ ├── Header.js │ │ ├── LoginContainer.js │ │ ├── UserContainer.js │ │ └── app.css │ └── index.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock ├── Chapter07 ├── .firebaserc ├── .gitignore ├── firebase.json ├── functions │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package.json ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── bundle.js │ ├── firebase-messaging-sw.js │ ├── index.html │ └── manifest.json ├── scripts │ └── copy_assets.js ├── src │ ├── components │ │ ├── App.js │ │ ├── ChatContainer.js │ │ ├── Header.js │ │ ├── LoginContainer.js │ │ ├── UserContainer.js │ │ └── app.css │ └── index.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock ├── Chapter08 ├── .firebaserc ├── .gitignore ├── firebase.json ├── functions │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package.json ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── bundle.js │ ├── firebase-messaging-sw.js │ ├── index.html │ └── manifest.json ├── scripts │ └── copy_assets.js ├── src │ ├── components │ │ ├── App.js │ │ ├── ChatContainer.js │ │ ├── Header.js │ │ ├── LoginContainer.js │ │ ├── UserContainer.js │ │ └── app.css │ ├── index.js │ └── resources │ │ └── NotificationResource.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock ├── Chapter09 ├── .firebaserc ├── .gitignore ├── firebase.json ├── functions │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package.json ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── bundle.js │ ├── index.html │ └── manifest.json ├── scripts │ └── copy_assets.js ├── src │ ├── components │ │ ├── App.js │ │ ├── ChatContainer.js │ │ ├── Header.js │ │ ├── LoginContainer.js │ │ ├── UserContainer.js │ │ └── app.css │ └── index.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock ├── Chapter10 ├── .firebaserc ├── .gitignore ├── firebase.json ├── functions │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package.json ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── bundle.js │ ├── firebase-messaging-sw.js │ ├── index.html │ └── manifest.json ├── scripts │ └── copy_assets.js ├── src │ ├── components │ │ ├── App.js │ │ ├── ChatContainer.js │ │ ├── Header.js │ │ ├── LoginContainer.js │ │ ├── UserContainer.js │ │ └── app.css │ ├── index.js │ └── resources │ │ └── NotificationResource.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock ├── Chapter11 ├── .firebaserc ├── .gitignore ├── firebase.json ├── functions │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package.json ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── bundle.js │ ├── firebase-messaging-sw.js │ ├── index.html │ └── manifest.json ├── scripts │ └── copy_assets.js ├── src │ ├── components │ │ ├── App.js │ │ ├── AsyncComponent.js │ │ ├── ChatContainer.js │ │ ├── Header.js │ │ ├── LoginContainer.js │ │ ├── UserContainer.js │ │ └── app.css │ ├── index.js │ └── resources │ │ └── NotificationResource.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock ├── Chapter12 ├── .firebaserc ├── .gitignore ├── firebase.json ├── functions │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package.json ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-384.png │ │ ├── icon-512.png │ │ ├── icon.png │ │ └── splash.png │ ├── bundle.js │ ├── firebase-messaging-sw.js │ ├── index.html │ └── manifest.json ├── scripts │ └── copy_assets.js ├── src │ ├── components │ │ ├── App.js │ │ ├── AsyncComponent.js │ │ ├── ChatContainer.js │ │ ├── Header.js │ │ ├── LoginContainer.js │ │ ├── UserContainer.js │ │ └── app.css │ ├── index.js │ └── resources │ │ └── NotificationResource.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock ├── LICENSE ├── README.md └── master ├── .firebaserc ├── .gitignore ├── firebase.json ├── functions ├── index.js ├── package-lock.json └── package.json ├── package.json ├── public ├── assets │ ├── favicon.ico │ ├── icon-256.png │ ├── icon-384.png │ ├── icon-512.png │ ├── icon.png │ └── splash.png ├── bundle.js ├── firebase-messaging-sw.js ├── index.html └── manifest.json ├── scripts └── copy_assets.js ├── src ├── components │ ├── App.js │ ├── AsyncComponent.js │ ├── ChatContainer.js │ ├── Header.js │ ├── LoginContainer.js │ ├── UserContainer.js │ └── app.css ├── index.js └── resources │ └── NotificationResource.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock /Chapter01/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /Chapter01/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "react": "^15.6.1", 10 | "react-dom": "^15.6.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Chapter01/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter01/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter01/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter01/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter01/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter01/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter01/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter01/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter01/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter01/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter01/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter01/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter01/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

Hello world!

12 |
13 | 14 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /Chapter01/public/secrets.js: -------------------------------------------------------------------------------- 1 | window.apiKey = "AIzaSyBlhnemwIkdtQhv9M8un9BoHIQeLHPfm_g"; 2 | window.messagingSenderId = "120582509244" 3 | -------------------------------------------------------------------------------- /Chapter02/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /Chapter02/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server" 11 | }, 12 | "dependencies": { 13 | "babel-core": "^6.25.0", 14 | "babel-loader": "^7.1.1", 15 | "babel-plugin-transform-class-properties": "^6.24.1", 16 | "babel-preset-es2015": "^6.24.1", 17 | "babel-preset-react": "^6.24.1", 18 | "css-loader": "^0.28.4", 19 | "file-loader": "^0.11.2", 20 | "fs-extra": "^4.0.1", 21 | "html-webpack-plugin": "^2.30.1", 22 | "react": "^15.6.1", 23 | "react-dom": "^15.6.1", 24 | "react-hot-loader": "3.0.0", 25 | "style-loader": "^0.18.2", 26 | "webpack": "^3.5.4", 27 | "webpack-dev-server": "^2.7.1", 28 | "webpack-manifest-plugin": "^1.3.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Chapter02/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter02/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter02/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter02/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter02/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter02/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter02/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter02/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter02/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter02/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter02/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter02/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter02/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

Hello world!

11 |
12 | 13 | -------------------------------------------------------------------------------- /Chapter02/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /Chapter02/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './app.css'; 3 | 4 | const App = () => { 5 | return

Hello from React!!

6 | }; 7 | 8 | export default App; -------------------------------------------------------------------------------- /Chapter02/src/components/Component1.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter02/src/components/Component1.js -------------------------------------------------------------------------------- /Chapter02/src/components/Component2.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter02/src/components/Component2.js -------------------------------------------------------------------------------- /Chapter02/src/components/Component3.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter02/src/components/Component3.js -------------------------------------------------------------------------------- /Chapter02/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | 7 | if (module.hot) { 8 | module.hot.accept('./App', () => { 9 | const NextApp = require('./App').default; 10 | ReactDOM.render( 11 | , 12 | document.getElementById('root') 13 | ); 14 | }); 15 | } -------------------------------------------------------------------------------- /Chapter02/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:8080', 8 | 'webpack/hot/only-dev-server', 9 | __dirname + "/src/index.js" 10 | ], 11 | output: { 12 | path: __dirname + "/public", 13 | filename: "bundle.js", 14 | publicPath: "/" 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | query: { 23 | presets: ['es2015','react'], 24 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 25 | } 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { loader: "style-loader" }, 31 | { loader: "css-loader" } 32 | ] 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.HotModuleReplacementPlugin(), 38 | new HtmlWebpackPlugin({ 39 | inject: true, 40 | template: __dirname + '/public/index.html', 41 | }) 42 | ], 43 | devServer: { 44 | contentBase: "./public", 45 | historyApiFallback: true, 46 | inline: true, 47 | hot: true 48 | } 49 | }; -------------------------------------------------------------------------------- /Chapter02/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/src/index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'bundle.js', 10 | publicPath: './' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | query: { 19 | presets: ['es2015', 'react'], 20 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 21 | } 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 26 | }, 27 | { 28 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.json$/], 29 | loader: 'file-loader', 30 | options: { 31 | name: 'static/media/[name].[ext]' 32 | } 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify('production') 40 | } 41 | }), 42 | new HtmlWebpackPlugin({ 43 | inject: true, 44 | template: __dirname + '/public/index.html', 45 | minify: { 46 | removeComments: true, 47 | collapseWhitespace: true, 48 | removeRedundantAttributes: true, 49 | useShortDoctype: true, 50 | removeEmptyAttributes: true, 51 | removeStyleLinkTypeAttributes: true, 52 | keepClosingSlash: true, 53 | minifyJS: true, 54 | minifyCSS: true, 55 | minifyURLs: true 56 | } 57 | }), 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false, 61 | reduce_vars: false 62 | }, 63 | output: { 64 | comments: false 65 | }, 66 | sourceMap: true 67 | }), 68 | new ManifestPlugin({ 69 | fileName: 'asset-manifest.json' 70 | }) 71 | ] 72 | }; 73 | -------------------------------------------------------------------------------- /Chapter03/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /Chapter03/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server" 11 | }, 12 | "dependencies": { 13 | "babel-core": "^6.25.0", 14 | "babel-loader": "^7.1.1", 15 | "babel-plugin-transform-class-properties": "^6.24.1", 16 | "babel-preset-es2015": "^6.24.1", 17 | "babel-preset-react": "^6.24.1", 18 | "css-loader": "^0.28.4", 19 | "file-loader": "^0.11.2", 20 | "fs-extra": "^4.0.1", 21 | "html-webpack-plugin": "^2.30.1", 22 | "react": "^15.6.1", 23 | "react-dom": "^15.6.1", 24 | "react-hot-loader": "3.0.0", 25 | "style-loader": "^0.18.2", 26 | "webpack": "^3.5.4", 27 | "webpack-dev-server": "^2.7.1", 28 | "webpack-manifest-plugin": "^1.3.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Chapter03/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter03/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter03/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter03/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter03/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter03/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter03/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter03/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter03/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter03/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter03/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter03/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter03/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /Chapter03/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /Chapter03/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import LoginContainer from './LoginContainer'; 3 | import './app.css'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /Chapter03/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = () => { 4 | return ( 5 | 9 | ); 10 | }; 11 | 12 | export default Header; 13 | -------------------------------------------------------------------------------- /Chapter03/src/components/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | 4 | class LoginContainer extends Component { 5 | state = { email: '', password: '' }; 6 | 7 | handleEmailChange = event => { 8 | this.setState({ email: event.target.value }); 9 | }; 10 | 11 | handlePasswordChange = event => { 12 | this.setState({ password: event.target.value }); 13 | }; 14 | 15 | handleSubmit = event => { 16 | event.preventDefault(); 17 | console.log(this.state); 18 | }; 19 | 20 | render() { 21 | return ( 22 |
23 |
24 |
25 |

Sign in or sign up by entering your email and password.

26 | 32 | 38 | 41 |
42 |
43 | ); 44 | } 45 | } 46 | 47 | export default LoginContainer; 48 | -------------------------------------------------------------------------------- /Chapter03/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App' 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | 7 | if (module.hot) { 8 | module.hot.accept('./components/App', () => { 9 | const NextApp = require('./components/App').default; 10 | ReactDOM.render( 11 | , 12 | document.getElementById('root') 13 | ); 14 | }); 15 | } -------------------------------------------------------------------------------- /Chapter03/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:8080', 8 | 'webpack/hot/only-dev-server', 9 | __dirname + "/src/index.js" 10 | ], 11 | output: { 12 | path: __dirname + "/public", 13 | filename: "bundle.js" 14 | }, 15 | module: { 16 | loaders: [ 17 | { 18 | test: /\.js$/, 19 | exclude: /node_modules/, 20 | loader: 'babel-loader', 21 | query: { 22 | presets: ['es2015','react'], 23 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 24 | } 25 | }, 26 | { 27 | test: /\.css$/, 28 | use: [ 29 | { loader: "style-loader" }, 30 | { loader: "css-loader" } 31 | ] 32 | } 33 | ] 34 | }, 35 | plugins: [ 36 | new webpack.HotModuleReplacementPlugin(), 37 | new HtmlWebpackPlugin({ 38 | inject: true, 39 | template: __dirname + '/public/index.html', 40 | }) 41 | ], 42 | devServer: { 43 | contentBase: "./public", 44 | historyApiFallback: true, 45 | inline: true, 46 | hot: true 47 | } 48 | }; -------------------------------------------------------------------------------- /Chapter03/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/src/index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'bundle.js', 10 | publicPath: './' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | query: { 19 | presets: ['es2015', 'react'], 20 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 21 | } 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 26 | }, 27 | { 28 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.json$/], 29 | loader: 'file-loader', 30 | options: { 31 | name: 'static/media/[name].[ext]' 32 | } 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify('production') 40 | } 41 | }), 42 | new HtmlWebpackPlugin({ 43 | inject: true, 44 | template: __dirname + '/public/index.html', 45 | minify: { 46 | removeComments: true, 47 | collapseWhitespace: true, 48 | removeRedundantAttributes: true, 49 | useShortDoctype: true, 50 | removeEmptyAttributes: true, 51 | removeStyleLinkTypeAttributes: true, 52 | keepClosingSlash: true, 53 | minifyJS: true, 54 | minifyCSS: true, 55 | minifyURLs: true 56 | } 57 | }), 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false, 61 | reduce_vars: false 62 | }, 63 | output: { 64 | comments: false 65 | }, 66 | sourceMap: true 67 | }), 68 | new ManifestPlugin({ 69 | fileName: 'asset-manifest.json' 70 | }) 71 | ] 72 | }; 73 | -------------------------------------------------------------------------------- /Chapter04/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "chatastrophe-draft" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Chapter04/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /public/secrets.js 4 | -------------------------------------------------------------------------------- /Chapter04/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Chapter04/functions/index.js: -------------------------------------------------------------------------------- 1 | var functions = require('firebase-functions'); 2 | 3 | // // Create and Deploy Your First Cloud Functions 4 | // // https://firebase.google.com/docs/functions/write-firebase-functions 5 | // 6 | // exports.helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }); 9 | -------------------------------------------------------------------------------- /Chapter04/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "~4.2.1", 6 | "firebase-functions": "^0.5.7" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /Chapter04/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server", 11 | "deploy": "npm run build && firebase deploy" 12 | }, 13 | "dependencies": { 14 | "babel-core": "^6.25.0", 15 | "babel-loader": "^7.1.1", 16 | "babel-plugin-transform-class-properties": "^6.24.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "css-loader": "^0.28.4", 20 | "file-loader": "^0.11.2", 21 | "fs-extra": "^4.0.1", 22 | "html-webpack-plugin": "^2.30.1", 23 | "react": "^15.6.1", 24 | "react-dom": "^15.6.1", 25 | "react-hot-loader": "3.0.0", 26 | "style-loader": "^0.18.2", 27 | "webpack": "^3.5.4", 28 | "webpack-dev-server": "^2.7.1", 29 | "webpack-manifest-plugin": "^1.3.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Chapter04/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter04/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter04/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter04/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter04/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter04/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter04/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter04/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter04/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter04/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter04/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter04/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter04/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 25 | 26 | -------------------------------------------------------------------------------- /Chapter04/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /Chapter04/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import LoginContainer from './LoginContainer'; 3 | import './app.css'; 4 | 5 | class App extends Component { 6 | state = { user: null }; 7 | 8 | componentDidMount() { 9 | firebase.auth().onAuthStateChanged(user => { 10 | if (user) { 11 | this.setState({ user }); 12 | } 13 | }); 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 | 20 |
21 | ); 22 | } 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /Chapter04/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = () => { 4 | return ( 5 | 9 | ); 10 | }; 11 | 12 | export default Header; 13 | -------------------------------------------------------------------------------- /Chapter04/src/components/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | 4 | class LoginContainer extends Component { 5 | state = { email: '', password: '', error: '' }; 6 | 7 | handleEmailChange = event => { 8 | this.setState({ email: event.target.value }); 9 | }; 10 | 11 | handlePasswordChange = event => { 12 | this.setState({ password: event.target.value }); 13 | }; 14 | 15 | handleSubmit = event => { 16 | event.preventDefault(); 17 | this.setState({ error: '' }); 18 | if (this.state.email && this.state.password) { 19 | this.login(); 20 | } else { 21 | this.setState({ error: 'Please fill in both fields.' }); 22 | } 23 | }; 24 | 25 | login() { 26 | firebase 27 | .auth() 28 | .signInWithEmailAndPassword(this.state.email, this.state.password) 29 | .then(res => { 30 | console.log(res); 31 | }) 32 | .catch(error => { 33 | if (error.code === 'auth/user-not-found') { 34 | this.signup(); 35 | } else { 36 | this.setState({ error: 'Error logging in.' }); 37 | } 38 | }); 39 | } 40 | 41 | signup() { 42 | firebase 43 | .auth() 44 | .createUserWithEmailAndPassword(this.state.email, this.state.password) 45 | .then(res => { 46 | console.log(res); 47 | }) 48 | .catch(error => { 49 | console.log(error); 50 | this.setState({ error: 'Error signing up.' }); 51 | }); 52 | } 53 | 54 | render() { 55 | return ( 56 |
57 |
58 |
59 |

Sign in or sign up by entering your email and password.

60 | 66 | 72 |

73 | {this.state.error} 74 |

75 | 78 |
79 |
80 | ); 81 | } 82 | } 83 | 84 | export default LoginContainer; 85 | -------------------------------------------------------------------------------- /Chapter04/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App' 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | 7 | if (module.hot) { 8 | module.hot.accept('./components/App', () => { 9 | const NextApp = require('./components/App').default; 10 | ReactDOM.render( 11 | , 12 | document.getElementById('root') 13 | ); 14 | }); 15 | } -------------------------------------------------------------------------------- /Chapter04/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:8080', 8 | 'webpack/hot/only-dev-server', 9 | __dirname + "/src/index.js" 10 | ], 11 | output: { 12 | path: __dirname + "/public", 13 | filename: "bundle.js" 14 | }, 15 | module: { 16 | loaders: [ 17 | { 18 | test: /\.js$/, 19 | exclude: /node_modules/, 20 | loader: 'babel-loader', 21 | query: { 22 | presets: ['es2015','react'], 23 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 24 | } 25 | }, 26 | { 27 | test: /\.css$/, 28 | use: [ 29 | { loader: "style-loader" }, 30 | { loader: "css-loader" } 31 | ] 32 | } 33 | ] 34 | }, 35 | plugins: [ 36 | new webpack.HotModuleReplacementPlugin(), 37 | new HtmlWebpackPlugin({ 38 | inject: true, 39 | template: __dirname + '/public/index.html', 40 | }) 41 | ], 42 | devServer: { 43 | contentBase: "./public", 44 | historyApiFallback: true, 45 | inline: true, 46 | hot: true 47 | } 48 | }; -------------------------------------------------------------------------------- /Chapter04/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/src/index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'bundle.js', 10 | publicPath: './' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | query: { 19 | presets: ['es2015', 'react'], 20 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 21 | } 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 26 | }, 27 | { 28 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.json$/], 29 | loader: 'file-loader', 30 | options: { 31 | name: 'static/media/[name].[ext]' 32 | } 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify('production') 40 | } 41 | }), 42 | new HtmlWebpackPlugin({ 43 | inject: true, 44 | template: __dirname + '/public/index.html', 45 | minify: { 46 | removeComments: true, 47 | collapseWhitespace: true, 48 | removeRedundantAttributes: true, 49 | useShortDoctype: true, 50 | removeEmptyAttributes: true, 51 | removeStyleLinkTypeAttributes: true, 52 | keepClosingSlash: true, 53 | minifyJS: true, 54 | minifyCSS: true, 55 | minifyURLs: true 56 | } 57 | }), 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false, 61 | reduce_vars: false 62 | }, 63 | output: { 64 | comments: false 65 | }, 66 | sourceMap: true 67 | }), 68 | new ManifestPlugin({ 69 | fileName: 'asset-manifest.json' 70 | }) 71 | ] 72 | }; 73 | -------------------------------------------------------------------------------- /Chapter05/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "chatastrophe-draft" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Chapter05/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /public/secrets.js 4 | -------------------------------------------------------------------------------- /Chapter05/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Chapter05/functions/index.js: -------------------------------------------------------------------------------- 1 | var functions = require('firebase-functions'); 2 | 3 | // // Create and Deploy Your First Cloud Functions 4 | // // https://firebase.google.com/docs/functions/write-firebase-functions 5 | // 6 | // exports.helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }); 9 | -------------------------------------------------------------------------------- /Chapter05/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "~4.2.1", 6 | "firebase-functions": "^0.5.7" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /Chapter05/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server", 11 | "deploy": "npm run build && firebase deploy" 12 | }, 13 | "dependencies": { 14 | "babel-core": "^6.25.0", 15 | "babel-loader": "^7.1.1", 16 | "babel-plugin-transform-class-properties": "^6.24.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "css-loader": "^0.28.4", 20 | "file-loader": "^0.11.2", 21 | "fs-extra": "^4.0.1", 22 | "html-webpack-plugin": "^2.30.1", 23 | "react": "^15.6.1", 24 | "react-dom": "^15.6.1", 25 | "react-hot-loader": "3.0.0", 26 | "react-router-dom": "^4.2.2", 27 | "style-loader": "^0.18.2", 28 | "webpack": "^3.5.4", 29 | "webpack-dev-server": "^2.7.1", 30 | "webpack-manifest-plugin": "^1.3.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Chapter05/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter05/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter05/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter05/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter05/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter05/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter05/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter05/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter05/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter05/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter05/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter05/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter05/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 25 | 26 | -------------------------------------------------------------------------------- /Chapter05/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /Chapter05/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route, withRouter } from 'react-router-dom'; 3 | import LoginContainer from './LoginContainer'; 4 | import ChatContainer from './ChatContainer'; 5 | import UserContainer from './UserContainer'; 6 | import './app.css'; 7 | 8 | class App extends Component { 9 | componentDidMount() { 10 | firebase.auth().onAuthStateChanged(user => { 11 | if (user) { 12 | this.setState({ user }); 13 | } else { 14 | this.props.history.push('/login'); 15 | } 16 | }); 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 | 23 | 24 | 25 |
26 | ); 27 | } 28 | } 29 | 30 | export default withRouter(App); 31 | -------------------------------------------------------------------------------- /Chapter05/src/components/ChatContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | 4 | export default class ChatContainer extends Component { 5 | handleLogout = () => { 6 | firebase.auth().signOut(); 7 | }; 8 | 9 | render() { 10 | return ( 11 |
12 |
13 | 16 |
17 |

Hello from ChatContainer

18 |
19 | ); 20 | } 21 | } -------------------------------------------------------------------------------- /Chapter05/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = (props) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /Chapter05/src/components/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | 4 | class LoginContainer extends Component { 5 | state = { email: '', password: '', error: '' }; 6 | 7 | handleEmailChange = event => { 8 | this.setState({ email: event.target.value }); 9 | }; 10 | 11 | handlePasswordChange = event => { 12 | this.setState({ password: event.target.value }); 13 | }; 14 | 15 | handleSubmit = event => { 16 | event.preventDefault(); 17 | this.setState({ error: '' }); 18 | if (this.state.email && this.state.password) { 19 | this.login(); 20 | } else { 21 | this.setState({ error: 'Please fill in both fields.' }); 22 | } 23 | }; 24 | 25 | onLogin() { 26 | this.props.history.push("/"); 27 | } 28 | 29 | login() { 30 | firebase 31 | .auth() 32 | .signInWithEmailAndPassword(this.state.email, this.state.password) 33 | .then(res => { 34 | this.onLogin(); 35 | }) 36 | .catch(error => { 37 | if (error.code === 'auth/user-not-found') { 38 | this.signup(); 39 | } else { 40 | this.setState({ error: 'Error logging in.' }); 41 | } 42 | }); 43 | } 44 | 45 | signup() { 46 | firebase 47 | .auth() 48 | .createUserWithEmailAndPassword(this.state.email, this.state.password) 49 | .then(res => { 50 | this.onLogin(); 51 | }) 52 | .catch(error => { 53 | console.log(error); 54 | this.setState({ error: 'Error signing up.' }); 55 | }); 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |
62 |
63 |

Sign in or sign up by entering your email and password.

64 | 70 | 76 |

77 | {this.state.error} 78 |

79 | 82 |
83 |
84 | ); 85 | } 86 | } 87 | 88 | export default LoginContainer; 89 | -------------------------------------------------------------------------------- /Chapter05/src/components/UserContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Header from './Header'; 4 | 5 | export default class UserContainer extends Component { 6 | render() { 7 | return ( 8 |
9 |
10 | 11 | 14 | 15 |
16 |

Hello from UserContainer for User {this.props.match.params.id}

17 |
18 | ); 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter05/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './components/App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('./components/App', () => { 15 | const NextApp = require('./components/App').default; 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); 22 | }); 23 | } -------------------------------------------------------------------------------- /Chapter05/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:8080', 8 | 'webpack/hot/only-dev-server', 9 | __dirname + "/src/index.js" 10 | ], 11 | output: { 12 | path: __dirname + "/public", 13 | filename: "bundle.js", 14 | publicPath: "/" 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | query: { 23 | presets: ['es2015','react'], 24 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 25 | } 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { loader: "style-loader" }, 31 | { loader: "css-loader" } 32 | ] 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.HotModuleReplacementPlugin(), 38 | new HtmlWebpackPlugin({ 39 | inject: true, 40 | template: __dirname + '/public/index.html', 41 | }) 42 | ], 43 | devServer: { 44 | contentBase: "./public", 45 | historyApiFallback: true, 46 | inline: true, 47 | hot: true 48 | } 49 | }; -------------------------------------------------------------------------------- /Chapter05/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/src/index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'bundle.js', 10 | publicPath: './' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | query: { 19 | presets: ['es2015', 'react'], 20 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 21 | } 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 26 | }, 27 | { 28 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.json$/], 29 | loader: 'file-loader', 30 | options: { 31 | name: 'static/media/[name].[ext]' 32 | } 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify('production') 40 | } 41 | }), 42 | new HtmlWebpackPlugin({ 43 | inject: true, 44 | template: __dirname + '/public/index.html', 45 | minify: { 46 | removeComments: true, 47 | collapseWhitespace: true, 48 | removeRedundantAttributes: true, 49 | useShortDoctype: true, 50 | removeEmptyAttributes: true, 51 | removeStyleLinkTypeAttributes: true, 52 | keepClosingSlash: true, 53 | minifyJS: true, 54 | minifyCSS: true, 55 | minifyURLs: true 56 | } 57 | }), 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false, 61 | reduce_vars: false 62 | }, 63 | output: { 64 | comments: false 65 | }, 66 | sourceMap: true 67 | }), 68 | new ManifestPlugin({ 69 | fileName: 'asset-manifest.json' 70 | }) 71 | ] 72 | }; 73 | -------------------------------------------------------------------------------- /Chapter06/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "chatastrophe-draft" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Chapter06/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /public/secrets.js 4 | -------------------------------------------------------------------------------- /Chapter06/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Chapter06/functions/index.js: -------------------------------------------------------------------------------- 1 | var functions = require('firebase-functions'); 2 | 3 | // // Create and Deploy Your First Cloud Functions 4 | // // https://firebase.google.com/docs/functions/write-firebase-functions 5 | // 6 | // exports.helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }); 9 | -------------------------------------------------------------------------------- /Chapter06/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "~4.2.1", 6 | "firebase-functions": "^0.5.7" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /Chapter06/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server", 11 | "deploy": "npm run build && firebase deploy" 12 | }, 13 | "dependencies": { 14 | "babel-core": "^6.25.0", 15 | "babel-loader": "^7.1.1", 16 | "babel-plugin-transform-class-properties": "^6.24.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "css-loader": "^0.28.4", 20 | "file-loader": "^0.11.2", 21 | "fs-extra": "^4.0.1", 22 | "html-webpack-plugin": "^2.30.1", 23 | "react": "^15.6.1", 24 | "react-dom": "^15.6.1", 25 | "react-hot-loader": "3.0.0", 26 | "react-router-dom": "^4.2.2", 27 | "style-loader": "^0.18.2", 28 | "webpack": "^3.5.4", 29 | "webpack-dev-server": "^2.7.1", 30 | "webpack-manifest-plugin": "^1.3.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Chapter06/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter06/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter06/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter06/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter06/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter06/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter06/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter06/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter06/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter06/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter06/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter06/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter06/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 25 | 26 | -------------------------------------------------------------------------------- /Chapter06/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /Chapter06/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route, withRouter } from 'react-router-dom'; 3 | import LoginContainer from './LoginContainer'; 4 | import ChatContainer from './ChatContainer'; 5 | import UserContainer from './UserContainer'; 6 | import './app.css'; 7 | 8 | class App extends Component { 9 | state = { user: null, messages: [], messagesLoaded: false }; 10 | 11 | componentDidMount() { 12 | firebase.auth().onAuthStateChanged(user => { 13 | if (user) { 14 | this.setState({ user }); 15 | } else { 16 | this.props.history.push('/login'); 17 | } 18 | }); 19 | firebase 20 | .database() 21 | .ref('/messages') 22 | .on('value', snapshot => { 23 | this.onMessage(snapshot); 24 | if (!this.state.messagesLoaded) { 25 | this.setState({ messagesLoaded: true }); 26 | } 27 | }); 28 | } 29 | 30 | onMessage = snapshot => { 31 | const messages = Object.keys(snapshot.val()).map(key => { 32 | const msg = snapshot.val()[key]; 33 | msg.id = key; 34 | return msg; 35 | }); 36 | this.setState({ messages }); 37 | }; 38 | 39 | handleSubmitMessage = msg => { 40 | const data = { 41 | msg, 42 | author: this.state.user.email, 43 | user_id: this.state.user.uid, 44 | timestamp: Date.now() 45 | }; 46 | firebase 47 | .database() 48 | .ref('messages/') 49 | .push(data); 50 | }; 51 | 52 | render() { 53 | return ( 54 |
55 | 56 | ( 60 | 66 | )} 67 | /> 68 | ( 71 | 76 | )} 77 | /> 78 |
79 | ); 80 | } 81 | } 82 | 83 | export default withRouter(App); 84 | -------------------------------------------------------------------------------- /Chapter06/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = (props) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /Chapter06/src/components/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | 4 | class LoginContainer extends Component { 5 | state = { email: '', password: '', error: '' }; 6 | 7 | handleEmailChange = event => { 8 | this.setState({ email: event.target.value }); 9 | }; 10 | 11 | handlePasswordChange = event => { 12 | this.setState({ password: event.target.value }); 13 | }; 14 | 15 | handleSubmit = event => { 16 | event.preventDefault(); 17 | this.setState({ error: '' }); 18 | if (this.state.email && this.state.password) { 19 | this.login(); 20 | } else { 21 | this.setState({ error: 'Please fill in both fields.' }); 22 | } 23 | }; 24 | 25 | onLogin() { 26 | this.props.history.push("/"); 27 | } 28 | 29 | login() { 30 | firebase 31 | .auth() 32 | .signInWithEmailAndPassword(this.state.email, this.state.password) 33 | .then(res => { 34 | this.onLogin(); 35 | }) 36 | .catch(error => { 37 | if (error.code === 'auth/user-not-found') { 38 | this.signup(); 39 | } else { 40 | this.setState({ error: 'Error logging in.' }); 41 | } 42 | }); 43 | } 44 | 45 | signup() { 46 | firebase 47 | .auth() 48 | .createUserWithEmailAndPassword(this.state.email, this.state.password) 49 | .then(res => { 50 | this.onLogin(); 51 | }) 52 | .catch(error => { 53 | console.log(error); 54 | this.setState({ error: 'Error signing up.' }); 55 | }); 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |
62 |
63 |

Sign in or sign up by entering your email and password.

64 | 70 | 76 |

77 | {this.state.error} 78 |

79 | 82 |
83 |
84 | ); 85 | } 86 | } 87 | 88 | export default LoginContainer; 89 | -------------------------------------------------------------------------------- /Chapter06/src/components/UserContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Header from './Header'; 4 | 5 | export default class UserContainer extends Component { 6 | renderedUserEmail = false; 7 | 8 | getAuthor = author => { 9 | if (!this.renderedUserEmail) { 10 | this.renderedUserEmail = true; 11 | return

{author}

; 12 | } 13 | }; 14 | 15 | render() { 16 | return ( 17 |
18 |
19 | 20 | 21 | 22 |
23 | {this.props.messagesLoaded ? ( 24 |
25 | {this.props.messages.map(msg => { 26 | if (msg.user_id === this.props.userID) { 27 | return ( 28 |
29 | {this.getAuthor(msg.author)} 30 |

{msg.msg}

31 |
32 | ); 33 | } 34 | })} 35 |
36 | ) : ( 37 |
38 | logo 39 |
40 | )} 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Chapter06/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './components/App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('./components/App', () => { 15 | const NextApp = require('./components/App').default; 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); 22 | }); 23 | } -------------------------------------------------------------------------------- /Chapter06/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:8080', 8 | 'webpack/hot/only-dev-server', 9 | __dirname + "/src/index.js" 10 | ], 11 | output: { 12 | path: __dirname + "/public", 13 | filename: "bundle.js", 14 | publicPath: "/" 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | query: { 23 | presets: ['es2015','react'], 24 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 25 | } 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { loader: "style-loader" }, 31 | { loader: "css-loader" } 32 | ] 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.HotModuleReplacementPlugin(), 38 | new HtmlWebpackPlugin({ 39 | inject: true, 40 | template: __dirname + '/public/index.html', 41 | }) 42 | ], 43 | devServer: { 44 | contentBase: "./public", 45 | historyApiFallback: true, 46 | inline: true, 47 | hot: true 48 | } 49 | }; -------------------------------------------------------------------------------- /Chapter06/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/src/index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'bundle.js', 10 | publicPath: './' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | query: { 19 | presets: ['es2015', 'react'], 20 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 21 | } 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 26 | }, 27 | { 28 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.json$/], 29 | loader: 'file-loader', 30 | options: { 31 | name: 'static/media/[name].[ext]' 32 | } 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify('production') 40 | } 41 | }), 42 | new HtmlWebpackPlugin({ 43 | inject: true, 44 | template: __dirname + '/public/index.html', 45 | minify: { 46 | removeComments: true, 47 | collapseWhitespace: true, 48 | removeRedundantAttributes: true, 49 | useShortDoctype: true, 50 | removeEmptyAttributes: true, 51 | removeStyleLinkTypeAttributes: true, 52 | keepClosingSlash: true, 53 | minifyJS: true, 54 | minifyCSS: true, 55 | minifyURLs: true 56 | } 57 | }), 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false, 61 | reduce_vars: false 62 | }, 63 | output: { 64 | comments: false 65 | }, 66 | sourceMap: true 67 | }), 68 | new ManifestPlugin({ 69 | fileName: 'asset-manifest.json' 70 | }) 71 | ] 72 | }; 73 | -------------------------------------------------------------------------------- /Chapter07/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "chatastrophe-draft" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Chapter07/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /public/secrets.js 4 | -------------------------------------------------------------------------------- /Chapter07/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Chapter07/functions/index.js: -------------------------------------------------------------------------------- 1 | var functions = require('firebase-functions'); 2 | 3 | // // Create and Deploy Your First Cloud Functions 4 | // // https://firebase.google.com/docs/functions/write-firebase-functions 5 | // 6 | // exports.helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }); 9 | -------------------------------------------------------------------------------- /Chapter07/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "~4.2.1", 6 | "firebase-functions": "^0.5.7" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /Chapter07/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server", 11 | "deploy": "npm run build && firebase deploy" 12 | }, 13 | "dependencies": { 14 | "babel-core": "^6.25.0", 15 | "babel-loader": "^7.1.1", 16 | "babel-plugin-transform-class-properties": "^6.24.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "css-loader": "^0.28.4", 20 | "file-loader": "^0.11.2", 21 | "fs-extra": "^4.0.1", 22 | "html-webpack-plugin": "^2.30.1", 23 | "react": "^15.6.1", 24 | "react-dom": "^15.6.1", 25 | "react-hot-loader": "3.0.0", 26 | "react-router-dom": "^4.2.2", 27 | "style-loader": "^0.18.2", 28 | "webpack": "^3.5.4", 29 | "webpack-dev-server": "^2.7.1", 30 | "webpack-manifest-plugin": "^1.3.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Chapter07/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter07/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter07/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter07/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter07/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter07/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter07/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter07/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter07/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter07/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter07/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter07/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter07/public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-app.js'); 2 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-messaging.js'); 3 | 4 | firebase.initializeApp({ 5 | 'messagingSenderId': '120582509244' 6 | }); 7 | 8 | console.log(firebase.messaging()); -------------------------------------------------------------------------------- /Chapter07/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Chatastrophe 12 | 13 | 14 |
15 | 16 | 17 | 30 | 47 | 48 | -------------------------------------------------------------------------------- /Chapter07/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chatastrophe", 3 | "short_name": "Chatastrophe", 4 | "icons": [ 5 | { 6 | "src":"/assets/icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/icon-256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/assets/icon-384.png", 17 | "sizes": "384x384", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/assets/icon-512.png", 22 | "sizes": "512x512", 23 | "type": "image/png" 24 | } 25 | ], 26 | "start_url": "/?utm_source=homescreen", 27 | "background_color": "#e05a47", 28 | "theme_color": "#e05a47", 29 | "display": "standalone" 30 | } -------------------------------------------------------------------------------- /Chapter07/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /Chapter07/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = (props) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /Chapter07/src/components/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | 4 | class LoginContainer extends Component { 5 | state = { email: '', password: '', error: '' }; 6 | 7 | handleEmailChange = event => { 8 | this.setState({ email: event.target.value }); 9 | }; 10 | 11 | handlePasswordChange = event => { 12 | this.setState({ password: event.target.value }); 13 | }; 14 | 15 | handleSubmit = event => { 16 | event.preventDefault(); 17 | this.setState({ error: '' }); 18 | if (this.state.email && this.state.password) { 19 | this.login(); 20 | } else { 21 | this.setState({ error: 'Please fill in both fields.' }); 22 | } 23 | }; 24 | 25 | onLogin() { 26 | this.props.history.push("/"); 27 | } 28 | 29 | login() { 30 | firebase 31 | .auth() 32 | .signInWithEmailAndPassword(this.state.email, this.state.password) 33 | .then(res => { 34 | this.onLogin(); 35 | }) 36 | .catch(error => { 37 | if (error.code === 'auth/user-not-found') { 38 | this.signup(); 39 | } else { 40 | this.setState({ error: 'Error logging in.' }); 41 | } 42 | }); 43 | } 44 | 45 | signup() { 46 | firebase 47 | .auth() 48 | .createUserWithEmailAndPassword(this.state.email, this.state.password) 49 | .then(res => { 50 | this.onLogin(); 51 | }) 52 | .catch(error => { 53 | console.log(error); 54 | this.setState({ error: 'Error signing up.' }); 55 | }); 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |
62 |
63 |

Sign in or sign up by entering your email and password.

64 | 70 | 76 |

77 | {this.state.error} 78 |

79 | 82 |
83 |
84 | ); 85 | } 86 | } 87 | 88 | export default LoginContainer; 89 | -------------------------------------------------------------------------------- /Chapter07/src/components/UserContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Header from './Header'; 4 | 5 | export default class UserContainer extends Component { 6 | renderedUserEmail = false; 7 | 8 | getAuthor = author => { 9 | if (!this.renderedUserEmail) { 10 | this.renderedUserEmail = true; 11 | return

{author}

; 12 | } 13 | }; 14 | 15 | render() { 16 | return ( 17 |
18 |
19 | 20 | 21 | 22 |
23 | {this.props.messagesLoaded ? ( 24 |
25 | {this.props.messages.map(msg => { 26 | if (msg.user_id === this.props.userID) { 27 | return ( 28 |
29 | {this.getAuthor(msg.author)} 30 |

{msg.msg}

31 |
32 | ); 33 | } 34 | })} 35 |
36 | ) : ( 37 |
38 | logo 39 |
40 | )} 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Chapter07/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './components/App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('./components/App', () => { 15 | const NextApp = require('./components/App').default; 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); 22 | }); 23 | } -------------------------------------------------------------------------------- /Chapter07/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:8080', 8 | 'webpack/hot/only-dev-server', 9 | __dirname + "/src/index.js" 10 | ], 11 | output: { 12 | path: __dirname + "/public", 13 | filename: "bundle.js", 14 | publicPath: "/" 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | query: { 23 | presets: ['es2015','react'], 24 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 25 | } 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { loader: "style-loader" }, 31 | { loader: "css-loader" } 32 | ] 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.HotModuleReplacementPlugin(), 38 | new HtmlWebpackPlugin({ 39 | inject: true, 40 | template: __dirname + '/public/index.html', 41 | }) 42 | ], 43 | devServer: { 44 | contentBase: "./public", 45 | historyApiFallback: true, 46 | inline: true, 47 | hot: true 48 | } 49 | }; -------------------------------------------------------------------------------- /Chapter07/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/src/index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'bundle.js', 10 | publicPath: './' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | query: { 19 | presets: ['es2015', 'react'], 20 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 21 | } 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 26 | }, 27 | { 28 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.json$/], 29 | loader: 'file-loader', 30 | options: { 31 | name: 'static/media/[name].[ext]' 32 | } 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify('production') 40 | } 41 | }), 42 | new HtmlWebpackPlugin({ 43 | inject: true, 44 | template: __dirname + '/public/index.html', 45 | minify: { 46 | removeComments: true, 47 | collapseWhitespace: true, 48 | removeRedundantAttributes: true, 49 | useShortDoctype: true, 50 | removeEmptyAttributes: true, 51 | removeStyleLinkTypeAttributes: true, 52 | keepClosingSlash: true, 53 | minifyJS: true, 54 | minifyCSS: true, 55 | minifyURLs: true 56 | } 57 | }), 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false, 61 | reduce_vars: false 62 | }, 63 | output: { 64 | comments: false 65 | }, 66 | sourceMap: true 67 | }), 68 | new ManifestPlugin({ 69 | fileName: 'asset-manifest.json' 70 | }) 71 | ] 72 | }; 73 | -------------------------------------------------------------------------------- /Chapter08/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "chatastrophe-draft" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Chapter08/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /public/secrets.js 4 | -------------------------------------------------------------------------------- /Chapter08/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Chapter08/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const admin = require('firebase-admin'); 3 | admin.initializeApp(functions.config().firebase); 4 | 5 | exports.sendNotifications = functions.database 6 | .ref('/messages/{messageId}') 7 | .onWrite(event => { 8 | const snapshot = event.data; 9 | if (snapshot.previous.val()) { 10 | return; 11 | } 12 | const payload = { 13 | notification: { 14 | title: `${snapshot.val().author}`, 15 | body: `${snapshot.val().msg}`, 16 | icon: 'assets/icon.png', 17 | click_action: `https://${functions.config().firebase.authDomain}` 18 | } 19 | }; 20 | return admin 21 | .database() 22 | .ref('fcmTokens') 23 | .once('value') 24 | .then(allTokens => { 25 | if (allTokens.val()) { 26 | const tokens = []; 27 | for (let fcmTokenKey in allTokens.val()) { 28 | const fcmToken = allTokens.val()[fcmTokenKey]; 29 | if (fcmToken.user_id !== snapshot.val().user_id) { 30 | tokens.push(fcmToken.token); 31 | } 32 | } 33 | if (tokens.length > 0) { 34 | return admin 35 | .messaging() 36 | .sendToDevice(tokens, payload) 37 | .then(response => { 38 | const tokensToRemove = []; 39 | response.results.forEach((result, index) => { 40 | const error = result.error; 41 | if (error) { 42 | console.error( 43 | 'Failure sending notification to', 44 | tokens[index], 45 | error 46 | ); 47 | if ( 48 | error.code === 'messaging/invalid-registration-token' || 49 | error.code === 50 | 'messaging/registration-token-not-registered' 51 | ) { 52 | tokensToRemove.push( 53 | allTokens.ref.child(tokens[index]).remove() 54 | ); 55 | } 56 | } 57 | }); 58 | return Promise.all(tokensToRemove); 59 | }); 60 | } 61 | } 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /Chapter08/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "~4.2.1", 6 | "firebase-functions": "^0.5.7" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /Chapter08/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server", 11 | "deploy": "npm run build && firebase deploy" 12 | }, 13 | "dependencies": { 14 | "babel-core": "^6.25.0", 15 | "babel-loader": "^7.1.1", 16 | "babel-plugin-transform-class-properties": "^6.24.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "css-loader": "^0.28.4", 20 | "file-loader": "^0.11.2", 21 | "fs-extra": "^4.0.1", 22 | "html-webpack-plugin": "^2.30.1", 23 | "react": "^15.6.1", 24 | "react-dom": "^15.6.1", 25 | "react-hot-loader": "3.0.0", 26 | "react-router-dom": "^4.2.2", 27 | "style-loader": "^0.18.2", 28 | "webpack": "^3.5.4", 29 | "webpack-dev-server": "^2.7.1", 30 | "webpack-manifest-plugin": "^1.3.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Chapter08/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter08/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter08/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter08/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter08/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter08/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter08/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter08/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter08/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter08/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter08/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter08/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter08/public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-app.js'); 2 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-messaging.js'); 3 | 4 | firebase.initializeApp({ 5 | 'messagingSenderId': '85734589405' 6 | }); 7 | 8 | console.log(firebase.messaging()); -------------------------------------------------------------------------------- /Chapter08/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Chatastrophe 12 | 13 | 14 |
15 | 16 | 17 | 30 | 47 | 48 | -------------------------------------------------------------------------------- /Chapter08/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chatastrophe", 3 | "short_name": "Chatastrophe", 4 | "icons": [ 5 | { 6 | "src":"/assets/icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/icon-256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/assets/icon-384.png", 17 | "sizes": "384x384", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/assets/icon-512.png", 22 | "sizes": "512x512", 23 | "type": "image/png" 24 | } 25 | ], 26 | "start_url": "/?utm_source=homescreen", 27 | "background_color": "#e05a47", 28 | "theme_color": "#e05a47", 29 | "display": "standalone" 30 | } -------------------------------------------------------------------------------- /Chapter08/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /Chapter08/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = (props) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /Chapter08/src/components/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | 4 | class LoginContainer extends Component { 5 | state = { email: '', password: '', error: '' }; 6 | 7 | handleEmailChange = event => { 8 | this.setState({ email: event.target.value }); 9 | }; 10 | 11 | handlePasswordChange = event => { 12 | this.setState({ password: event.target.value }); 13 | }; 14 | 15 | handleSubmit = event => { 16 | event.preventDefault(); 17 | this.setState({ error: '' }); 18 | if (this.state.email && this.state.password) { 19 | this.login(); 20 | } else { 21 | this.setState({ error: 'Please fill in both fields.' }); 22 | } 23 | }; 24 | 25 | onLogin() { 26 | this.props.history.push("/"); 27 | } 28 | 29 | login() { 30 | firebase 31 | .auth() 32 | .signInWithEmailAndPassword(this.state.email, this.state.password) 33 | .then(res => { 34 | this.onLogin(); 35 | }) 36 | .catch(error => { 37 | if (error.code === 'auth/user-not-found') { 38 | this.signup(); 39 | } else { 40 | this.setState({ error: 'Error logging in.' }); 41 | } 42 | }); 43 | } 44 | 45 | signup() { 46 | firebase 47 | .auth() 48 | .createUserWithEmailAndPassword(this.state.email, this.state.password) 49 | .then(res => { 50 | this.onLogin(); 51 | }) 52 | .catch(error => { 53 | console.log(error); 54 | this.setState({ error: 'Error signing up.' }); 55 | }); 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |
62 |
63 |

Sign in or sign up by entering your email and password.

64 | 70 | 76 |

77 | {this.state.error} 78 |

79 | 82 |
83 |
84 | ); 85 | } 86 | } 87 | 88 | export default LoginContainer; 89 | -------------------------------------------------------------------------------- /Chapter08/src/components/UserContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Header from './Header'; 4 | 5 | export default class UserContainer extends Component { 6 | renderedUserEmail = false; 7 | 8 | getAuthor = author => { 9 | if (!this.renderedUserEmail) { 10 | this.renderedUserEmail = true; 11 | return

{author}

; 12 | } 13 | }; 14 | 15 | render() { 16 | return ( 17 |
18 |
19 | 20 | 21 | 22 |
23 | {this.props.messagesLoaded ? ( 24 |
25 | {this.props.messages.map(msg => { 26 | if (msg.user_id === this.props.userID) { 27 | return ( 28 |
29 | {this.getAuthor(msg.author)} 30 |

{msg.msg}

31 |
32 | ); 33 | } 34 | })} 35 |
36 | ) : ( 37 |
38 | logo 39 |
40 | )} 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Chapter08/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './components/App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('./components/App', () => { 15 | const NextApp = require('./components/App').default; 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); 22 | }); 23 | } -------------------------------------------------------------------------------- /Chapter08/src/resources/NotificationResource.js: -------------------------------------------------------------------------------- 1 | export default class NotificationResource { 2 | allTokens = []; 3 | tokensLoaded = false; 4 | user = null; 5 | 6 | constructor(messaging, database) { 7 | this.messaging = messaging; 8 | this.database = database; 9 | this.messaging 10 | .requestPermission() 11 | .then(res => { 12 | console.log('Permission granted'); 13 | }) 14 | .catch(err => { 15 | console.log('no access', err); 16 | }); 17 | this.setupTokenRefresh(); 18 | this.database.ref('/fcmTokens').on('value', snapshot => { 19 | this.allTokens = snapshot.val(); 20 | this.tokensLoaded = true; 21 | }); 22 | } 23 | 24 | setupTokenRefresh() { 25 | this.messaging.onTokenRefresh(() => { 26 | this.saveTokenToServer(); 27 | }); 28 | } 29 | 30 | saveTokenToServer() { 31 | this.messaging.getToken().then(res => { 32 | if (this.tokensLoaded) { 33 | const existingToken = this.findExistingToken(res); 34 | if (existingToken) { 35 | firebase 36 | .database() 37 | .ref(`/fcmTokens/${existingToken}`) 38 | .set({ 39 | token: res, 40 | user_id: this.user.uid 41 | }); 42 | } else { 43 | this.registerToken(res); 44 | } 45 | } 46 | }); 47 | } 48 | 49 | registerToken(token) { 50 | firebase 51 | .database() 52 | .ref('fcmTokens/') 53 | .push({ 54 | token: token, 55 | user_id: this.user.uid 56 | }); 57 | } 58 | 59 | findExistingToken(tokenToSave) { 60 | for (let tokenKey in this.allTokens) { 61 | const token = this.allTokens[tokenKey].token; 62 | if (token === tokenToSave) { 63 | return tokenKey; 64 | } 65 | } 66 | return false; 67 | } 68 | 69 | changeUser(user) { 70 | this.user = user; 71 | this.saveTokenToServer(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Chapter08/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:8080', 8 | 'webpack/hot/only-dev-server', 9 | __dirname + "/src/index.js" 10 | ], 11 | output: { 12 | path: __dirname + "/public", 13 | filename: "bundle.js", 14 | publicPath: "/" 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | query: { 23 | presets: ['es2015','react'], 24 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 25 | } 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { loader: "style-loader" }, 31 | { loader: "css-loader" } 32 | ] 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.HotModuleReplacementPlugin(), 38 | new HtmlWebpackPlugin({ 39 | inject: true, 40 | template: __dirname + '/public/index.html', 41 | }) 42 | ], 43 | devServer: { 44 | contentBase: "./public", 45 | historyApiFallback: true, 46 | inline: true, 47 | hot: true 48 | } 49 | }; -------------------------------------------------------------------------------- /Chapter08/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/src/index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'bundle.js', 10 | publicPath: './' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | query: { 19 | presets: ['es2015', 'react'], 20 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 21 | } 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 26 | }, 27 | { 28 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.json$/], 29 | loader: 'file-loader', 30 | options: { 31 | name: 'static/media/[name].[ext]' 32 | } 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify('production') 40 | } 41 | }), 42 | new HtmlWebpackPlugin({ 43 | inject: true, 44 | template: __dirname + '/public/index.html', 45 | minify: { 46 | removeComments: true, 47 | collapseWhitespace: true, 48 | removeRedundantAttributes: true, 49 | useShortDoctype: true, 50 | removeEmptyAttributes: true, 51 | removeStyleLinkTypeAttributes: true, 52 | keepClosingSlash: true, 53 | minifyJS: true, 54 | minifyCSS: true, 55 | minifyURLs: true 56 | } 57 | }), 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false, 61 | reduce_vars: false 62 | }, 63 | output: { 64 | comments: false 65 | }, 66 | sourceMap: true 67 | }), 68 | new ManifestPlugin({ 69 | fileName: 'asset-manifest.json' 70 | }) 71 | ] 72 | }; 73 | -------------------------------------------------------------------------------- /Chapter09/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "chatastrophe-draft" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Chapter09/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /public/secrets.js 4 | -------------------------------------------------------------------------------- /Chapter09/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Chapter09/functions/index.js: -------------------------------------------------------------------------------- 1 | var functions = require('firebase-functions'); 2 | 3 | // // Create and Deploy Your First Cloud Functions 4 | // // https://firebase.google.com/docs/functions/write-firebase-functions 5 | // 6 | // exports.helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }); 9 | -------------------------------------------------------------------------------- /Chapter09/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "~4.2.1", 6 | "firebase-functions": "^0.5.7" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /Chapter09/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server", 11 | "deploy": "npm run build && firebase deploy" 12 | }, 13 | "dependencies": { 14 | "babel-core": "^6.25.0", 15 | "babel-loader": "^7.1.1", 16 | "babel-plugin-transform-class-properties": "^6.24.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "css-loader": "^0.28.4", 20 | "file-loader": "^0.11.2", 21 | "fs-extra": "^4.0.1", 22 | "html-webpack-plugin": "^2.30.1", 23 | "react": "^15.6.1", 24 | "react-dom": "^15.6.1", 25 | "react-hot-loader": "3.0.0", 26 | "react-router-dom": "^4.2.2", 27 | "style-loader": "^0.18.2", 28 | "webpack": "^3.5.4", 29 | "webpack-dev-server": "^2.7.1", 30 | "webpack-manifest-plugin": "^1.3.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Chapter09/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter09/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter09/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter09/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter09/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter09/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter09/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter09/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter09/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter09/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter09/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter09/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter09/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Chatastrophe 12 | 13 | 14 |
15 | 16 | 17 | 30 | 31 | -------------------------------------------------------------------------------- /Chapter09/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chatastrophe", 3 | "short_name": "Chatastrophe", 4 | "icons": [ 5 | { 6 | "src":"/assets/icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/icon-256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/assets/icon-384.png", 17 | "sizes": "384x384", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/assets/icon-512.png", 22 | "sizes": "512x512", 23 | "type": "image/png" 24 | } 25 | ], 26 | "start_url": "/?utm_source=homescreen", 27 | "background_color": "#e05a47", 28 | "theme_color": "#e05a47", 29 | "display": "standalone" 30 | } -------------------------------------------------------------------------------- /Chapter09/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /Chapter09/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route, withRouter } from 'react-router-dom'; 3 | import LoginContainer from './LoginContainer'; 4 | import ChatContainer from './ChatContainer'; 5 | import UserContainer from './UserContainer'; 6 | import './app.css'; 7 | 8 | class App extends Component { 9 | state = { user: null, messages: [], messagesLoaded: false }; 10 | 11 | componentDidMount() { 12 | firebase.auth().onAuthStateChanged(user => { 13 | if (user) { 14 | this.setState({ user }); 15 | } else { 16 | this.props.history.push('/login'); 17 | } 18 | }); 19 | firebase 20 | .database() 21 | .ref('/messages') 22 | .on('value', snapshot => { 23 | this.onMessage(snapshot); 24 | if (!this.state.messagesLoaded) { 25 | this.setState({ messagesLoaded: true }); 26 | } 27 | }); 28 | this.listenForInstallBanner(); 29 | } 30 | 31 | listenForInstallBanner = () => { 32 | window.addEventListener('beforeinstallprompt', e => { 33 | console.log('beforeinstallprompt Event fired'); 34 | e.preventDefault(); 35 | // Stash the event so it can be triggered later. 36 | this.deferredPrompt = e; 37 | }); 38 | }; 39 | 40 | onMessage = snapshot => { 41 | const messages = Object.keys(snapshot.val()).map(key => { 42 | const msg = snapshot.val()[key]; 43 | msg.id = key; 44 | return msg; 45 | }); 46 | this.setState({ messages }); 47 | }; 48 | 49 | handleSubmitMessage = msg => { 50 | const data = { 51 | msg, 52 | author: this.state.user.email, 53 | user_id: this.state.user.uid, 54 | timestamp: Date.now() 55 | }; 56 | firebase 57 | .database() 58 | .ref('messages/') 59 | .push(data); 60 | if (this.deferredPrompt) { 61 | this.deferredPrompt.prompt(); 62 | this.deferredPrompt.userChoice.then(choice => { 63 | console.log(choice); 64 | }); 65 | this.deferredPrompt = null; 66 | } 67 | }; 68 | 69 | render() { 70 | return ( 71 |
72 | 73 | ( 77 | 83 | )} 84 | /> 85 | ( 88 | 93 | )} 94 | /> 95 |
96 | ); 97 | } 98 | } 99 | 100 | export default withRouter(App); 101 | -------------------------------------------------------------------------------- /Chapter09/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = (props) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /Chapter09/src/components/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | 4 | class LoginContainer extends Component { 5 | state = { email: '', password: '', error: '' }; 6 | 7 | handleEmailChange = event => { 8 | this.setState({ email: event.target.value }); 9 | }; 10 | 11 | handlePasswordChange = event => { 12 | this.setState({ password: event.target.value }); 13 | }; 14 | 15 | handleSubmit = event => { 16 | event.preventDefault(); 17 | this.setState({ error: '' }); 18 | if (this.state.email && this.state.password) { 19 | this.login(); 20 | } else { 21 | this.setState({ error: 'Please fill in both fields.' }); 22 | } 23 | }; 24 | 25 | onLogin() { 26 | this.props.history.push("/"); 27 | } 28 | 29 | login() { 30 | firebase 31 | .auth() 32 | .signInWithEmailAndPassword(this.state.email, this.state.password) 33 | .then(res => { 34 | this.onLogin(); 35 | }) 36 | .catch(error => { 37 | if (error.code === 'auth/user-not-found') { 38 | this.signup(); 39 | } else { 40 | this.setState({ error: 'Error logging in.' }); 41 | } 42 | }); 43 | } 44 | 45 | signup() { 46 | firebase 47 | .auth() 48 | .createUserWithEmailAndPassword(this.state.email, this.state.password) 49 | .then(res => { 50 | this.onLogin(); 51 | }) 52 | .catch(error => { 53 | console.log(error); 54 | this.setState({ error: 'Error signing up.' }); 55 | }); 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |
62 |
63 |

Sign in or sign up by entering your email and password.

64 | 70 | 76 |

77 | {this.state.error} 78 |

79 | 82 |
83 |
84 | ); 85 | } 86 | } 87 | 88 | export default LoginContainer; 89 | -------------------------------------------------------------------------------- /Chapter09/src/components/UserContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Header from './Header'; 4 | 5 | export default class UserContainer extends Component { 6 | renderedUserEmail = false; 7 | 8 | getAuthor = author => { 9 | if (!this.renderedUserEmail) { 10 | this.renderedUserEmail = true; 11 | return

{author}

; 12 | } 13 | }; 14 | 15 | render() { 16 | return ( 17 |
18 |
19 | 20 | 21 | 22 |
23 | {this.props.messagesLoaded ? ( 24 |
25 | {this.props.messages.map(msg => { 26 | if (msg.user_id === this.props.userID) { 27 | return ( 28 |
29 | {this.getAuthor(msg.author)} 30 |

{msg.msg}

31 |
32 | ); 33 | } 34 | })} 35 |
36 | ) : ( 37 |
38 | logo 39 |
40 | )} 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Chapter09/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './components/App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('./components/App', () => { 15 | const NextApp = require('./components/App').default; 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); 22 | }); 23 | } -------------------------------------------------------------------------------- /Chapter09/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:8080', 8 | 'webpack/hot/only-dev-server', 9 | __dirname + "/src/index.js" 10 | ], 11 | output: { 12 | path: __dirname + "/public", 13 | filename: "bundle.js", 14 | publicPath: "/" 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | query: { 23 | presets: ['es2015','react'], 24 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 25 | } 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { loader: "style-loader" }, 31 | { loader: "css-loader" } 32 | ] 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.HotModuleReplacementPlugin(), 38 | new HtmlWebpackPlugin({ 39 | inject: true, 40 | template: __dirname + '/public/index.html', 41 | }) 42 | ], 43 | devServer: { 44 | contentBase: "./public", 45 | historyApiFallback: true, 46 | inline: true, 47 | hot: true 48 | } 49 | }; -------------------------------------------------------------------------------- /Chapter09/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/src/index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'bundle.js', 10 | publicPath: './' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | query: { 19 | presets: ['es2015', 'react'], 20 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 21 | } 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 26 | }, 27 | { 28 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.json$/], 29 | loader: 'file-loader', 30 | options: { 31 | name: 'static/media/[name].[ext]' 32 | } 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify('production') 40 | } 41 | }), 42 | new HtmlWebpackPlugin({ 43 | inject: true, 44 | template: __dirname + '/public/index.html', 45 | minify: { 46 | removeComments: true, 47 | collapseWhitespace: true, 48 | removeRedundantAttributes: true, 49 | useShortDoctype: true, 50 | removeEmptyAttributes: true, 51 | removeStyleLinkTypeAttributes: true, 52 | keepClosingSlash: true, 53 | minifyJS: true, 54 | minifyCSS: true, 55 | minifyURLs: true 56 | } 57 | }), 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false, 61 | reduce_vars: false 62 | }, 63 | output: { 64 | comments: false 65 | }, 66 | sourceMap: true 67 | }), 68 | new ManifestPlugin({ 69 | fileName: 'asset-manifest.json' 70 | }) 71 | ] 72 | }; 73 | -------------------------------------------------------------------------------- /Chapter10/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "chatastrophe-draft" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Chapter10/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /public/secrets.js 4 | -------------------------------------------------------------------------------- /Chapter10/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Chapter10/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const admin = require('firebase-admin'); 3 | admin.initializeApp(functions.config().firebase); 4 | 5 | exports.sendNotifications = functions.database 6 | .ref('/messages/{messageId}') 7 | .onWrite(event => { 8 | const snapshot = event.data; 9 | if (snapshot.previous.val()) { 10 | return; 11 | } 12 | const payload = { 13 | notification: { 14 | title: `${snapshot.val().author}`, 15 | body: `${snapshot.val().msg}`, 16 | icon: 'assets/icon.png', 17 | click_action: `https://${functions.config().firebase.authDomain}` 18 | } 19 | }; 20 | return admin 21 | .database() 22 | .ref('fcmTokens') 23 | .once('value') 24 | .then(allTokens => { 25 | if (allTokens.val()) { 26 | const tokens = []; 27 | for (let fcmTokenKey in allTokens.val()) { 28 | const fcmToken = allTokens.val()[fcmTokenKey]; 29 | if (fcmToken.user_id !== snapshot.val().user_id) { 30 | tokens.push(fcmToken.token); 31 | } 32 | } 33 | if (tokens.length > 0) { 34 | return admin 35 | .messaging() 36 | .sendToDevice(tokens, payload) 37 | .then(response => { 38 | const tokensToRemove = []; 39 | response.results.forEach((result, index) => { 40 | const error = result.error; 41 | if (error) { 42 | console.error( 43 | 'Failure sending notification to', 44 | tokens[index], 45 | error 46 | ); 47 | if ( 48 | error.code === 'messaging/invalid-registration-token' || 49 | error.code === 50 | 'messaging/registration-token-not-registered' 51 | ) { 52 | tokensToRemove.push( 53 | allTokens.ref.child(tokens[index]).remove() 54 | ); 55 | } 56 | } 57 | }); 58 | return Promise.all(tokensToRemove); 59 | }); 60 | } 61 | } 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /Chapter10/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "~4.2.1", 6 | "firebase-functions": "^0.5.7" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /Chapter10/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server", 11 | "deploy": "npm run build && firebase deploy" 12 | }, 13 | "dependencies": { 14 | "babel-core": "^6.25.0", 15 | "babel-loader": "^7.1.1", 16 | "babel-plugin-transform-class-properties": "^6.24.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "css-loader": "^0.28.4", 20 | "file-loader": "^0.11.2", 21 | "fs-extra": "^4.0.1", 22 | "html-webpack-plugin": "^2.30.1", 23 | "react": "^15.6.1", 24 | "react-dom": "^15.6.1", 25 | "react-hot-loader": "3.0.0", 26 | "react-router-dom": "^4.2.2", 27 | "style-loader": "^0.18.2", 28 | "webpack": "^3.5.4", 29 | "webpack-dev-server": "^2.7.1", 30 | "webpack-manifest-plugin": "^1.3.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Chapter10/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter10/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter10/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter10/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter10/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter10/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter10/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter10/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter10/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter10/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter10/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter10/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter10/public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-app.js'); 2 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-messaging.js'); 3 | 4 | firebase.initializeApp({ 5 | 'messagingSenderId': '85734589405' 6 | }); 7 | 8 | console.log(firebase.messaging()); -------------------------------------------------------------------------------- /Chapter10/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chatastrophe", 3 | "short_name": "Chatastrophe", 4 | "icons": [ 5 | { 6 | "src":"/assets/icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/icon-256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/assets/icon-384.png", 17 | "sizes": "384x384", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/assets/icon-512.png", 22 | "sizes": "512x512", 23 | "type": "image/png" 24 | } 25 | ], 26 | "start_url": "/?utm_source=homescreen", 27 | "background_color": "#e05a47", 28 | "theme_color": "#e05a47", 29 | "display": "standalone" 30 | } -------------------------------------------------------------------------------- /Chapter10/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /Chapter10/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = (props) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /Chapter10/src/components/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | 4 | class LoginContainer extends Component { 5 | state = { email: '', password: '', error: '' }; 6 | 7 | handleEmailChange = event => { 8 | this.setState({ email: event.target.value }); 9 | }; 10 | 11 | handlePasswordChange = event => { 12 | this.setState({ password: event.target.value }); 13 | }; 14 | 15 | handleSubmit = event => { 16 | event.preventDefault(); 17 | this.setState({ error: '' }); 18 | if (this.state.email && this.state.password) { 19 | this.login(); 20 | } else { 21 | this.setState({ error: 'Please fill in both fields.' }); 22 | } 23 | }; 24 | 25 | onLogin() { 26 | this.props.history.push("/"); 27 | } 28 | 29 | login() { 30 | firebase 31 | .auth() 32 | .signInWithEmailAndPassword(this.state.email, this.state.password) 33 | .then(res => { 34 | this.onLogin(); 35 | }) 36 | .catch(error => { 37 | if (error.code === 'auth/user-not-found') { 38 | this.signup(); 39 | } else { 40 | this.setState({ error: 'Error logging in.' }); 41 | } 42 | }); 43 | } 44 | 45 | signup() { 46 | firebase 47 | .auth() 48 | .createUserWithEmailAndPassword(this.state.email, this.state.password) 49 | .then(res => { 50 | this.onLogin(); 51 | }) 52 | .catch(error => { 53 | console.log(error); 54 | this.setState({ error: 'Error signing up.' }); 55 | }); 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |
62 |
63 |

Sign in or sign up by entering your email and password.

64 | 70 | 76 |

77 | {this.state.error} 78 |

79 | 82 |
83 |
84 | ); 85 | } 86 | } 87 | 88 | export default LoginContainer; 89 | -------------------------------------------------------------------------------- /Chapter10/src/components/UserContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Header from './Header'; 4 | 5 | export default class UserContainer extends Component { 6 | renderedUserEmail = false; 7 | 8 | getAuthor = author => { 9 | if (!this.renderedUserEmail) { 10 | this.renderedUserEmail = true; 11 | return

{author}

; 12 | } 13 | }; 14 | 15 | render() { 16 | return ( 17 |
18 |
19 | 20 | 21 | 22 |
23 | {this.props.messagesLoaded ? ( 24 |
25 | {this.props.messages.map(msg => { 26 | if (msg.user_id === this.props.userID) { 27 | return ( 28 |
29 | {this.getAuthor(msg.author)} 30 |

{msg.msg}

31 |
32 | ); 33 | } 34 | })} 35 |
36 | ) : ( 37 |
38 | logo 39 |
40 | )} 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Chapter10/src/components/app.css: -------------------------------------------------------------------------------- 1 | #test-image { 2 | margin-top: 20px; 3 | } 4 | 5 | h1, p { 6 | margin: 0; 7 | } 8 | 9 | button, input { 10 | border: none; 11 | background-color: white; 12 | } 13 | 14 | input { 15 | border: 1px solid rgba(0,0,0,.2); 16 | padding: 0 5px; 17 | } 18 | 19 | button:hover { 20 | cursor: pointer; 21 | opacity: .75; 22 | } 23 | 24 | button.red { 25 | background-color: #A62824; 26 | color: white; 27 | padding: 7px 10px; 28 | border-radius: 3px; 29 | text-transform: uppercase; 30 | } 31 | 32 | button.red.light { 33 | background-color: #A62824; 34 | } 35 | 36 | button.red:focus { 37 | background-color: #7b1c19; 38 | } 39 | 40 | #ChatContainer #chat-input { 41 | position: absolute; 42 | bottom: 0; 43 | left: 0; 44 | width: 100%; 45 | padding-top: 5px; 46 | background-color: white; 47 | } 48 | 49 | #ChatContainer #chat-input textarea { 50 | width: 85%; 51 | border-radius: 10px; 52 | border: none; 53 | border: 1px solid rgba(0,0,0,.2); 54 | padding: 5px 8px; 55 | float: left; 56 | } 57 | 58 | #ChatContainer #chat-input button { 59 | background: none; 60 | border: none; 61 | width: 15%; 62 | padding-top: 6px; 63 | } 64 | 65 | #ChatContainer #chat-input button svg { 66 | width: 24px; 67 | height: 24px; 68 | text-align: center; 69 | } 70 | 71 | #ChatContainer #chat-input button:hover { 72 | cursor: pointer; 73 | } 74 | 75 | #ChatContainer #chat-input button:hover svg path { 76 | fill: #A62824; 77 | } 78 | 79 | #message-container { 80 | height: calc(100% - 45px); 81 | overflow-y: auto; 82 | padding: 0 10px; 83 | } 84 | 85 | .message { 86 | margin: 7px 0; 87 | 88 | } 89 | 90 | .message p { 91 | border-radius: 5px; 92 | padding: 5px; 93 | background-color: #eaeaea; 94 | display: inline-block; 95 | } 96 | 97 | .message.mine { 98 | text-align: right; 99 | } 100 | 101 | .message a { 102 | text-decoration: none; 103 | } 104 | 105 | .message a:visited, .message a { 106 | color: black; 107 | } 108 | 109 | .message a:hover { 110 | color: #A62824; 111 | } 112 | 113 | .message p.author { 114 | background-color: white; 115 | font-size: .8em; 116 | display: block; 117 | text-decoration: none; 118 | } 119 | 120 | #LoginContainer form { 121 | max-width: 300px; 122 | margin: 0 auto; 123 | display: flex; 124 | justify-content: center; 125 | align-items: center; 126 | flex-direction: column; 127 | height: 80%; 128 | } 129 | 130 | #LoginContainer form p { 131 | font-size: .8em; 132 | text-align:center; 133 | } 134 | 135 | #LoginContainer form input { 136 | width: 200px; 137 | margin: 10px 0; 138 | height: 30px; 139 | } 140 | 141 | .error { 142 | margin: 5px 0; 143 | color: red; 144 | } -------------------------------------------------------------------------------- /Chapter10/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './components/App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('./components/App', () => { 15 | const NextApp = require('./components/App').default; 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); 22 | }); 23 | } -------------------------------------------------------------------------------- /Chapter10/src/resources/NotificationResource.js: -------------------------------------------------------------------------------- 1 | export default class NotificationResource { 2 | allTokens = []; 3 | tokensLoaded = false; 4 | user = null; 5 | 6 | constructor(messaging, database) { 7 | this.messaging = messaging; 8 | this.database = database; 9 | this.messaging 10 | .requestPermission() 11 | .then(res => { 12 | console.log('Permission granted'); 13 | }) 14 | .catch(err => { 15 | console.log('no access', err); 16 | }); 17 | this.setupTokenRefresh(); 18 | this.database.ref('/fcmTokens').on('value', snapshot => { 19 | this.allTokens = snapshot.val(); 20 | this.tokensLoaded = true; 21 | }); 22 | } 23 | 24 | setupTokenRefresh() { 25 | this.messaging.onTokenRefresh(() => { 26 | this.saveTokenToServer(); 27 | }); 28 | } 29 | 30 | saveTokenToServer() { 31 | this.messaging.getToken().then(res => { 32 | if (this.tokensLoaded) { 33 | const existingToken = this.findExistingToken(res); 34 | if (existingToken) { 35 | firebase 36 | .database() 37 | .ref(`/fcmTokens/${existingToken}`) 38 | .set({ 39 | token: res, 40 | user_id: this.user.uid 41 | }); 42 | } else { 43 | this.registerToken(res); 44 | } 45 | } 46 | }); 47 | } 48 | 49 | registerToken(token) { 50 | firebase 51 | .database() 52 | .ref('fcmTokens/') 53 | .push({ 54 | token: token, 55 | user_id: this.user.uid 56 | }); 57 | } 58 | 59 | findExistingToken(tokenToSave) { 60 | for (let tokenKey in this.allTokens) { 61 | const token = this.allTokens[tokenKey].token; 62 | if (token === tokenToSave) { 63 | return tokenKey; 64 | } 65 | } 66 | return false; 67 | } 68 | 69 | changeUser(user) { 70 | this.user = user; 71 | this.saveTokenToServer(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Chapter10/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:8080', 8 | 'webpack/hot/only-dev-server', 9 | __dirname + "/src/index.js" 10 | ], 11 | output: { 12 | path: __dirname + "/public", 13 | filename: "bundle.js", 14 | publicPath: "/" 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | query: { 23 | presets: ['es2015','react'], 24 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 25 | } 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { loader: "style-loader" }, 31 | { loader: "css-loader" } 32 | ] 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.HotModuleReplacementPlugin(), 38 | new HtmlWebpackPlugin({ 39 | inject: true, 40 | template: __dirname + '/public/index.html', 41 | }) 42 | ], 43 | devServer: { 44 | contentBase: "./public", 45 | historyApiFallback: true, 46 | inline: true, 47 | hot: true 48 | } 49 | }; -------------------------------------------------------------------------------- /Chapter10/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/src/index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'bundle.js', 10 | publicPath: './' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | query: { 19 | presets: ['es2015', 'react'], 20 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 21 | } 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 26 | }, 27 | { 28 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.json$/], 29 | loader: 'file-loader', 30 | options: { 31 | name: 'static/media/[name].[ext]' 32 | } 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify('production') 40 | } 41 | }), 42 | new HtmlWebpackPlugin({ 43 | inject: true, 44 | template: __dirname + '/public/index.html', 45 | minify: { 46 | removeComments: true, 47 | collapseWhitespace: true, 48 | removeRedundantAttributes: true, 49 | useShortDoctype: true, 50 | removeEmptyAttributes: true, 51 | removeStyleLinkTypeAttributes: true, 52 | keepClosingSlash: true, 53 | minifyJS: true, 54 | minifyCSS: true, 55 | minifyURLs: true 56 | } 57 | }), 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false, 61 | reduce_vars: false 62 | }, 63 | output: { 64 | comments: false 65 | }, 66 | sourceMap: true 67 | }), 68 | new ManifestPlugin({ 69 | fileName: 'asset-manifest.json' 70 | }) 71 | ] 72 | }; 73 | -------------------------------------------------------------------------------- /Chapter11/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "chatastrophe-draft" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Chapter11/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /public/secrets.js 4 | -------------------------------------------------------------------------------- /Chapter11/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Chapter11/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const admin = require('firebase-admin'); 3 | admin.initializeApp(functions.config().firebase); 4 | 5 | exports.sendNotifications = functions.database 6 | .ref('/messages/{messageId}') 7 | .onWrite(event => { 8 | const snapshot = event.data; 9 | if (snapshot.previous.val()) { 10 | return; 11 | } 12 | const payload = { 13 | notification: { 14 | title: `${snapshot.val().author}`, 15 | body: `${snapshot.val().msg}`, 16 | icon: 'assets/icon.png', 17 | click_action: `https://${functions.config().firebase.authDomain}` 18 | } 19 | }; 20 | return admin 21 | .database() 22 | .ref('fcmTokens') 23 | .once('value') 24 | .then(allTokens => { 25 | if (allTokens.val()) { 26 | const tokens = []; 27 | for (let fcmTokenKey in allTokens.val()) { 28 | const fcmToken = allTokens.val()[fcmTokenKey]; 29 | if (fcmToken.user_id !== snapshot.val().user_id) { 30 | tokens.push(fcmToken.token); 31 | } 32 | } 33 | if (tokens.length > 0) { 34 | return admin 35 | .messaging() 36 | .sendToDevice(tokens, payload) 37 | .then(response => { 38 | const tokensToRemove = []; 39 | response.results.forEach((result, index) => { 40 | const error = result.error; 41 | if (error) { 42 | console.error( 43 | 'Failure sending notification to', 44 | tokens[index], 45 | error 46 | ); 47 | if ( 48 | error.code === 'messaging/invalid-registration-token' || 49 | error.code === 50 | 'messaging/registration-token-not-registered' 51 | ) { 52 | tokensToRemove.push( 53 | allTokens.ref.child(tokens[index]).remove() 54 | ); 55 | } 56 | } 57 | }); 58 | return Promise.all(tokensToRemove); 59 | }); 60 | } 61 | } 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /Chapter11/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "~4.2.1", 6 | "firebase-functions": "^0.5.7" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /Chapter11/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server", 11 | "deploy": "npm run build && firebase deploy" 12 | }, 13 | "dependencies": { 14 | "babel-core": "^6.25.0", 15 | "babel-loader": "^7.1.1", 16 | "babel-plugin-transform-class-properties": "^6.24.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "css-loader": "^0.28.4", 20 | "file-loader": "^0.11.2", 21 | "fs-extra": "^4.0.1", 22 | "html-webpack-plugin": "^2.30.1", 23 | "react": "^15.6.1", 24 | "react-dom": "^15.6.1", 25 | "react-hot-loader": "3.0.0", 26 | "react-router-dom": "^4.2.2", 27 | "style-loader": "^0.18.2", 28 | "webpack": "^3.5.4", 29 | "webpack-dev-server": "^2.7.1", 30 | "webpack-manifest-plugin": "^1.3.1" 31 | }, 32 | "devDependencies": { 33 | "babel-preset-stage-2": "^6.24.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Chapter11/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter11/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter11/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter11/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter11/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter11/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter11/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter11/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter11/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter11/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter11/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter11/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter11/public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-app.js'); 2 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-messaging.js'); 3 | 4 | firebase.initializeApp({ 5 | 'messagingSenderId': '85734589405' 6 | }); 7 | 8 | console.log(firebase.messaging()); -------------------------------------------------------------------------------- /Chapter11/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chatastrophe", 3 | "short_name": "Chatastrophe", 4 | "icons": [ 5 | { 6 | "src":"/assets/icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/icon-256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/assets/icon-384.png", 17 | "sizes": "384x384", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/assets/icon-512.png", 22 | "sizes": "512x512", 23 | "type": "image/png" 24 | } 25 | ], 26 | "start_url": "/?utm_source=homescreen", 27 | "background_color": "#e05a47", 28 | "theme_color": "#e05a47", 29 | "display": "standalone" 30 | } -------------------------------------------------------------------------------- /Chapter11/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /Chapter11/src/components/AsyncComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default function asyncComponent(getComponent) { 4 | return class AsyncComponent extends Component { 5 | state = { Component: null }; 6 | 7 | componentWillMount() { 8 | if (!this.state.Component) { 9 | getComponent().then(Component => { 10 | this.setState({ Component }); 11 | }); 12 | } 13 | } 14 | 15 | render() { 16 | const { Component } = this.state; 17 | if (Component) { 18 | return ; 19 | } 20 | return null; 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /Chapter11/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = (props) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /Chapter11/src/components/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | 4 | class LoginContainer extends Component { 5 | state = { email: '', password: '', error: '' }; 6 | 7 | handleEmailChange = event => { 8 | this.setState({ email: event.target.value }); 9 | }; 10 | 11 | handlePasswordChange = event => { 12 | this.setState({ password: event.target.value }); 13 | }; 14 | 15 | handleSubmit = event => { 16 | event.preventDefault(); 17 | this.setState({ error: '' }); 18 | if (this.state.email && this.state.password) { 19 | this.login(); 20 | } else { 21 | this.setState({ error: 'Please fill in both fields.' }); 22 | } 23 | }; 24 | 25 | onLogin() { 26 | this.props.history.push("/"); 27 | } 28 | 29 | login() { 30 | firebase 31 | .auth() 32 | .signInWithEmailAndPassword(this.state.email, this.state.password) 33 | .then(res => { 34 | this.onLogin(); 35 | }) 36 | .catch(error => { 37 | if (error.code === 'auth/user-not-found') { 38 | this.signup(); 39 | } else { 40 | this.setState({ error: 'Error logging in.' }); 41 | } 42 | }); 43 | } 44 | 45 | signup() { 46 | firebase 47 | .auth() 48 | .createUserWithEmailAndPassword(this.state.email, this.state.password) 49 | .then(res => { 50 | this.onLogin(); 51 | }) 52 | .catch(error => { 53 | console.log(error); 54 | this.setState({ error: 'Error signing up.' }); 55 | }); 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |
62 |
63 |

Sign in or sign up by entering your email and password.

64 | 70 | 76 |

77 | {this.state.error} 78 |

79 | 82 |
83 |
84 | ); 85 | } 86 | } 87 | 88 | export default LoginContainer; 89 | -------------------------------------------------------------------------------- /Chapter11/src/components/UserContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Header from './Header'; 4 | 5 | export default class UserContainer extends Component { 6 | renderedUserEmail = false; 7 | 8 | getAuthor = author => { 9 | if (!this.renderedUserEmail) { 10 | this.renderedUserEmail = true; 11 | return

{author}

; 12 | } 13 | }; 14 | 15 | render() { 16 | return ( 17 |
18 |
19 | 20 | 21 | 22 |
23 | {this.props.messagesLoaded ? ( 24 |
25 | {this.props.messages.map(msg => { 26 | if (msg.user_id === this.props.userID) { 27 | return ( 28 |
29 | {this.getAuthor(msg.author)} 30 |

{msg.msg}

31 |
32 | ); 33 | } 34 | })} 35 |
36 | ) : ( 37 |
38 | logo 39 |
40 | )} 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Chapter11/src/components/app.css: -------------------------------------------------------------------------------- 1 | #test-image { 2 | margin-top: 20px; 3 | } 4 | 5 | h1, p { 6 | margin: 0; 7 | } 8 | 9 | button, input { 10 | border: none; 11 | background-color: white; 12 | } 13 | 14 | input { 15 | border: 1px solid rgba(0,0,0,.2); 16 | padding: 0 5px; 17 | } 18 | 19 | button:hover { 20 | cursor: pointer; 21 | opacity: .75; 22 | } 23 | 24 | button.red { 25 | background-color: #A62824; 26 | color: white; 27 | padding: 7px 10px; 28 | border-radius: 3px; 29 | text-transform: uppercase; 30 | } 31 | 32 | button.red.light { 33 | background-color: #A62824; 34 | } 35 | 36 | button.red:focus { 37 | background-color: #7b1c19; 38 | } 39 | 40 | #ChatContainer #chat-input { 41 | position: absolute; 42 | bottom: 0; 43 | left: 0; 44 | width: 100%; 45 | padding-top: 5px; 46 | background-color: white; 47 | } 48 | 49 | #ChatContainer #chat-input textarea { 50 | width: 85%; 51 | border-radius: 10px; 52 | border: none; 53 | border: 1px solid rgba(0,0,0,.2); 54 | padding: 5px 8px; 55 | float: left; 56 | } 57 | 58 | #ChatContainer #chat-input button { 59 | background: none; 60 | border: none; 61 | width: 15%; 62 | padding-top: 6px; 63 | } 64 | 65 | #ChatContainer #chat-input button svg { 66 | width: 24px; 67 | height: 24px; 68 | text-align: center; 69 | } 70 | 71 | #ChatContainer #chat-input button:hover { 72 | cursor: pointer; 73 | } 74 | 75 | #ChatContainer #chat-input button:hover svg path { 76 | fill: #A62824; 77 | } 78 | 79 | #message-container { 80 | height: calc(100% - 45px); 81 | overflow-y: auto; 82 | padding: 0 10px; 83 | } 84 | 85 | .message { 86 | margin: 7px 0; 87 | 88 | } 89 | 90 | .message p { 91 | border-radius: 5px; 92 | padding: 5px; 93 | background-color: #eaeaea; 94 | display: inline-block; 95 | } 96 | 97 | .message.mine { 98 | text-align: right; 99 | } 100 | 101 | .message a { 102 | text-decoration: none; 103 | } 104 | 105 | .message a:visited, .message a { 106 | color: black; 107 | } 108 | 109 | .message a:hover { 110 | color: #A62824; 111 | } 112 | 113 | .message p.author { 114 | background-color: white; 115 | font-size: .8em; 116 | display: block; 117 | text-decoration: none; 118 | } 119 | 120 | #LoginContainer form { 121 | max-width: 300px; 122 | margin: 0 auto; 123 | display: flex; 124 | justify-content: center; 125 | align-items: center; 126 | flex-direction: column; 127 | height: 80%; 128 | } 129 | 130 | #LoginContainer form p { 131 | font-size: .8em; 132 | text-align:center; 133 | } 134 | 135 | #LoginContainer form input { 136 | width: 200px; 137 | margin: 10px 0; 138 | height: 30px; 139 | } 140 | 141 | .error { 142 | margin: 5px 0; 143 | color: red; 144 | } -------------------------------------------------------------------------------- /Chapter11/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './components/App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('./components/App', () => { 15 | const NextApp = require('./components/App').default; 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); 22 | }); 23 | } -------------------------------------------------------------------------------- /Chapter11/src/resources/NotificationResource.js: -------------------------------------------------------------------------------- 1 | export default class NotificationResource { 2 | allTokens = []; 3 | tokensLoaded = false; 4 | user = null; 5 | 6 | constructor(messaging, database) { 7 | this.messaging = messaging; 8 | this.database = database; 9 | this.messaging 10 | .requestPermission() 11 | .then(res => { 12 | console.log('Permission granted'); 13 | }) 14 | .catch(err => { 15 | console.log('no access', err); 16 | }); 17 | this.setupTokenRefresh(); 18 | this.database.ref('/fcmTokens').on('value', snapshot => { 19 | this.allTokens = snapshot.val(); 20 | this.tokensLoaded = true; 21 | }); 22 | } 23 | 24 | setupTokenRefresh() { 25 | this.messaging.onTokenRefresh(() => { 26 | this.saveTokenToServer(); 27 | }); 28 | } 29 | 30 | saveTokenToServer() { 31 | this.messaging.getToken().then(res => { 32 | if (this.tokensLoaded) { 33 | const existingToken = this.findExistingToken(res); 34 | if (existingToken) { 35 | firebase 36 | .database() 37 | .ref(`/fcmTokens/${existingToken}`) 38 | .set({ 39 | token: res, 40 | user_id: this.user.uid 41 | }); 42 | } else { 43 | this.registerToken(res); 44 | } 45 | } 46 | }); 47 | } 48 | 49 | registerToken(token) { 50 | firebase 51 | .database() 52 | .ref('fcmTokens/') 53 | .push({ 54 | token: token, 55 | user_id: this.user.uid 56 | }); 57 | } 58 | 59 | findExistingToken(tokenToSave) { 60 | for (let tokenKey in this.allTokens) { 61 | const token = this.allTokens[tokenKey].token; 62 | if (token === tokenToSave) { 63 | return tokenKey; 64 | } 65 | } 66 | return false; 67 | } 68 | 69 | changeUser(user) { 70 | this.user = user; 71 | this.saveTokenToServer(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Chapter11/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:8080', 8 | 'webpack/hot/only-dev-server', 9 | __dirname + "/src/index.js" 10 | ], 11 | output: { 12 | path: __dirname + "/public", 13 | filename: "bundle.js", 14 | publicPath: "/" 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | query: { 23 | presets: ['es2015', 'react', 'stage-2'], 24 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 25 | } 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { loader: "style-loader" }, 31 | { loader: "css-loader" } 32 | ] 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.HotModuleReplacementPlugin(), 38 | new HtmlWebpackPlugin({ 39 | inject: true, 40 | template: __dirname + '/public/index.html', 41 | }) 42 | ], 43 | devServer: { 44 | contentBase: "./public", 45 | historyApiFallback: true, 46 | inline: true, 47 | hot: true 48 | } 49 | }; -------------------------------------------------------------------------------- /Chapter11/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/src/index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'static/js/[name].[hash:8].js', 10 | chunkFilename: 'static/js/[name].[hash:8].chunk.js', 11 | publicPath: './' 12 | }, 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.js$/, 17 | exclude: /node_modules/, 18 | loader: 'babel-loader', 19 | query: { 20 | presets: ['es2015', 'react', 'stage-2'], 21 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 22 | } 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 27 | }, 28 | { 29 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.json$/], 30 | loader: 'file-loader', 31 | options: { 32 | name: 'static/media/[name].[ext]' 33 | } 34 | } 35 | ] 36 | }, 37 | plugins: [ 38 | new webpack.DefinePlugin({ 39 | 'process.env': { 40 | NODE_ENV: JSON.stringify('production') 41 | } 42 | }), 43 | new HtmlWebpackPlugin({ 44 | inject: true, 45 | template: __dirname + '/public/index.html', 46 | minify: { 47 | removeComments: true, 48 | collapseWhitespace: true, 49 | removeRedundantAttributes: true, 50 | useShortDoctype: true, 51 | removeEmptyAttributes: true, 52 | removeStyleLinkTypeAttributes: true, 53 | keepClosingSlash: true, 54 | minifyJS: true, 55 | minifyCSS: true, 56 | minifyURLs: true 57 | } 58 | }), 59 | new webpack.optimize.UglifyJsPlugin({ 60 | compress: { 61 | warnings: false, 62 | reduce_vars: false 63 | }, 64 | output: { 65 | comments: false 66 | }, 67 | sourceMap: true 68 | }), 69 | new ManifestPlugin({ 70 | fileName: 'asset-manifest.json' 71 | }) 72 | ] 73 | }; 74 | -------------------------------------------------------------------------------- /Chapter12/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "chatastrophe-draft" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Chapter12/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /public/secrets.js 4 | -------------------------------------------------------------------------------- /Chapter12/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Chapter12/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const admin = require('firebase-admin'); 3 | admin.initializeApp(functions.config().firebase); 4 | 5 | exports.sendNotifications = functions.database 6 | .ref('/messages/{messageId}') 7 | .onWrite(event => { 8 | const snapshot = event.data; 9 | if (snapshot.previous.val()) { 10 | return; 11 | } 12 | const payload = { 13 | notification: { 14 | title: `${snapshot.val().author}`, 15 | body: `${snapshot.val().msg}`, 16 | icon: 'assets/icon.png', 17 | click_action: `https://${functions.config().firebase.authDomain}` 18 | } 19 | }; 20 | return admin 21 | .database() 22 | .ref('fcmTokens') 23 | .once('value') 24 | .then(allTokens => { 25 | if (allTokens.val()) { 26 | const tokens = []; 27 | for (let fcmTokenKey in allTokens.val()) { 28 | const fcmToken = allTokens.val()[fcmTokenKey]; 29 | if (fcmToken.user_id !== snapshot.val().user_id) { 30 | tokens.push(fcmToken.token); 31 | } 32 | } 33 | if (tokens.length > 0) { 34 | return admin 35 | .messaging() 36 | .sendToDevice(tokens, payload) 37 | .then(response => { 38 | const tokensToRemove = []; 39 | response.results.forEach((result, index) => { 40 | const error = result.error; 41 | if (error) { 42 | console.error( 43 | 'Failure sending notification to', 44 | tokens[index], 45 | error 46 | ); 47 | if ( 48 | error.code === 'messaging/invalid-registration-token' || 49 | error.code === 50 | 'messaging/registration-token-not-registered' 51 | ) { 52 | tokensToRemove.push( 53 | allTokens.ref.child(tokens[index]).remove() 54 | ); 55 | } 56 | } 57 | }); 58 | return Promise.all(tokensToRemove); 59 | }); 60 | } 61 | } 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /Chapter12/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "~4.2.1", 6 | "firebase-functions": "^0.5.7" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /Chapter12/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server", 11 | "deploy": "npm run build && firebase deploy" 12 | }, 13 | "dependencies": { 14 | "babel-core": "^6.25.0", 15 | "babel-loader": "^7.1.1", 16 | "babel-plugin-transform-class-properties": "^6.24.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "css-loader": "^0.28.4", 20 | "file-loader": "^0.11.2", 21 | "fs-extra": "^4.0.1", 22 | "html-webpack-plugin": "^2.30.1", 23 | "react": "^15.6.1", 24 | "react-dom": "^15.6.1", 25 | "react-hot-loader": "3.0.0", 26 | "react-router-dom": "^4.2.2", 27 | "style-loader": "^0.18.2", 28 | "webpack": "^3.5.4", 29 | "webpack-dev-server": "^2.7.1", 30 | "webpack-manifest-plugin": "^1.3.1" 31 | }, 32 | "devDependencies": { 33 | "babel-preset-stage-2": "^6.24.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Chapter12/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter12/public/assets/favicon.ico -------------------------------------------------------------------------------- /Chapter12/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter12/public/assets/icon-256.png -------------------------------------------------------------------------------- /Chapter12/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter12/public/assets/icon-384.png -------------------------------------------------------------------------------- /Chapter12/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter12/public/assets/icon-512.png -------------------------------------------------------------------------------- /Chapter12/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter12/public/assets/icon.png -------------------------------------------------------------------------------- /Chapter12/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/Chapter12/public/assets/splash.png -------------------------------------------------------------------------------- /Chapter12/public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-app.js'); 2 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-messaging.js'); 3 | 4 | firebase.initializeApp({ 5 | messagingSenderId: '85734589405' 6 | }); 7 | 8 | const CACHE_NAME = 'v1'; 9 | 10 | self.addEventListener('install', event => { 11 | event.waitUntil( 12 | caches.open(CACHE_NAME).then(cache => { 13 | fetch('asset-manifest.json').then(response => { 14 | if (response.ok) { 15 | response.json().then(manifest => { 16 | const urls = Object.keys(manifest).map(key => manifest[key]); 17 | urls.push('/'); 18 | urls.push('/assets/icon.png'); 19 | cache.addAll(urls); 20 | }); 21 | } 22 | }); 23 | }) 24 | ); 25 | }); 26 | 27 | self.addEventListener('fetch', event => { 28 | event.respondWith( 29 | caches.match(event.request).then(response => { 30 | return response || fetch(event.request); 31 | }) 32 | ); 33 | }); 34 | 35 | self.addEventListener('activate', event => { 36 | event.waitUntil( 37 | cache.keys().then(keyList => { 38 | Promise.all( 39 | keyList.map(key => { 40 | if (key !== CACHE_NAME) { 41 | return caches.delete(key); 42 | } 43 | }) 44 | ); 45 | }) 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /Chapter12/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chatastrophe", 3 | "short_name": "Chatastrophe", 4 | "icons": [ 5 | { 6 | "src":"/assets/icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/icon-256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/assets/icon-384.png", 17 | "sizes": "384x384", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/assets/icon-512.png", 22 | "sizes": "512x512", 23 | "type": "image/png" 24 | } 25 | ], 26 | "start_url": "/?utm_source=homescreen", 27 | "background_color": "#e05a47", 28 | "theme_color": "#e05a47", 29 | "display": "standalone" 30 | } -------------------------------------------------------------------------------- /Chapter12/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /Chapter12/src/components/AsyncComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default function asyncComponent(getComponent) { 4 | return class AsyncComponent extends Component { 5 | state = { Component: null }; 6 | 7 | componentWillMount() { 8 | if (!this.state.Component) { 9 | getComponent().then(Component => { 10 | this.setState({ Component }); 11 | }); 12 | } 13 | } 14 | 15 | render() { 16 | const { Component } = this.state; 17 | if (Component) { 18 | return ; 19 | } 20 | return null; 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /Chapter12/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = (props) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /Chapter12/src/components/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | 4 | class LoginContainer extends Component { 5 | state = { email: '', password: '', error: '' }; 6 | 7 | handleEmailChange = event => { 8 | this.setState({ email: event.target.value }); 9 | }; 10 | 11 | handlePasswordChange = event => { 12 | this.setState({ password: event.target.value }); 13 | }; 14 | 15 | handleSubmit = event => { 16 | event.preventDefault(); 17 | this.setState({ error: '' }); 18 | if (this.state.email && this.state.password) { 19 | this.login(); 20 | } else { 21 | this.setState({ error: 'Please fill in both fields.' }); 22 | } 23 | }; 24 | 25 | onLogin() { 26 | this.props.history.push("/"); 27 | } 28 | 29 | login() { 30 | firebase 31 | .auth() 32 | .signInWithEmailAndPassword(this.state.email, this.state.password) 33 | .then(res => { 34 | this.onLogin(); 35 | }) 36 | .catch(error => { 37 | if (error.code === 'auth/user-not-found') { 38 | this.signup(); 39 | } else { 40 | this.setState({ error: 'Error logging in.' }); 41 | } 42 | }); 43 | } 44 | 45 | signup() { 46 | firebase 47 | .auth() 48 | .createUserWithEmailAndPassword(this.state.email, this.state.password) 49 | .then(res => { 50 | this.onLogin(); 51 | }) 52 | .catch(error => { 53 | console.log(error); 54 | this.setState({ error: 'Error signing up.' }); 55 | }); 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |
62 |
63 |

Sign in or sign up by entering your email and password.

64 | 70 | 76 |

77 | {this.state.error} 78 |

79 | 82 |
83 |
84 | ); 85 | } 86 | } 87 | 88 | export default LoginContainer; 89 | -------------------------------------------------------------------------------- /Chapter12/src/components/UserContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Header from './Header'; 4 | 5 | export default class UserContainer extends Component { 6 | renderedUserEmail = false; 7 | 8 | getAuthor = author => { 9 | if (!this.renderedUserEmail) { 10 | this.renderedUserEmail = true; 11 | return

{author}

; 12 | } 13 | }; 14 | 15 | render() { 16 | return ( 17 |
18 |
19 | 20 | 21 | 22 |
23 | {this.props.messagesLoaded ? ( 24 |
25 | {this.props.messages.map(msg => { 26 | if (msg.user_id === this.props.userID) { 27 | return ( 28 |
29 | {this.getAuthor(msg.author)} 30 |

{msg.msg}

31 |
32 | ); 33 | } 34 | })} 35 |
36 | ) : ( 37 |
38 | logo 39 |
40 | )} 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Chapter12/src/components/app.css: -------------------------------------------------------------------------------- 1 | #test-image { 2 | margin-top: 20px; 3 | } 4 | 5 | h1, p { 6 | margin: 0; 7 | } 8 | 9 | button, input { 10 | border: none; 11 | background-color: white; 12 | } 13 | 14 | input { 15 | border: 1px solid rgba(0,0,0,.2); 16 | padding: 0 5px; 17 | } 18 | 19 | button:hover { 20 | cursor: pointer; 21 | opacity: .75; 22 | } 23 | 24 | button.red { 25 | background-color: #A62824; 26 | color: white; 27 | padding: 7px 10px; 28 | border-radius: 3px; 29 | text-transform: uppercase; 30 | } 31 | 32 | button.red.light { 33 | background-color: #A62824; 34 | } 35 | 36 | button.red:focus { 37 | background-color: #7b1c19; 38 | } 39 | 40 | #ChatContainer #chat-input { 41 | position: absolute; 42 | bottom: 0; 43 | left: 0; 44 | width: 100%; 45 | padding-top: 5px; 46 | background-color: white; 47 | } 48 | 49 | #ChatContainer #chat-input textarea { 50 | width: 85%; 51 | border-radius: 10px; 52 | border: none; 53 | border: 1px solid rgba(0,0,0,.2); 54 | padding: 5px 8px; 55 | float: left; 56 | } 57 | 58 | #ChatContainer #chat-input button { 59 | background: none; 60 | border: none; 61 | width: 15%; 62 | padding-top: 6px; 63 | } 64 | 65 | #ChatContainer #chat-input button svg { 66 | width: 24px; 67 | height: 24px; 68 | text-align: center; 69 | } 70 | 71 | #ChatContainer #chat-input button:hover { 72 | cursor: pointer; 73 | } 74 | 75 | #ChatContainer #chat-input button:hover svg path { 76 | fill: #A62824; 77 | } 78 | 79 | #message-container { 80 | height: calc(100% - 45px); 81 | overflow-y: auto; 82 | padding: 0 10px; 83 | } 84 | 85 | .message { 86 | margin: 7px 0; 87 | 88 | } 89 | 90 | .message p { 91 | border-radius: 5px; 92 | padding: 5px; 93 | background-color: #eaeaea; 94 | display: inline-block; 95 | } 96 | 97 | .message.mine { 98 | text-align: right; 99 | } 100 | 101 | .message a { 102 | text-decoration: none; 103 | } 104 | 105 | .message a:visited, .message a { 106 | color: black; 107 | } 108 | 109 | .message a:hover { 110 | color: #A62824; 111 | } 112 | 113 | .message p.author { 114 | background-color: white; 115 | font-size: .8em; 116 | display: block; 117 | text-decoration: none; 118 | } 119 | 120 | #LoginContainer form { 121 | max-width: 300px; 122 | margin: 0 auto; 123 | display: flex; 124 | justify-content: center; 125 | align-items: center; 126 | flex-direction: column; 127 | height: 80%; 128 | } 129 | 130 | #LoginContainer form p { 131 | font-size: .8em; 132 | text-align:center; 133 | } 134 | 135 | #LoginContainer form input { 136 | width: 200px; 137 | margin: 10px 0; 138 | height: 30px; 139 | } 140 | 141 | .error { 142 | margin: 5px 0; 143 | color: red; 144 | } -------------------------------------------------------------------------------- /Chapter12/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './components/App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('./components/App', () => { 15 | const NextApp = require('./components/App').default; 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); 22 | }); 23 | } -------------------------------------------------------------------------------- /Chapter12/src/resources/NotificationResource.js: -------------------------------------------------------------------------------- 1 | export default class NotificationResource { 2 | allTokens = []; 3 | tokensLoaded = false; 4 | user = null; 5 | 6 | constructor(messaging, database) { 7 | this.messaging = messaging; 8 | this.database = database; 9 | this.messaging 10 | .requestPermission() 11 | .then(res => { 12 | console.log('Permission granted'); 13 | }) 14 | .catch(err => { 15 | console.log('no access', err); 16 | }); 17 | this.setupTokenRefresh(); 18 | this.database.ref('/fcmTokens').on('value', snapshot => { 19 | this.allTokens = snapshot.val(); 20 | this.tokensLoaded = true; 21 | }); 22 | } 23 | 24 | setupTokenRefresh() { 25 | this.messaging.onTokenRefresh(() => { 26 | this.saveTokenToServer(); 27 | }); 28 | } 29 | 30 | saveTokenToServer() { 31 | this.messaging.getToken().then(res => { 32 | if (this.tokensLoaded) { 33 | const existingToken = this.findExistingToken(res); 34 | if (existingToken) { 35 | firebase 36 | .database() 37 | .ref(`/fcmTokens/${existingToken}`) 38 | .set({ 39 | token: res, 40 | user_id: this.user.uid 41 | }); 42 | } else { 43 | this.registerToken(res); 44 | } 45 | } 46 | }); 47 | } 48 | 49 | registerToken(token) { 50 | firebase 51 | .database() 52 | .ref('fcmTokens/') 53 | .push({ 54 | token: token, 55 | user_id: this.user.uid 56 | }); 57 | } 58 | 59 | findExistingToken(tokenToSave) { 60 | for (let tokenKey in this.allTokens) { 61 | const token = this.allTokens[tokenKey].token; 62 | if (token === tokenToSave) { 63 | return tokenKey; 64 | } 65 | } 66 | return false; 67 | } 68 | 69 | changeUser(user) { 70 | this.user = user; 71 | this.saveTokenToServer(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Chapter12/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:8080', 8 | 'webpack/hot/only-dev-server', 9 | __dirname + "/src/index.js" 10 | ], 11 | output: { 12 | path: __dirname + "/public", 13 | filename: "bundle.js", 14 | publicPath: "/" 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | query: { 23 | presets: ['es2015', 'react', 'stage-2'], 24 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 25 | } 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { loader: "style-loader" }, 31 | { loader: "css-loader" } 32 | ] 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.HotModuleReplacementPlugin(), 38 | new HtmlWebpackPlugin({ 39 | inject: true, 40 | template: __dirname + '/public/index.html', 41 | }) 42 | ], 43 | devServer: { 44 | contentBase: "./public", 45 | historyApiFallback: true, 46 | inline: true, 47 | hot: true 48 | } 49 | }; -------------------------------------------------------------------------------- /Chapter12/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + '/src/index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'static/js/[name].[hash:8].js', 10 | chunkFilename: 'static/js/[name].[hash:8].chunk.js', 11 | publicPath: './' 12 | }, 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.js$/, 17 | exclude: /node_modules/, 18 | loader: 'babel-loader', 19 | query: { 20 | presets: ['es2015', 'react', 'stage-2'], 21 | plugins: ['react-hot-loader/babel', 'transform-class-properties'] 22 | } 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 27 | }, 28 | { 29 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.json$/], 30 | loader: 'file-loader', 31 | options: { 32 | name: 'static/media/[name].[ext]' 33 | } 34 | } 35 | ] 36 | }, 37 | plugins: [ 38 | new webpack.DefinePlugin({ 39 | 'process.env': { 40 | NODE_ENV: JSON.stringify('production') 41 | } 42 | }), 43 | new HtmlWebpackPlugin({ 44 | inject: true, 45 | template: __dirname + '/public/index.html', 46 | minify: { 47 | removeComments: true, 48 | collapseWhitespace: true, 49 | removeRedundantAttributes: true, 50 | useShortDoctype: true, 51 | removeEmptyAttributes: true, 52 | removeStyleLinkTypeAttributes: true, 53 | keepClosingSlash: true, 54 | minifyJS: true, 55 | minifyCSS: true, 56 | minifyURLs: true 57 | } 58 | }), 59 | new webpack.optimize.UglifyJsPlugin({ 60 | compress: { 61 | warnings: false, 62 | reduce_vars: false 63 | }, 64 | output: { 65 | comments: false 66 | }, 67 | sourceMap: true 68 | }), 69 | new ManifestPlugin({ 70 | fileName: 'asset-manifest.json' 71 | }) 72 | ] 73 | }; 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Packt 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 | -------------------------------------------------------------------------------- /master/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "chatastrophe-draft" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /master/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /public/secrets.js 4 | -------------------------------------------------------------------------------- /master/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /master/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const admin = require('firebase-admin'); 3 | admin.initializeApp(functions.config().firebase); 4 | 5 | exports.sendNotifications = functions.database 6 | .ref('/messages/{messageId}') 7 | .onWrite(event => { 8 | const snapshot = event.data; 9 | if (snapshot.previous.val()) { 10 | return; 11 | } 12 | const payload = { 13 | notification: { 14 | title: `${snapshot.val().author}`, 15 | body: `${snapshot.val().msg}`, 16 | icon: 'assets/icon.png', 17 | click_action: `https://${functions.config().firebase.authDomain}` 18 | } 19 | }; 20 | return admin 21 | .database() 22 | .ref('fcmTokens') 23 | .once('value') 24 | .then(allTokens => { 25 | if (allTokens.val()) { 26 | const tokens = []; 27 | for (let fcmTokenKey in allTokens.val()) { 28 | const fcmToken = allTokens.val()[fcmTokenKey]; 29 | if (fcmToken.user_id !== snapshot.val().user_id) { 30 | tokens.push(fcmToken.token); 31 | } 32 | } 33 | if (tokens.length > 0) { 34 | return admin 35 | .messaging() 36 | .sendToDevice(tokens, payload) 37 | .then(response => { 38 | const tokensToRemove = []; 39 | response.results.forEach((result, index) => { 40 | const error = result.error; 41 | if (error) { 42 | console.error( 43 | 'Failure sending notification to', 44 | tokens[index], 45 | error 46 | ); 47 | if ( 48 | error.code === 'messaging/invalid-registration-token' || 49 | error.code === 50 | 'messaging/registration-token-not-registered' 51 | ) { 52 | tokensToRemove.push( 53 | allTokens.ref.child(tokens[index]).remove() 54 | ); 55 | } 56 | } 57 | }); 58 | return Promise.all(tokensToRemove); 59 | }); 60 | } 61 | } 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /master/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "~4.2.1", 6 | "firebase-functions": "^0.5.7" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /master/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatastrophe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:scottdomes/chatastrophe.git", 6 | "author": "YScott Domes ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "node scripts/copy_assets.js && node_modules/.bin/webpack --config webpack.config.prod.js", 10 | "start": "node_modules/.bin/webpack-dev-server", 11 | "deploy": "npm run build && firebase deploy" 12 | }, 13 | "dependencies": { 14 | "babel-core": "^6.25.0", 15 | "babel-loader": "^7.1.1", 16 | "babel-plugin-transform-class-properties": "^6.24.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "css-loader": "^0.28.4", 20 | "file-loader": "^0.11.2", 21 | "fs-extra": "^4.0.1", 22 | "html-webpack-plugin": "^2.30.1", 23 | "react": "^15.6.1", 24 | "react-dom": "^15.6.1", 25 | "react-hot-loader": "3.0.0", 26 | "react-router-dom": "^4.2.2", 27 | "style-loader": "^0.18.2", 28 | "webpack": "^3.5.4", 29 | "webpack-dev-server": "^2.7.1", 30 | "webpack-manifest-plugin": "^1.3.1" 31 | }, 32 | "devDependencies": { 33 | "babel-preset-stage-2": "^6.24.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /master/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/master/public/assets/favicon.ico -------------------------------------------------------------------------------- /master/public/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/master/public/assets/icon-256.png -------------------------------------------------------------------------------- /master/public/assets/icon-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/master/public/assets/icon-384.png -------------------------------------------------------------------------------- /master/public/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/master/public/assets/icon-512.png -------------------------------------------------------------------------------- /master/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/master/public/assets/icon.png -------------------------------------------------------------------------------- /master/public/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Progressive-Web-Apps-with-React/029502493903228d9b7a6c11c3b92ea2c3ce9a8b/master/public/assets/splash.png -------------------------------------------------------------------------------- /master/public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-app.js'); 2 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-messaging.js'); 3 | 4 | firebase.initializeApp({ 5 | messagingSenderId: '85734589405' 6 | }); 7 | 8 | const CACHE_NAME = 'v1'; 9 | 10 | self.addEventListener('install', event => { 11 | event.waitUntil( 12 | caches.open(CACHE_NAME).then(cache => { 13 | fetch('asset-manifest.json').then(response => { 14 | if (response.ok) { 15 | response.json().then(manifest => { 16 | const urls = Object.keys(manifest).map(key => manifest[key]); 17 | urls.push('/'); 18 | urls.push('/assets/icon.png'); 19 | cache.addAll(urls); 20 | }); 21 | } 22 | }); 23 | }) 24 | ); 25 | }); 26 | 27 | self.addEventListener('fetch', event => { 28 | event.respondWith( 29 | caches.match(event.request).then(response => { 30 | return response || fetch(event.request); 31 | }) 32 | ); 33 | }); 34 | 35 | self.addEventListener('activate', event => { 36 | event.waitUntil( 37 | caches.keys().then(keyList => { 38 | Promise.all( 39 | keyList.map(key => { 40 | if (key !== CACHE_NAME) { 41 | return caches.delete(key); 42 | } 43 | }) 44 | ); 45 | }) 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /master/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chatastrophe", 3 | "short_name": "Chatastrophe", 4 | "icons": [ 5 | { 6 | "src":"/assets/icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/icon-256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/assets/icon-384.png", 17 | "sizes": "384x384", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/assets/icon-512.png", 22 | "sizes": "512x512", 23 | "type": "image/png" 24 | } 25 | ], 26 | "start_url": "/?utm_source=homescreen", 27 | "background_color": "#e05a47", 28 | "theme_color": "#e05a47", 29 | "display": "standalone" 30 | } -------------------------------------------------------------------------------- /master/scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | fs.copySync('public', 'build', { 4 | dereference: true, 5 | filter: file => file !== 'public/index.html' 6 | }); -------------------------------------------------------------------------------- /master/src/components/AsyncComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default function asyncComponent(getComponent) { 4 | return class AsyncComponent extends Component { 5 | state = { Component: null }; 6 | 7 | componentWillMount() { 8 | if (!this.state.Component) { 9 | getComponent().then(Component => { 10 | this.setState({ Component }); 11 | }); 12 | } 13 | } 14 | 15 | render() { 16 | const { Component } = this.state; 17 | if (Component) { 18 | return ; 19 | } 20 | return null; 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /master/src/components/ChatContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import ReactDOM from 'react-dom'; 4 | import Header from './Header'; 5 | 6 | export default class ChatContainer extends Component { 7 | state = { newMessage: '' }; 8 | 9 | componentDidMount() { 10 | this.scrollToBottom(); 11 | } 12 | 13 | componentDidUpdate(previousProps) { 14 | if (previousProps.messages.length !== this.props.messages.length) { 15 | this.scrollToBottom(); 16 | } 17 | } 18 | 19 | scrollToBottom = () => { 20 | const messageContainer = ReactDOM.findDOMNode(this.messageContainer); 21 | if (messageContainer) { 22 | messageContainer.scrollTop = messageContainer.scrollHeight; 23 | } 24 | }; 25 | 26 | handleLogout = () => { 27 | firebase.auth().signOut(); 28 | }; 29 | 30 | handleInputChange = e => { 31 | this.setState({ newMessage: e.target.value }); 32 | }; 33 | 34 | handleSubmit = () => { 35 | this.props.onSubmit(this.state.newMessage); 36 | this.setState({ newMessage: '' }); 37 | }; 38 | 39 | handleKeyDown = e => { 40 | if (e.key === 'Enter') { 41 | e.preventDefault(); 42 | this.handleSubmit(); 43 | } 44 | }; 45 | 46 | getAuthor = (msg, nextMsg) => { 47 | if (!nextMsg || nextMsg.author !== msg.author) { 48 | return ( 49 |

50 | {msg.author} 51 |

52 | ); 53 | } 54 | }; 55 | 56 | render() { 57 | return ( 58 |
59 |
60 | 63 |
64 | {this.props.messagesLoaded ? ( 65 |
{ 68 | this.messageContainer = element; 69 | }}> 70 | {this.props.messages.map((msg, i) => ( 71 |
75 |

{msg.msg}

76 | {this.getAuthor(msg, this.props.messages[i + 1])} 77 |
78 | ))} 79 |
80 | ) : ( 81 |
82 | logo 83 |
84 | )} 85 |
86 |