From 26e1468905000a0549e9f5c7aea3b347b4fc7e12 Mon Sep 17 00:00:00 2001 From: Xin Wei Date: Tue, 16 May 2017 17:28:46 -0700 Subject: [PATCH] Add sample for server-side rendering of a React app (#1) * add sample for server-side rendering of a react app * update main Readme --- README.md | 9 ++ isomorphic-react-app/.gitignore | 3 + isomorphic-react-app/README.md | 38 +++++ isomorphic-react-app/database.rules.json | 5 + isomorphic-react-app/firebase.json | 14 ++ isomorphic-react-app/functions/data-seed.json | 115 +++++++++++++++ .../functions/firebase-database.js | 59 ++++++++ isomorphic-react-app/functions/index.js | 60 ++++++++ isomorphic-react-app/functions/package.json | 13 ++ isomorphic-react-app/functions/template.js | 47 ++++++ isomorphic-react-app/public/assets/custom.css | 134 ++++++++++++++++++ isomorphic-react-app/src/components/Home.jsx | 50 +++++++ isomorphic-react-app/src/components/User.jsx | 118 +++++++++++++++ isomorphic-react-app/src/containers/App.jsx | 71 ++++++++++ .../src/containers/ClientApp.js | 11 ++ .../src/containers/ServerApp.jsx | 20 +++ isomorphic-react-app/src/package.json | 30 ++++ .../src/webpack.client.config.js | 27 ++++ isomorphic-react-app/src/webpack.config.js | 37 +++++ .../src/webpack.server.config.js | 32 +++++ 20 files changed, 893 insertions(+) create mode 100644 isomorphic-react-app/.gitignore create mode 100644 isomorphic-react-app/README.md create mode 100644 isomorphic-react-app/database.rules.json create mode 100644 isomorphic-react-app/firebase.json create mode 100644 isomorphic-react-app/functions/data-seed.json create mode 100644 isomorphic-react-app/functions/firebase-database.js create mode 100644 isomorphic-react-app/functions/index.js create mode 100644 isomorphic-react-app/functions/package.json create mode 100644 isomorphic-react-app/functions/template.js create mode 100644 isomorphic-react-app/public/assets/custom.css create mode 100644 isomorphic-react-app/src/components/Home.jsx create mode 100644 isomorphic-react-app/src/components/User.jsx create mode 100644 isomorphic-react-app/src/containers/App.jsx create mode 100644 isomorphic-react-app/src/containers/ClientApp.js create mode 100644 isomorphic-react-app/src/containers/ServerApp.jsx create mode 100644 isomorphic-react-app/src/package.json create mode 100644 isomorphic-react-app/src/webpack.client.config.js create mode 100644 isomorphic-react-app/src/webpack.config.js create mode 100644 isomorphic-react-app/src/webpack.server.config.js diff --git a/README.md b/README.md index fdbe984..7e73e96 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ This quickstart sample demonstrates using **Cloud Functions** triggered by **Fir This quickstart sample demonstrates using **Cloud Functions** triggered by **PubSub events**. The functions log the PubSub payload in a Hello world message. +### [Authenticated JSON API](/authenticated-json-api) +This sample shows how to authenticate access to a JSON API to only allow access to data for a specific Firebase user. + ### [Authorized HTTP endpoint](/authorized-https-endpoint) This samples shows how to restrict an HTTPS Function to only the Firebase users of your app. @@ -64,11 +67,17 @@ Uses a Realtime Database trigger. Demonstrates how to authorize with a 3rd party sign-in mechanism (LinkedIn in this case), create a Firebase custom auth token, update the user's profile and authorize Firebase. Uses an HTTP trigger. +### [Image Maker](/image-maker) +This sample demonstrates how to create various customized images through Cloud Functions and Hosting and serve it to the client. + ### [Authorize with Instagram](/instagram-auth) Demonstrates how to authorize with a 3rd party sign-in mechanism (Instagram in this case), create a Firebase custom auth token, update the user's profile and authorize Firebase. Uses an HTTP trigger. +### [Isomorphic React App](/isomorphic-react-app) +This sample demonstrates how to create an isomorphic React application with server-side data fetching. This sample uses Cloud Functions, Hosting, and the Realtime Database. + ### [Authorize with LINE](/line-auth) Demonstrates how to authorize with a 3rd party sign-in mechanism (LINE in this case), create a Firebase custom auth token, update the user's profile and authorize Firebase. diff --git a/isomorphic-react-app/.gitignore b/isomorphic-react-app/.gitignore new file mode 100644 index 0000000..33d47c6 --- /dev/null +++ b/isomorphic-react-app/.gitignore @@ -0,0 +1,3 @@ +.firebaserc +node_modules +*.bundle.js diff --git a/isomorphic-react-app/README.md b/isomorphic-react-app/README.md new file mode 100644 index 0000000..bbb2ee0 --- /dev/null +++ b/isomorphic-react-app/README.md @@ -0,0 +1,38 @@ +# Isomorphic React App + +This sample shows how to create an isomorphic React application using using Functions, Hosting, and Realtime Database. + +During the initial request to your app, Hosting will proxy your request to a Node express server. +The server will then load any data that's necessary for your app (through the Realtime Database). It will also generate the markup that's needed from React. It will inject the markup with the preloaded state before passing it to the client. + +This sample uses `react-router` to demonstrate simple routing logic for React. + +The `src` folder contains the source code for React app. It also uses [Webpack](https://github.com/webpack/webpack) to bundle the app and generate two bundles, one for the server, and one for the client. The server bundle is required by the express server to generate the initial markup, and the client bundle is included in the `functions/template.js` - the markup that's being passed onto the client. + +## Setting up the sample + + 1. Create a Firebase Project using the [Firebase Console](https://console.firebase.google.com). + 1. Clone or download this repo and open the `isomorphic-react-app` directory. + 1. You must have the Firebase CLI installed. If you don't have it install it with `npm install -g firebase-tools` and then configure it with `firebase login`. + 1. Configure the CLI locally by using `firebase use --add` and select your project in the list. + 1. Install dependencies locally by running: `cd functions; npm install; cd ../src; npm install` + 1. Run `npm run build` within the `src` folder to start webpack, which will bundle the app. It will output `functions/build/server.bundle.js` and `public/assets/client.bundle.js` + 1. Import the sample `functions/data-seed.json` to your Firebase Realtime Database. For more details, see [https://support.google.com/firebase/answer/6386780?hl=en#import](https://support.google.com/firebase/answer/6386780?hl=en#import) + + +## Deploy and test + +This sample comes with a web-based UI for testing the function. To test it out: + + 1. Deploy your project using `firebase deploy` + 1. Open the app using `firebase open hosting:site`, this will open a browser. + + +## Contributing + +We'd love that you contribute to the project. Before doing so please read our [Contributor guide](../CONTRIBUTING.md). + + +## License + +© Google, 2017. Licensed under an [Apache-2](../LICENSE) license. diff --git a/isomorphic-react-app/database.rules.json b/isomorphic-react-app/database.rules.json new file mode 100644 index 0000000..367f53e --- /dev/null +++ b/isomorphic-react-app/database.rules.json @@ -0,0 +1,5 @@ +{ + "rules": { + ".read": true + } +} diff --git a/isomorphic-react-app/firebase.json b/isomorphic-react-app/firebase.json new file mode 100644 index 0000000..3b91257 --- /dev/null +++ b/isomorphic-react-app/firebase.json @@ -0,0 +1,14 @@ +{ + "hosting": { + "public": "public", + "rewrites": [ + { + "source": "**", + "function": "app" + } + ] + }, + "database": { + "rules": "database.rules.json" + } +} diff --git a/isomorphic-react-app/functions/data-seed.json b/isomorphic-react-app/functions/data-seed.json new file mode 100644 index 0000000..f31e460 --- /dev/null +++ b/isomorphic-react-app/functions/data-seed.json @@ -0,0 +1,115 @@ +{ + "employees" : { + "berthaye" : { + "birthday" : "November 11, 1992", + "email" : "berthaye@fakecompany.com", + "id" : "berthaye", + "image" : "https://firebasestorage.googleapis.com/v0/b/oompa-loompa-aeb91.appspot.com/o/berthaye.png?alt=media&token=e65a55b1-f27d-403b-ad70-8ae89a081303", + "level" : 1, + "location" : "San Francisco, CA, USA", + "name" : "Bertha Ye", + "number" : "1-570-788-4003", + "position" : "Software Engineer", + "twitter" : "@fake-bertha" + }, + "georgebrown" : { + "birthday" : "April 3, 1982", + "email" : "georgebrown@fakecompany.com", + "id" : "georgebrown", + "image" : "https://firebasestorage.googleapis.com/v0/b/oompa-loompa-aeb91.appspot.com/o/georgebrown.png?alt=media&token=d958b887-ff50-4166-a8c0-a032304abf6f", + "level" : 5, + "location" : "San Francisco, CA, USA", + "name" : "George Brown", + "number" : "1-707-658-7561", + "position" : "President and CEO", + "reports" : { + "lydiabennet" : true, + "marcelovaldez" : true, + "tanyabearden" : true + }, + "twitter" : "@fake-georgebrown" + }, + "kellyelrod" : { + "birthday" : "July 8, 1989", + "email" : "kellyelrod@fakecompany.com", + "id" : "kellyelrod", + "image" : "https://firebasestorage.googleapis.com/v0/b/oompa-loompa-aeb91.appspot.com/o/kellyelrod.png?alt=media&token=bd2501c9-2491-4c00-9d40-53923485c836", + "level" : 2, + "location" : "Portland, CA, USA", + "name" : "Kelly Elrod", + "number" : "1-847-384-3036", + "position" : "Marketing Manager", + "twitter" : "@fake-kelly" + }, + "lydiabennet" : { + "birthday" : "November 20, 1979", + "email" : "lydiabennet@fakecompany.com", + "id" : "lydiabennet", + "image" : "https://firebasestorage.googleapis.com/v0/b/oompa-loompa-aeb91.appspot.com/o/lydiabennet.png?alt=media&token=e022d0c2-df05-49d5-945c-d1f29e2e2b71", + "level" : 4, + "location" : "San Francisco, CA, USA", + "name" : "Lydia Bennet", + "number" : "1-309-423-4299", + "position" : "VP of Marketing", + "reports" : { + "kellyelrod" : true + }, + "twitter" : "@fake-lydia" + }, + "marcelovaldez" : { + "birthday" : "May 18, 1980", + "email" : "marcelovaldez@fakecompany.com", + "id" : "marcelovaldez", + "image" : "https://firebasestorage.googleapis.com/v0/b/oompa-loompa-aeb91.appspot.com/o/marcelovaldez.png?alt=media&token=e8ddfda2-5395-40dc-baf1-197dbae9318d", + "level" : 4, + "location" : "Boston, MA, USA", + "name" : "Marcelo Valdez", + "number" : "1-978-401-9913", + "position" : "VP of Sales", + "reports" : { + "staceyhansen" : true + }, + "twitter" : "@fake-marcelo" + }, + "staceyhansen" : { + "birthday" : "April 29, 1990", + "email" : "staceyhansen@fakecompany.com", + "id" : "staceyhansen", + "image" : "https://firebasestorage.googleapis.com/v0/b/oompa-loompa-aeb91.appspot.com/o/staceyhansen.png?alt=media&token=9100f50f-2740-4dd2-bf1d-58e066e0722b", + "level" : 1, + "location" : "San Francisco, CA, USA", + "name" : "Stacey Hansen", + "number" : "1-936-524-2834", + "position" : "Sales Representative", + "twitter" : "@fake-leslie" + }, + "tanyabearden" : { + "birthday" : "December 20, 1982", + "email" : "tanyabearden@fakecompany.com", + "id" : "tanyabearden", + "image" : "https://firebasestorage.googleapis.com/v0/b/oompa-loompa-aeb91.appspot.com/o/tanyabearden.png?alt=media&token=bb575a43-8cef-43b0-b680-9bc400f9b181", + "level" : 4, + "location" : "Portland, OR, USA", + "name" : "Tanya Bearden", + "number" : "1-518-822-6241", + "position" : "VP of Engineering", + "reports" : { + "berthaye" : true, + "tiffanycaldwell" : true + }, + "twitter" : "@fake-tanya" + }, + "tiffanycaldwell" : { + "birthday" : "April 10, 1991", + "email" : "tiffanycaldwell@fakecompany.com", + "id" : "tiffanycaldwell", + "image" : "https://firebasestorage.googleapis.com/v0/b/oompa-loompa-aeb91.appspot.com/o/tiffanycaldwell.png?alt=media&token=1f6c4a8d-4567-4a8a-9673-77295e8e950f", + "level" : 2, + "location" : "Boston, CA, USA", + "name" : "Tiffany Caldwell", + "number" : "1-336-524-5631", + "position" : "Sr Software Engineer", + "twitter" : "@fake-tiffany" + } + } +} diff --git a/isomorphic-react-app/functions/firebase-database.js b/isomorphic-react-app/functions/firebase-database.js new file mode 100644 index 0000000..6533680 --- /dev/null +++ b/isomorphic-react-app/functions/firebase-database.js @@ -0,0 +1,59 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Client and Server Data Fetching Logic +// Uses either the client firebase (initialized from hosting init script) +// Or serverside firebase +const firebase = global.firebase || require('firebase'); + +// Initialize Firebase SDK +// Only should be called once on the server +// the client should already be initialized from hosting init script +const initializeApp = (config) => { + if (firebase.apps.length === 0) { + firebase.initializeApp(config); + } +} + +// Get and return all employees +const getAllEmployees = () => { + return firebase.database().ref('/employees').orderByChild('level').once('value').then((snap) => { + return { employees: snap.val() }; + }); +}; + +// Get and return an employee by their id number +// also fetch all of the employee's direct reports (if any) +const getEmployeeById = (employeeId) => { + return firebase.database().ref(`/employees/${employeeId}`).once('value').then((snap) => { + const promises = []; + const snapshot = snap.val(); + if (snapshot.reports) { + Object.keys(snapshot.reports).forEach((userId) => { + promises.push(firebase.database().ref(`/employees/${userId}`).once('value').then((snap) => snap.val())); + }) + } + return firebase.Promise.all(promises).then((resp) => { + return { currentEmployee: { employee: snapshot, reports: resp } }; + }); + }); +}; + +module.exports = { + getAllEmployees, + getEmployeeById, + initializeApp +}; diff --git a/isomorphic-react-app/functions/index.js b/isomorphic-react-app/functions/index.js new file mode 100644 index 0000000..30fe535 --- /dev/null +++ b/isomorphic-react-app/functions/index.js @@ -0,0 +1,60 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const functions = require('firebase-functions'); +const firebase = require('firebase'); +const app = require('express')(); +const React = require('react'); +const ReactDOMServer = require('react-dom/server'); + +// React App +const ServerApp = React.createFactory(require('./build/server.bundle.js').default); +const template = require('./template'); + +// Server-side Data Loading +const appConfig = functions.config().firebase; +const database = require('./firebase-database'); +database.initializeApp(appConfig); + +// Helper function to get the markup from React, inject the initial state, and +// send the server-side markup to the client +const renderApplication = (url, res, initialState) => { + const html = ReactDOMServer.renderToString(ServerApp({url: url, context: {}, initialState, appConfig })); + const templatedHtml = template({ body: html, initialState: JSON.stringify(initialState)}); + res.send(templatedHtml); +}; + +app.get('/favicon.ico', function(req, res) { + res.send(204); +}); + +app.get('/:userId?', (req, res) => { + res.set('Cache-Control', 'public, max-age=60, s-maxage=180'); + if (req.params.userId) { + // client is requesting user-details page with userId + // load the data for that employee and its direct reports + database.getEmployeeById(req.params.userId).then((resp) => { + renderApplication(req.url, res, resp); + }); + } else { + // index page. load data for all employees + database.getAllEmployees().then((resp) => { + renderApplication(req.url, res, resp); + }); + } +}); + +exports.app = functions.https.onRequest(app); diff --git a/isomorphic-react-app/functions/package.json b/isomorphic-react-app/functions/package.json new file mode 100644 index 0000000..b62c46e --- /dev/null +++ b/isomorphic-react-app/functions/package.json @@ -0,0 +1,13 @@ +{ + "name": "functions-isomorphic-react-app", + "description": "A sample isomorphic React app using Functions and Hosting", + "dependencies": { + "express": "4.15.2", + "firebase": "3.9.0", + "firebase-admin": "4.0.5", + "firebase-functions": "0.5.5", + "react": "15.5.4", + "react-dom": "15.5.4", + "request": "2.81.0" + } +} diff --git a/isomorphic-react-app/functions/template.js b/isomorphic-react-app/functions/template.js new file mode 100644 index 0000000..201789e --- /dev/null +++ b/isomorphic-react-app/functions/template.js @@ -0,0 +1,47 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Template file that the server will use to inject the React markup and +// initial state before sending it to the client + +const template = function(opts) { + return ` + + + + Employee Directory + + + + + + + +
${opts.body}
+ + + + + + + + + `; +} + +module.exports = template; diff --git a/isomorphic-react-app/public/assets/custom.css b/isomorphic-react-app/public/assets/custom.css new file mode 100644 index 0000000..56d1383 --- /dev/null +++ b/isomorphic-react-app/public/assets/custom.css @@ -0,0 +1,134 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Custom CSS */ + +body { + font-family: Lato,'Helvetica Neue',Helvetica,Arial,sans-serif; +} + +.navbar-custom { + background: #2C3E50; + text-transform: uppercase; + font-weight: 700; + border: none; + padding: 20px 0; +} + +.navbar-custom .navbar-brand { + color: #fff; + font-size: 1.5em; +} + +.navbar-custom .navbar-brand:hover { + color: gray; +} + +ul.cards { + margin-top: 20px; +} + +li.card-inline { + display: inline-block; + width: 300px; + height: 326px; + margin: 5px; + text-align: center; +} + +img.card-image { + width: 150px; + padding-top: 20px; +} + +.user-details { + margin-bottom: 40px; +} + +.hero { + text-align: center; + background: #18BC9C; + padding: 20px; + color: #fff; +} + +img.employee-image { + width: 350px; + margin: 10px 0 20px; +} + +td.title { + text-align: right; + font-weight: 700; +} + +.callout { + padding: 20px; + margin: 20px auto; + border: 1px solid #eee; + border-left-width: 5px; + border-radius: 3px; + width: 1000px; + border-left-color: #ce4844; +} + +.callout h4 { + color: #ce4844; + margin-top: 0; + margin-bottom: 5px; +} + +.details-table, .reports { + width: 1000px; + margin: 0 auto; +} + +.reports { + margin-top: 30px; +} + +img.report-img { + width: 100px; + padding-right: 10px; +} + +.report-desc { + width: 230px; +} + +.report-desc p { + margin: 0; + color: gray +} + +.direct-report { + display: inline-block; +} + +.direct-report .card { + width: 352px; + margin: 5px; + padding: 10px; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.loader { + color: gray; + font-size: 11px; + margin-top: 30px; +} diff --git a/isomorphic-react-app/src/components/Home.jsx b/isomorphic-react-app/src/components/Home.jsx new file mode 100644 index 0000000..5446771 --- /dev/null +++ b/isomorphic-react-app/src/components/Home.jsx @@ -0,0 +1,50 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Link } from 'react-router-dom'; +import _ from 'lodash'; + +export default class Home extends React.Component { + componentDidMount() { + // if no data is present, load the employees data + if(_.isEmpty(this.props.employees)) { + this.props.getAllEmployees(); + } + } + + render() { + if(_.isEmpty(this.props.employees)) { + return
Loading...
; + } + return ( +
+ +
+ ); + } +} diff --git a/isomorphic-react-app/src/components/User.jsx b/isomorphic-react-app/src/components/User.jsx new file mode 100644 index 0000000..97945ff --- /dev/null +++ b/isomorphic-react-app/src/components/User.jsx @@ -0,0 +1,118 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Link } from 'react-router-dom'; +import _ from 'lodash'; + +export default class User extends React.Component { + componentDidMount() { + // if no data is present, load the employee data from the database + if(_.isEmpty(this.props.currentEmployee) || + this.props.currentEmployee.employee.id !== this.props.match.params.id) { + this.props.getEmployeeById(this.props.match.params.id); + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.match.params.id !== this.props.match.params.id) { + // user has navigated to a new employee details page + // load data for that employee and set to state + this.props.getEmployeeById(nextProps.match.params.id); + } + } + + // render function for direct reports + renderReports() { + const { reports, employee } = this.props.currentEmployee; + if (!reports || _.isEmpty(reports)) { + // Employee has no direct reports + return null; + } + return ( +
+

Direct Reports

+ { reports.map((report) => { + return ( +
+
+ +
+

{report.name}

+

{report.position}

+ View {report.id}'s profile +
+
+
+ ); + }) } +
+ ); + } + + render() { + if(_.isEmpty(this.props.currentEmployee)) { + return
Loading...
; + } + const { employee } = this.props.currentEmployee; + return ( +
+
+
+ +

{employee.name}

+

{employee.position}

+
+
+
+

Fake Data

+

All data displayed below are fake data and displayed for informational purposes only.

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Location: {employee.location}
Job Level: {employee.level}
Email: {employee.email}
Phone Number: {employee.number}
Twitter Handle: {employee.twitter}
Birthday: {employee.birthday}
+ { this.renderReports() } +
+
+ ) + } +} diff --git a/isomorphic-react-app/src/containers/App.jsx b/isomorphic-react-app/src/containers/App.jsx new file mode 100644 index 0000000..4cb4e76 --- /dev/null +++ b/isomorphic-react-app/src/containers/App.jsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Switch, Route } from 'react-router'; +import { Link } from 'react-router-dom'; +import Home from '../components/Home'; +import User from '../components/User'; +import database from 'firebase-database'; + +export default class App extends React.Component { + constructor(props) { + super(props); + // check to see if we have existing server-rendered data + // sets the state if we do, otherwise initialize it to an empty state + if (props.state) { + this.state = props.state; + } else { + this.state = { + employees: {}, + currentEmployee: {} + } + } + } + + // Loads an employee by its id + getEmployeeById = (employeeId) => { + this.setState({ + currentEmployee: {} + }); + database.getEmployeeById(employeeId).then(({ currentEmployee }) => { + this.setState({ + currentEmployee + }); + }); + } + + // Loads all employees + getAllEmployees = () => { + database.getAllEmployees().then(({employees}) => { + this.setState({ + employees + }); + }); + } + + render() { + return ( +
+ + + ( + + )}/> + ( + + )}/> + +
+ ) + } +} diff --git a/isomorphic-react-app/src/containers/ClientApp.js b/isomorphic-react-app/src/containers/ClientApp.js new file mode 100644 index 0000000..520de0d --- /dev/null +++ b/isomorphic-react-app/src/containers/ClientApp.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; + +ReactDOM.render(( + + + + ), document.getElementById('root') +); diff --git a/isomorphic-react-app/src/containers/ServerApp.jsx b/isomorphic-react-app/src/containers/ServerApp.jsx new file mode 100644 index 0000000..660efff --- /dev/null +++ b/isomorphic-react-app/src/containers/ServerApp.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { StaticRouter } from 'react-router'; +import App from './App'; +import database from 'firebase-database'; + +export default class ServerApp extends React.Component { + constructor(props) { + super(props); + database.initializeApp(props.appConfig); + } + + render() { + return ( + + + + ); + } +} diff --git a/isomorphic-react-app/src/package.json b/isomorphic-react-app/src/package.json new file mode 100644 index 0000000..798451f --- /dev/null +++ b/isomorphic-react-app/src/package.json @@ -0,0 +1,30 @@ +{ + "name": "employee-directory-app", + "description": "Sample employee directory app built with React and Firebase", + "scripts": { + "buildClient": "./node_modules/.bin/webpack --config ./webpack.client.config.js", + "buildServer": "./node_modules/.bin/webpack --config ./webpack.server.config.js", + "build": "npm run buildClient && npm run buildServer" + }, + "dependencies": { + "firebase": "3.9.0", + "lodash": "4.17.4", + "react": "15.5.4", + "react-dom": "15.5.4", + "react-router": "4.1.1", + "react-router-dom": "4.1.1" + }, + "devDependencies": { + "babel-core": "6.24.1", + "babel-loader": "7.0.0", + "babel-preset-react": "6.24.1", + "babel-preset-stage-0": "6.24.1", + "webpack": "2.5.0" + }, + "babel": { + "presets": [ + "react", + "stage-0" + ] + } +} diff --git a/isomorphic-react-app/src/webpack.client.config.js b/isomorphic-react-app/src/webpack.client.config.js new file mode 100644 index 0000000..8edb220 --- /dev/null +++ b/isomorphic-react-app/src/webpack.client.config.js @@ -0,0 +1,27 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Sample Webpack Configuration for Client Bundle +const baseConfig = require('./webpack.config'); +const path = require('path'); + +module.exports = Object.assign({}, { + entry: './containers/ClientApp.js', + output: { + filename: 'client.bundle.js', + path: path.resolve(__dirname, '../public/assets') + } +}, baseConfig); diff --git a/isomorphic-react-app/src/webpack.config.js b/isomorphic-react-app/src/webpack.config.js new file mode 100644 index 0000000..24d4969 --- /dev/null +++ b/isomorphic-react-app/src/webpack.config.js @@ -0,0 +1,37 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Sample Webpack Configuration +const path = require('path'); + +module.exports = { + resolve: { + extensions: [".js", ".jsx"], + alias: { + 'firebase-database': path.resolve(__dirname, '../functions/firebase-database'), + }, + }, + resolveLoader: { + modules: [path.resolve(__dirname, "./node_modules")], + }, + module: { + rules: [{ + test: /\.(js|jsx)$/, + exclude: /node_modules/, + loader: 'babel-loader' + }] + } +}; diff --git a/isomorphic-react-app/src/webpack.server.config.js b/isomorphic-react-app/src/webpack.server.config.js new file mode 100644 index 0000000..3bcae27 --- /dev/null +++ b/isomorphic-react-app/src/webpack.server.config.js @@ -0,0 +1,32 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Sample Webpack Configuration for Server Bundle +const baseConfig = require('./webpack.config'); +const path = require('path'); + +// Note that since this is for the server, it is important to +// set the target to node and set the libraryTarget to commonjs2 +module.exports = Object.assign({}, { + target: 'node', + entry: './containers/ServerApp.jsx', + output: { + filename: 'server.bundle.js', + path: path.resolve(__dirname, '../functions/build'), + libraryTarget: 'commonjs2', + } +}, baseConfig); +