From 73fd178173b328db432b8940a1a2296ac9792d28 Mon Sep 17 00:00:00 2001 From: Douglas Wade Date: Thu, 9 Jun 2016 20:38:10 -0700 Subject: [PATCH] Add bike share example (#222) --- .../react-server-examples/bike-share/.babelrc | 3 + .../bike-share/.gitignore | 3 + .../bike-share/Dockerfile | 5 ++ .../bike-share/README.md | 82 +++++++++++++++++++ .../bike-share/api/network.js | 32 ++++++++ .../bike-share/components/footer.js | 14 ++++ .../bike-share/components/header.js | 9 ++ .../bike-share/components/network-card.js | 29 +++++++ .../bike-share/components/network-list.js | 21 +++++ .../bike-share/components/station-card.js | 35 ++++++++ .../bike-share/components/station-list.js | 19 +++++ .../bike-share/docker-compose.yml | 22 +++++ .../bike-share/gulpfile.js | 12 +++ .../bike-share/middleware/request-to-port.js | 11 +++ .../bike-share/package.json | 47 +++++++++++ .../bike-share/pages/index.js | 45 ++++++++++ .../bike-share/pages/network.js | 61 ++++++++++++++ .../bike-share/routes.js | 22 +++++ .../bike-share/styles/base.scss | 26 ++++++ .../bike-share/styles/index.scss | 1 + .../bike-share/styles/network.scss | 1 + .../bike-share/styles/reset.scss | 34 ++++++++ 22 files changed, 534 insertions(+) create mode 100644 packages/react-server-examples/bike-share/.babelrc create mode 100644 packages/react-server-examples/bike-share/.gitignore create mode 100644 packages/react-server-examples/bike-share/Dockerfile create mode 100644 packages/react-server-examples/bike-share/README.md create mode 100644 packages/react-server-examples/bike-share/api/network.js create mode 100644 packages/react-server-examples/bike-share/components/footer.js create mode 100644 packages/react-server-examples/bike-share/components/header.js create mode 100644 packages/react-server-examples/bike-share/components/network-card.js create mode 100644 packages/react-server-examples/bike-share/components/network-list.js create mode 100644 packages/react-server-examples/bike-share/components/station-card.js create mode 100644 packages/react-server-examples/bike-share/components/station-list.js create mode 100644 packages/react-server-examples/bike-share/docker-compose.yml create mode 100644 packages/react-server-examples/bike-share/gulpfile.js create mode 100644 packages/react-server-examples/bike-share/middleware/request-to-port.js create mode 100644 packages/react-server-examples/bike-share/package.json create mode 100644 packages/react-server-examples/bike-share/pages/index.js create mode 100644 packages/react-server-examples/bike-share/pages/network.js create mode 100644 packages/react-server-examples/bike-share/routes.js create mode 100644 packages/react-server-examples/bike-share/styles/base.scss create mode 100644 packages/react-server-examples/bike-share/styles/index.scss create mode 100644 packages/react-server-examples/bike-share/styles/network.scss create mode 100644 packages/react-server-examples/bike-share/styles/reset.scss diff --git a/packages/react-server-examples/bike-share/.babelrc b/packages/react-server-examples/bike-share/.babelrc new file mode 100644 index 000000000..06ffde968 --- /dev/null +++ b/packages/react-server-examples/bike-share/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["react-server"] +} diff --git a/packages/react-server-examples/bike-share/.gitignore b/packages/react-server-examples/bike-share/.gitignore new file mode 100644 index 000000000..c2dc4f6ec --- /dev/null +++ b/packages/react-server-examples/bike-share/.gitignore @@ -0,0 +1,3 @@ +node_modules +build +__clientTemp diff --git a/packages/react-server-examples/bike-share/Dockerfile b/packages/react-server-examples/bike-share/Dockerfile new file mode 100644 index 000000000..dc7eabba1 --- /dev/null +++ b/packages/react-server-examples/bike-share/Dockerfile @@ -0,0 +1,5 @@ +FROM node:slim + +EXPOSE 3000 +ENV NODE_ENV=docker-dev +VOLUME /www \ No newline at end of file diff --git a/packages/react-server-examples/bike-share/README.md b/packages/react-server-examples/bike-share/README.md new file mode 100644 index 000000000..fd81a3d6d --- /dev/null +++ b/packages/react-server-examples/bike-share/README.md @@ -0,0 +1,82 @@ +# react-server-examples/bike-share + +An example project for `react-server` which demos server rendering, interactivity +on the client side, frameback, ReactServerAgent, url parameters and logging. +Uses [api.citybik.es](http://api.citybik.es/v2/) to get data about bike shares +and their availability around the world. + +To start in development mode: + +```shell +npm start +``` + +Then go to [localhost:3000](http://localhost:3000/). You will see an index page +that shows the covered bike share networks around the world. Each bike share +network is a link to a details page that, when clicked, loads an iframe in front +of the index page containing a network page for that bike share network, +including information about each station in that network, and the number of +available bikes the last time that data is available for. + +If you want to optimize the client code at the expense of startup time, type +`NODE_ENV=production npm start`. You can also use +[any react-server-cli arguments](../../react-server-cli#setting-options-manually) +after `--`. For example: + +```shell +# start in dev mode on port 4000 +npm start -- --port=4000 +``` + +# Developing using Docker and Docker Compose + +These steps assume you are familiar with docker and already have it installed. +Some basics: + +1. Download [Docker Toolbox](https://www.docker.com/products/docker-toolbox) and + install it. +2. Start `docker quick start shell` +3. Navigate to where you generated the project +4. Add a configuration to set the `host` option to the ip given by + `docker-machine ip`. An example configuration might be like: +```json +{ + "port": "3000", + "env": { + "docker": { + "host": "Your ip from `docker-machine ip` here" + }, + "staging": { + "port": "3000" + }, + "production": { + "port": "80" + } + } +} +``` +5. Now that your system is ready to go, start the containers: +```shell +docker-compose build --pull +docker-compose up +``` + +The containers will now be running. At any time, press ctrl+c to stop them. + +To clean up, run the following commands: + +```shell +docker-compose stop +docker-compose rm --all +docker volume ls # and get the name of the volume ending in react_server_node_modules +# this name will be different depending on the name of the project +docker volume rm _react_server_node_modules +``` + +The configuration included stores the node_modules directory in a "named volume". +This is a special persistent data-store that Docker uses to keep around the +node_modules directory so that they don't have to be built on each run of the +container. If you need to get into the container in order to investigate what +is in the volume, you can run `docker-compose exec react_server bash` which will +open a shell in the container. Be aware that the exec functionality doesn't +exist in Windows (as of this writing). diff --git a/packages/react-server-examples/bike-share/api/network.js b/packages/react-server-examples/bike-share/api/network.js new file mode 100644 index 000000000..51c05e327 --- /dev/null +++ b/packages/react-server-examples/bike-share/api/network.js @@ -0,0 +1,32 @@ +import {ReactServerAgent, logging} from 'react-server'; + +const logger = logging.getLogger(__LOGGER__); + +export default class NetworkApi { + setConfigValues() { + return {isRawResponse: true}; + } + + handleRoute(next) { + this.network = this.getRequest().getQuery().network; + logger.info(`got network api request${this.network ? ' for network ' + this.network : ''}`); + return next(); + } + + getContentType() { + return 'application/json'; + } + + getResponseData() { + let url = 'http://api.citybik.es/v2/networks'; + if (this.network) { + url += `/${this.network}`; + } + return new Promise(resolve => { + ReactServerAgent.get(url).then(data => { + logger.info(`got data ${JSON.stringify(data)} from url ${url}`); + resolve(JSON.stringify(data)); + }); + }); + } +} diff --git a/packages/react-server-examples/bike-share/components/footer.js b/packages/react-server-examples/bike-share/components/footer.js new file mode 100644 index 000000000..053f9ccac --- /dev/null +++ b/packages/react-server-examples/bike-share/components/footer.js @@ -0,0 +1,14 @@ +import React from 'react'; +import {logging} from 'react-server'; + +const logger = logging.getLogger(__LOGGER__); + +export default () => { + logger.info('rendering the footer'); + return (
+ Brought to you by + React Server + and + citybik.es +
); +}; diff --git a/packages/react-server-examples/bike-share/components/header.js b/packages/react-server-examples/bike-share/components/header.js new file mode 100644 index 000000000..35e12baf4 --- /dev/null +++ b/packages/react-server-examples/bike-share/components/header.js @@ -0,0 +1,9 @@ +import React from 'react'; +import {logging} from 'react-server'; + +const logger = logging.getLogger(__LOGGER__); + +export default () => { + logger.info('rendering the header'); + return (

React Server city bikes page

); +}; diff --git a/packages/react-server-examples/bike-share/components/network-card.js b/packages/react-server-examples/bike-share/components/network-card.js new file mode 100644 index 000000000..0af4b66bf --- /dev/null +++ b/packages/react-server-examples/bike-share/components/network-card.js @@ -0,0 +1,29 @@ +import React from 'react'; +import {logging, Link} from 'react-server'; + +const logger = logging.getLogger(__LOGGER__); + +const NetworkCard = ({id, name, location, company}) => { + logger.info(`rendering card for network ${name}`); + return ( +
{name} in {location.city}, {location.country}, run by {company}
+ ); +}; + +NetworkCard.propTypes = { + company: React.PropTypes.string, + href: React.PropTypes.string, + id: React.PropTypes.string, + location: React.PropTypes.shape({ + city: React.PropTypes.string, + country: React.PropTypes.string, + latitude: React.PropTypes.number, + longitude: React.PropTypes.number + }), + name: React.PropTypes.string, + stations: React.PropTypes.array +}; + +NetworkCard.displayName = 'NetworkCard'; + +export default NetworkCard; diff --git a/packages/react-server-examples/bike-share/components/network-list.js b/packages/react-server-examples/bike-share/components/network-list.js new file mode 100644 index 000000000..a6f884713 --- /dev/null +++ b/packages/react-server-examples/bike-share/components/network-list.js @@ -0,0 +1,21 @@ +import React from 'react'; +import {logging} from 'react-server'; +import NetworkCard from './network-card'; + +const logger = logging.getLogger(__LOGGER__); + +const NetworkList = ({networks}) => { + logger.info(`rendering list of ${networks.length} networks`); + const networkCards = networks.map(network => { + return ; + }); + return
{networkCards}
; +}; + +NetworkList.propTypes = { + networks: React.PropTypes.array +}; + +NetworkList.displayName = 'NetworkList'; + +export default NetworkList; diff --git a/packages/react-server-examples/bike-share/components/station-card.js b/packages/react-server-examples/bike-share/components/station-card.js new file mode 100644 index 000000000..257998919 --- /dev/null +++ b/packages/react-server-examples/bike-share/components/station-card.js @@ -0,0 +1,35 @@ +import React from 'react'; +import {logging} from 'react-server'; + +const logger = logging.getLogger(__LOGGER__); +const timeSinceTimestamp = s => { + const parsed = Date.parse(s); + const timeSince = (new Date()) - parsed; + const minutesSince = Math.floor(timeSince / 60000); + const secondsSince = Math.floor((timeSince / 1000) % 60); + return `${minutesSince} min, ${secondsSince} sec`; +}; + +const StationCard = ({station}) => { + logger.info(`rendering card for station ${JSON.stringify(station)}`); + return ( +
{station.name} had {station.empty_slots} empty slots {timeSinceTimestamp(station.timestamp)} ago.
+ ); +}; + +StationCard.propTypes = { + station: React.PropTypes.shape({ + empty_slots: React.PropTypes.number, // eslint-disable-line + extra: React.PropTypes.object, + free_bikes: React.PropTypes.number, // eslint-disable-line + id: React.PropTypes.string, + latitude: React.PropTypes.number, + longitude: React.PropTypes.number, + name: React.PropTypes.string, + timestamp: React.PropTypes.string + }) +}; + +StationCard.displayName = 'StationCard'; + +export default StationCard; diff --git a/packages/react-server-examples/bike-share/components/station-list.js b/packages/react-server-examples/bike-share/components/station-list.js new file mode 100644 index 000000000..b2ddddf51 --- /dev/null +++ b/packages/react-server-examples/bike-share/components/station-list.js @@ -0,0 +1,19 @@ +import React from 'react'; +import {logging} from 'react-server'; +import StationCard from './station-card'; + +const logger = logging.getLogger(__LOGGER__); + +const StationList = ({stations}) => { + logger.info(`rendering list of ${stations.length} stations`); + const stationCards = stations.map(station => ); + return
{stationCards}
; +}; + +StationList.propTypes = { + stations: React.PropTypes.array +}; + +StationList.displayName = 'StationList'; + +export default StationList; diff --git a/packages/react-server-examples/bike-share/docker-compose.yml b/packages/react-server-examples/bike-share/docker-compose.yml new file mode 100644 index 000000000..0464d7898 --- /dev/null +++ b/packages/react-server-examples/bike-share/docker-compose.yml @@ -0,0 +1,22 @@ +version: '2' +services: + react_server: # The label of the service + build: . # The location of the Dockerfile to build + volumes: # Files to share with the container and the host + - .:/www # Share the project in /www in the container + - react_server_node_modules:/www/node_modules # A special volume so that OS specific node_modules are built correctly + working_dir: /www # The location all commands should run from + environment: + NODE_ENV: 'docker' # Set the NODE_ENV environment variable + command: /bin/bash -c "npm install && npm start" # The command to run in when the container starts + ports: # Ports to expose to your host + - '3000:3000' + - '3001:3001' + # An example database + # my_db: + # image: rethinkdb:latest + # You can reference the db/service by using the dns name `my_db` and docker will do the rest + +# Volume to store separate from the container runtime and host +volumes: + react_server_node_modules: \ No newline at end of file diff --git a/packages/react-server-examples/bike-share/gulpfile.js b/packages/react-server-examples/bike-share/gulpfile.js new file mode 100644 index 000000000..731c61cfa --- /dev/null +++ b/packages/react-server-examples/bike-share/gulpfile.js @@ -0,0 +1,12 @@ +const gulp = require('gulp'); +const tagger = require('react-server-gulp-module-tagger'); +const babel = require('gulp-babel'); + +gulp.task('default', () => { + gulp.src(['api/*.js', 'components/*.js', 'middleware/*.js', 'pages/*.js', 'routes.js'], {base: '.'}) + .pipe(tagger()) + .pipe(babel({ + presets: ['react-server'] + })) + .pipe(gulp.dest('build/')); +}); diff --git a/packages/react-server-examples/bike-share/middleware/request-to-port.js b/packages/react-server-examples/bike-share/middleware/request-to-port.js new file mode 100644 index 000000000..b91751d02 --- /dev/null +++ b/packages/react-server-examples/bike-share/middleware/request-to-port.js @@ -0,0 +1,11 @@ +import {ReactServerAgent} from 'react-server'; +export default class RequestToPortMiddleware { + handleRoute(next) { + if (typeof window === 'undefined') { //eslint-disable-line + ReactServerAgent.plugRequest(req => { + req.urlPrefix('localhost:3000'); + }); + } + return next(); + } +} diff --git a/packages/react-server-examples/bike-share/package.json b/packages/react-server-examples/bike-share/package.json new file mode 100644 index 000000000..4738dec16 --- /dev/null +++ b/packages/react-server-examples/bike-share/package.json @@ -0,0 +1,47 @@ +{ + "name": "react-server-bike-share", + "version": "0.0.1", + "private": true, + "description": "A react-server instance", + "main": "HelloWorld.js", + "author": "Doug Wade ", + "scripts": { + "clean": "rm -rf build __clientTemp", + "start": "npm run clean && gulp && npm run styles && react-server-cli --routesFile build/routes.js", + "styles": "node-sass styles/index.scss build/styles/index.css && node-sass styles/network.scss build/styles/network.css", + "test": "xo && nsp check" + }, + "license": "Apache-2.0", + "dependencies": { + "babel-plugin-transform-runtime": "^6.8.0", + "babel-preset-es2015": "^6.6.0", + "babel-preset-react": "^6.5.0", + "babel-runtime": "^6.6.1", + "react": "~0.14.2", + "react-dom": "~0.14.2", + "react-server": "^0.3.1", + "react-server-cli": "^0.3.1", + "superagent": "1.2.0" + }, + "devDependencies": { + "babel-preset-react-server": "^0.3.0", + "eslint-config-xo-react": "^0.7.0", + "eslint-plugin-react": "^5.1.1", + "gulp": "^3.9.1", + "gulp-babel": "^6.1.2", + "node-sass": "^3.7.0", + "nsp": "^2.3.3", + "react-server-gulp-module-tagger": "^0.3.0", + "xo": "^0.15.1" + }, + "xo": { + "esnext": true, + "extends": "xo-react", + "globals": [ + "__LOGGER__" + ], + "ignores": [ + "__clientTemp/**/*" + ] + } +} diff --git a/packages/react-server-examples/bike-share/pages/index.js b/packages/react-server-examples/bike-share/pages/index.js new file mode 100644 index 000000000..cfd4f1360 --- /dev/null +++ b/packages/react-server-examples/bike-share/pages/index.js @@ -0,0 +1,45 @@ +import React from 'react'; +import {ReactServerAgent, RootElement, TheFold} from 'react-server'; +import NetworkList from '../components/network-list'; +import Header from '../components/header'; +import Footer from '../components/footer'; +import '../styles/index.css'; + +export default class IndexPage { + handleRoute(next) { + this.data = ReactServerAgent.get('/api/networks').then(d => d.body.body); + return next(); + } + + getTitle() { + return 'React Server Bike Share'; + } + + getElements() { + return [ + +
+ , + + + , + , + +