Giter Club home page Giter Club logo

isomorphic500's Introduction

isomorphic500

Isomorphic500 is a small isomorphic (universal) web application featuring photos from 500px.

It is built on express using React and Flux with yahoo/fluxible. It is developed with webpack and react-hot-loader and written with babeljs with the help of eslint. It supports multiple languages using react-intl.

Build Status

The intent of this project is to solidify my experience with these technologies and perhaps to inspire other developers in their journey with React and Flux. It works also as example of a javascript development environment with all the cool recent stuff :-)

  • see the demo on isomorphic500.herokuapp.com (with source maps!)
  • clone this repo and run the server to confirm it is actually working
  • edit a react component or a css style, and see the updated app as you save your changes!
  • read on for some technical details

Get help Join the gitter chat or the #isomorphic500 on reactiflux :-)

Clone this repo

Note This app has been tested on node 4

git clone https://github.com/gpbl/isomorphic500.git
cd isomorphic500
npm install

Start the app

npm run dev

and open localhost:3000.

You can also try the built app:

npm run build   # First, build for production
npm run prod    # then, run the production version

then open localhost:8080.

If you are starting the server on Windows, please read #58

Table of Contents

Application structure

.
├── index.js            # Starts the express server and the webpack dev server
├── config              # Contains the configuration for dev and prod environments
├── nodemon.json        # Configure nodemon to watch some files
├── src
│   ├── app.js          # The fluxible app
│   ├── client.js       # Entry point for the client
│   ├── config.js       # Config loader (load the config files from /config)
│   ├── routes.js       # Routes used by fluxible-router
│   ├── server.js       # Start the express server and render the routes server-side
│   │
│   ├── actions         # Fluxible actions
│   ├── components      # React components
│   ├── constants       # Constants
│   ├── containers      # Contains React containers components
│   │   ├── ...
│   │   ├── Html.js     # Used to render the <html> document server-side
│   │   └── Root.js     # Root component

│   ├── intl            # Contains the messages for i18n
│   ├── server          # Server-side only code
│   │   ├── ga.js              # Google Analytics script
│   │   ├── intl-polyfill.js   # Patch node to support `Intl` and locale-data
│   │   ├── render.js          # Middleware to render server-side the fluxible app
│   │   └── setLocale.js       # Middleware to detect and set the request's locale
│   ├── services        # Fetchr services
│   ├── stores          # Fluxible stores
│   ├── style           # Contains the Sass files
│   └── utils         
│       ├── APIUtils.js            # Wrapper to superagent for communicating with 500px API
│       ├── CookieUtils.js         # Utility to write/read cookies 
│       ├── IntlComponents.js      # Exports wrapped react-intl components
│       ├── IntlUtils.js           # Utilities to load `Intl` and locale-data
│       ├── connectToIntlStore.js  # Connects react-intl components with the IntlStore
│       ├── getIntlMessage.js      # Get react-intl messages
│       └── trackPageView.js       # Track a page view with google analitics
├── static              
│   ├── assets         # Static files
│   └── dist           # Output files for webpack on production
└── webpack
    ├── dev.config.js  # Webpack config for development
    ├── prod.config.js # Webpack config for building the production files
    └── server.js      # Used to starts the webpack dev server

The fluxible app

The src/app file is the core of the Fluxible application:

  • it configures Fluxible with src/containers/Root.js as the root component.
  • it registers the stores so they can work on the same React context
  • it adds the fetchr plugin, to share the same API requests both client and server-side
  • it makes possible to dehydrate the stores on the server and rehydrate them on the client

Async data

I used Fetchr and fluxible-plugin-fetchr. Fetchr services run only on server and send superagent requests to 500px.

Router

This app uses fluxible-router for routing. Fluxible-router works pretty well in fluxible applications since it follows the flux paradigm. The Application component uses the @handleHistory decorator to bind the router to the app.

Stores

Instead of directly listening to stores, components use fluxible's @connectToStores decorator: a store state is passed to components as prop. See for example the PhotoPage or the FeaturedPage.

connectToStore can also "consume" store data without actually listening to any store. This is the case of NavBar or LocaleSwitcher.

Resource stores

While REST APIs usually return collections as arrays, a resource store keeps items as big object – like the PhotoStore. This simplifies the progressive resource updates that may happen during the app’s life.

List stores

A list store keeps references to a resource store, as the FeaturedStore holds the ids of the photos in PhotoStore.

The HtmlHeadStore

The HtmlHeadStore is a special store used to set the <head> meta-tags in the Html component, during server-side rendering. It is also listened by the Application component to change the browser's document.title.

This store listens to route actions and set its content according to the current route. It also get data from other stores (e.g. the photo's title from the PhotoStore), or the localized messages from the IntlStore.

Internationalization (i18n)

To give an example on how to implement i18n in a React application, isomorphic500 supports English, Italian, Portuguese and French.

This app adopts React Intl, which is a solid library for this purpose.

How the user’s locale is detected

The app sniffs the browser's accept-language request header. The locale npm module has a nice express middleware for that. Locales are restricted to those set in the app's config.

The user may want to override the detected locale: the LocaleSwitcher component set a cookie when the user chooses a language. Also, we enable the ?hl parameter in the query string to override it. Server-side, cookie and query string are detected by the setLocale middleware.

Setting up react-intl

React-intl requires some boilerplate to work properly. Difficulties here arise mainly for two reasons:

  1. React Intl relies on the Intl global API, not always available on node.js or some browsers (e.g. Safari). Luckly there's an Intl polyfill: on the server we can just "require" it – however on the browser we want to download it only when Intl is not supported.

  2. For each language, we need to load a set of locale data (used by Intl to format numbers and dates) and the translated strings, called messages (used by react-intl). While on node.js we can load them in memory, on the client they need to be downloaded first – and we want to download only the relevant data for the current locale.

On the server the solution is easy: as said, the server loads a polyfill including both Intl and the locale data. For supporting the browser, we can instead rely on our technology stack, i.e. flux and webpack.

On the client, we have to load the Intl polyfill and its locale data before rendering the app, i.e. in client.js.

For this purpose, I used webpack's require.ensure() to split Intl and localized data in multiple chunks. Only after they have been downloaded, the app can be mounted. See the loadIntlPolyfill() and loadLocaleData() functions in IntlUtils: they return a promise that is resolved when the webpack chunks are downloaded and required.

They are used in client.js before mounting the app.

Important: since react-intl assumes Intl is already in the global scope, we can't import the fluxible app (which imports react-intl in some of its components) before polyfilling Intl. That's why you see in client.js require("./app") inside the in the renderApp() function, and not as import on the top of the file.

Internationalization, the flux way

Lets talk about the data that react-intl needs to deliver translated content. Translated messages are saved in the intl directory and shared between client and server using the IntlStore.

This store listens to a LOAD_INTL_SERVER action dispatched by IntlActionCreator. We execute this action only server side before rendering the Html component together with the usual navigateAction. This allows to dehydrate/rehydrate the store content.

React-intl components need to have access to the IntlStore. Plus, since I'm using ES6 classes, I can't adopt the react-intl Mixin in my components. To solve this, I wrap the Formatted* components and make them available from IntlComponents.

Sending the locale to the API

While this is not required by the 500px API, we can send the current locale to the API so it can deliver localized content. This is made very easy by the Fetchr services, since they expose the req object: see for example the photo service.

Development

Run the development version with

npm run dev

nodemon

This task runs the server with nodemon. Nodemon will restart the server when some of the files specified in its config change.

Webpack

Webpack is used as commonjs module bundler, css builder (using sass-loader) and assets loader (images and svg files).

The development config enables source maps, the Hot Module Replacement and react-hot-loader. It loads CSS styles with <style>, to enable styles live reload). This config is used by the webpack-dev-server, serving the files bundled by Webpack.

This config uses the webpack-error-notification plugin. To get notified on errors while compiling the code, on Mac you must brew install terminal-notifier.

The production config builds the client-side production bundle from npm run build.

Both configs set a process.env.BROWSER global variable, useful to require CSS from the components, e.g:

// MyComponent
if (process.env.BROWSER) {
  require('../style/MyComponent.scss');
}

On production, files bundled by webpack are hashed. Javascript and CSS file names are saved in a static/dists/stats.json which is read by the Html component.

Babeljs

This app is written in Javascript-Babel. Babel config is in .babelrc (it only enables class properties). On Sublime Text, I installed babel-sublime to have full support of the Babel syntax!

.editorconfig

The .editorconfig file can be used with your IDE/editor to mantain a consistent coding style. See editorconfig.org for more info. (thanks to @lohek)

Linting

I use eslint with babel-eslint and the react plugin. I also configured Sublime Text with SublimeLinter-eslint.

I use the rules from my own eslint-config-gpbl shared configs.

npm run lint

I use SublimeLinter-scss-lint for linting the Sass files (.scss-lint.yml) (only with Sublime Text).

Debugging

The app uses debug to log debug messages. You can enable/disable the logging from Node by setting the DEBUG environment variable before running the server:

# enable logging for isomorphic500 and Fluxible
DEBUG=isomorphic500,Fluxible node index

# disable logging
DEBUG= node index

From the browser, you can enable/disable them by sending this command in the JavaScript console:

debug.enable('isomorphic500')
debug.disable()
// then, refresh!

isomorphic500's People

Contributors

al3x avatar gitter-badger avatar gpbl avatar juancabrera avatar julio-saito-linx avatar leplay avatar loeck avatar reymus avatar robink avatar ryardley avatar vsnig avatar wykks avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

isomorphic500's Issues

Errors on server while developing

It has been suggested here. Using webpack server-side we should be able to prevent the crash of the server: webpack should just report the error and call the developer's attention with a BELL character, printing the error stack in stdout.
Also, it should work nice with react-hot-reload on the server!

Split PhotoStore in FeaturedPhotoStore

The PhotoStore is both a "list store" and a "content store" (as defined here). Featured photo's ids should be stored in a FeaturedStore instead.

The FeaturedPage needs to receive props.photos filtered by the content of FeaturedStore, however this is not possibile with the current connectToStores implementation because stores can't see each others. Follow this issue

Improper exception handling?

I believe there is a problem in the current way exception are handled on the server rendering phase:

072f4c5#diff-0655f086119d60b6cf62f877f4128fa8R77

If the navigateAction fails somehow, you're still rendering the app in an inconsistant state.

Eg:

  1. start rendering app
  2. NAVIGATE_START is dispatched
  3. Page is currently rendering, then crash some reason, for example because one translation is missing
  4. An exception is thrown, catched here.
  5. App renders with a loader (because we are still in NAVIGATE_START phase) and nothing will ever happens

Isomorphic flux.

@cjsx React.DOM

'use strict'

_ = require('lodash')
React = require('react')
JobStore = require('../stores/JobStore')
Router = require('react-router')

findJob = (id)->
console.log 'hi'
i=0
jobs=JobStore.getAll()
while i < jobs.length
if jobs[i].id is id
return jobs[i]
i++

Job= React.createClass
mixins: [ Router.State ]
render: ->
job = findJob(@getparams().id)
console.log job
return(

{job.name}

)

module.exports = Job

showing error "Uncaught ReferenceError: job is not defined"

Live demo on Heroku

👍 great stuff!

I'd love to browse a live demo without installing the app.

Warning in browser console.

Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
(client) xgk2kmbk.2.0.1.0">isasdadhic500
(server) xgk2kmbk.2.0.1.0">isomorphic500

Any idea what this warning is about? Am I the only to get this warning?

Cordially,

Hervé

Can't require images with webpack on the backend

I've been using your template (which is great, btw) for my own project, and have run into some issues with the images. So, I am trying to include image from the meta, and I want it to be served by webpack. To make this happen, I am doing something like this in HtmlHeadStore:

  setInitialState() {
    this.title = this.formatMessage("meta.title");
    this.description = this.formatMessage("meta.description");
    this.images = [];

    this.images.push(`${BASE_URL}${require('../assets/images/fb.jpg')}`);
  }

Unfortunately, I am getting this error:

{ [SyntaxError: /Users/opportunato/projects/grabr-web-client/src/assets/images/fb.jpg: Unexpected character '�' (1:0)]
  pos: 0,
  loc: { line: 1, column: 0 },
  raisedAt: 0,
  _babel: true,
  codeFrame: '> 1 | ����\u001b[7m\u0000\u001b[27m\u001b[7m\u0018\u001b[27mExif\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27mII\u001b[1m*\u001b[22m\u001b[7m\u0000\u001b[27m\u001b[7m\b\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m��\u001b[7m\u0000\u001b[27m\u001b[7m\u0011\u001b[27mDucky\u001b[7m\u0000\u001b[27m\u001b[7m\u0001\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0004\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27mP\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m��\u001b[7m\u0003\u001b[27m\u001b[1m-\u001b[22mhttp\u001b[1m:\u001b[22m\u001b[90m//ns.adobe.com/xap/1.0/\u0000<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27        "> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#" xmp:CreatorTool="Adobe Photoshop CS6 (Macintosh)" xmpMM:InstanceID="xmp.iid:2EB4C76E07D811E588849AAD864FAADD" xmpMM:DocumentID="xmp.did:2EB4C76F07D811E588849AAD864FAADD"> <xmpMM:DerivedFrom stRef:instanceID="xmp.iid:2EB4C76C07D811E588849AAD864FAADD" stRef:documentID="xmp.did:2EB4C76D07D811E588849AAD864FAADD"/> </rdf:Description> </rdf:RDF> </x:xmpmeta> <?xpacket end="r"?>��\u0000&Adobe\u0000d�\u0000\u0000\u0000\u0001\u0003\u0000\u0015\u0004\u0003\u0006\u001b[39m\n    | ^\n  2 | \n  3 | \u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27mZe\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m��\u001b[7m\u0000\u001b[27m\u001b[7m\u0000\u001b[27m��\u001b[7m\u0000\u001b[27m\u001b[7m\u0001\u001b[27m�\u001b[1m/\u001b[22m��\u001b[7m\u0000\u001b[27m�\u001b[7m\u0000\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0003\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0003\u001b[27m\u001b[7m\u0004\u001b[27m\u001b[7m\u0003\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0002\u001b[27m\u001b[7m\u0003\u001b[27m\u001b[7m\u0004\u001b[27m\u001b[7m\u0005\u001b[27m\u001b[7m\u0004\u001b[27m\u001b[7m\u0004\u001b[27m\u001b[7m\u0004\u001b[27m\u001b[7m\u0004\u001b[27m\u001b[7m\u0004\u001b[27m\u001b[7m\u0005\u001b[27m\u001b[7m\u0006\u001b[27m\u001b[7m\u0005\u001b[27m\u001b[7m\u0005\u001b[27m\u001b[7m\u0005\u001b[27m\u001b[7m\u0005\u001b[27m\u001b[7m\u0005\u001b[27m\u001b[7m\u0005\u001b[27m\u001b[7m\u0006\u001b[27m\u001b[7m\u0006\u001b[27m\u001b[7m\u0007\u001b[27m\u001b[7m\u0007\u001b[27m\u001b[7m\b\u001b[27m\u001b[7m\u0007\u001b[27m\u001b[7m\u0007\u001b[27m\u001b[7m\u0006\u001b[27m\t\t\n  4 | ' }
SyntaxError: /Users/opportunato/projects/grabr-web-client/src/assets/images/fb.jpg: Unexpected character '�' (1:0)
> 1 | ����ExifII��DuckyP��-http://ns.adobe.com/xap/1.0/<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27        "> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#" xmp:CreatorTool="Adobe Photoshop CS6 (Macintosh)" xmpMM:InstanceID="xmp.iid:2EB4C76E07D811E588849AAD864FAADD" xmpMM:DocumentID="xmp.did:2EB4C76F07D811E588849AAD864FAADD"> <xmpMM:DerivedFrom stRef:instanceID="xmp.iid:2EB4C76C07D811E588849AAD864FAADD" stRef:documentID="xmp.did:2EB4C76D07D811E588849AAD864FAADD"/> </rdf:Description> </rdf:RDF> </x:xmpmeta> <?xpacket end="r"?>��&Adobed�
    | ^
  2 | 
  3 | Ze�����/���           
  4 | 
    at Parser.pp.raise (/Users/opportunato/projects/grabr-web-client/node_modules/babel-core/lib/acorn/src/location.js:73:13)
    at Parser.pp.getTokenFromCode (/Users/opportunato/projects/grabr-web-client/node_modules/babel-core/lib/acorn/src/tokenize.js:423:8)
    at Parser.pp.readToken (/Users/opportunato/projects/grabr-web-client/node_modules/babel-core/lib/acorn/src/tokenize.js:106:15)
    at Parser.<anonymous> (/Users/opportunato/projects/grabr-web-client/node_modules/babel-core/node_modules/acorn-jsx/inject.js:650:22)
    at Parser.readToken (/Users/opportunato/projects/grabr-web-client/node_modules/babel-core/lib/acorn/plugins/flow.js:694:22)
    at Parser.pp.nextToken (/Users/opportunato/projects/grabr-web-client/node_modules/babel-core/lib/acorn/src/tokenize.js:98:71)
    at Object.parse (/Users/opportunato/projects/grabr-web-client/node_modules/babel-core/lib/acorn/src/index.js:105:5)
    at exports.default (/Users/opportunato/projects/grabr-web-client/node_modules/babel-core/lib/babel/helpers/parse.js:47:19)
    at File.parse (/Users/opportunato/projects/grabr-web-client/node_modules/babel-core/lib/babel/transformation/file/index.js:533:46)
    at File.addCode (/Users/opportunato/projects/grabr-web-client/node_modules/babel-core/lib/babel/transformation/file/index.js:615:24)

Here is my dev webpack loaders config:

module: {
    loaders: [
      { test: /\.(jpe?g|png|gif|svg)$/, loader: "file" },
      { test: /\.(woff|woff2|eot|ttf|svg)$/, loader: 'url-loader?limit=8192' },
      { test: /\.js$/, exclude: /node_modules/, loaders: ["react-hot", "babel?cacheDirectory"] },
      { test: /\.scss$/, loader: "style!css!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap=true&sourceMapContents=true" }
    ]
  },

I've found similar issue here, but I don't understand why babel could be the issue, since it should ignore files without .js extensions.

What could be my problem?

IntlUtils.loadLocaleData seems like it could be improved

I'm not a very big fan of this part: https://github.com/gpbl/isomorphic500/blob/master/src/utils/IntlUtils.js#L64-L107

When I add a locale to one of my webapps, I love to simply have to create a new locale file, like en.js, declare it as being available in my config file, like dev.js, and that's it.

What is being done in loadLocaleData seems to go against the DRY principle.

I've tried to refactor the switch using dynamic requires but webpack is now complaining about it. Any thoughts?

Use of webpack for CSS preprocessing

Hi,

I noticed the use of gulp & livereload for preprocessing styles and reloading on the client. Wouldn't it be better to use webpack to handle this?

http://webpack.github.io/docs/stylesheets.html

Webpack can use loaders for SASS, then using the ExtractTextPlugin, it can build your required css files on the fly.

Good job anyway on this boilerplate. It uses almost everything I want to use for a new isomorphic react stack.

Development server crashes on start due to SCSS parsing error

After cloning this repo and running npm install, I get the following error when trying to start up the development server:

» npm run dev

> [email protected] dev /Users/al3x/src/third_party/javascript/isomorphic500
> NODE_ENV=development DEBUG=isomorphic500 node index

  isomorphic500 Intl is not supported, so the polyfill has been loaded. +0ms
/Users/al3x/src/third_party/javascript/isomorphic500/node_modules/babel-core/lib/babel/helpers/parse.js:70
    throw err;
          ^
SyntaxError: /Users/al3x/src/third_party/javascript/isomorphic500/src/style/LocaleSwitcher.scss: Unexpected token (1:0)
> 1 | @import 'constants/colors';
    | ^
  2 | 
  3 | .LocaleSwitcher-link {
  4 |   display: inline-block;
    at Parser.pp.raise (/Users/al3x/src/third_party/javascript/isomorphic500/node_modules/babel-core/lib/acorn/src/location.js:68:13)
    at Parser.pp.unexpected (/Users/al3x/src/third_party/javascript/isomorphic500/node_modules/babel-core/lib/acorn/src/parseutil.js:87:8)
    at Parser.pp.parseDecorator (/Users/al3x/src/third_party/javascript/isomorphic500/node_modules/babel-core/lib/acorn/src/statement.js:146:10)
    at Parser.pp.parseDecorators (/Users/al3x/src/third_party/javascript/isomorphic500/node_modules/babel-core/lib/acorn/src/statement.js:132:31)
    at Parser.pp.parseStatement (/Users/al3x/src/third_party/javascript/isomorphic500/node_modules/babel-core/lib/acorn/src/statement.js:46:10)
    at Parser.parseStatement (/Users/al3x/src/third_party/javascript/isomorphic500/node_modules/babel-core/lib/acorn/plugins/flow.js:631:22)
    at Parser.pp.parseTopLevel (/Users/al3x/src/third_party/javascript/isomorphic500/node_modules/babel-core/lib/acorn/src/statement.js:22:21)
    at Object.parse (/Users/al3x/src/third_party/javascript/isomorphic500/node_modules/babel-core/lib/acorn/src/index.js:109:12)
    at module.exports (/Users/al3x/src/third_party/javascript/isomorphic500/node_modules/babel-core/lib/babel/helpers/parse.js:39:21)
    at File.<anonymous> (/Users/al3x/src/third_party/javascript/isomorphic500/node_modules/babel-core/lib/babel/transformation/file/index.js:518:12)

npm ERR! Darwin 14.3.0
npm ERR! argv "/Users/al3x/.nvm/versions/io.js/v1.7.1/bin/iojs" "/Users/al3x/.nvm/versions/io.js/v1.7.1/bin/npm" "run" "dev"
npm ERR! node v1.7.1
npm ERR! npm  v2.7.6
npm ERR! code ELIFECYCLE
npm ERR! [email protected] dev: `NODE_ENV=development DEBUG=isomorphic500 node index`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the [email protected] dev script 'NODE_ENV=development DEBUG=isomorphic500 node index'.
npm ERR! This is most likely a problem with the isomorphic500 package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     NODE_ENV=development DEBUG=isomorphic500 node index
npm ERR! You can get their info via:
npm ERR!     npm owner ls isomorphic500
npm ERR! There is likely additional logging output above.

Service set up with onSave reload

Hi,

I really like how you set up your app and I am very much following how you set up the app.

I do have one question though:

When you are doing development and when you save files (server side code), does the app restart for you? It works great for all the client side work, but it doesn't reload node server when I save files for server side code.

Could you point me to how I achieve that?

Sorry, this is not really issues, but I would appreciate your help!

Get rid of sass using javascript styles

I'd like to remove the CSS preprocessor and go straight with javascript also for the CSS (whoo), but it seems the available modules do not fit well with my architecture.

  • react-style is on the top list, but it is not working yet with react-hot-loader.
  • RCSS does not work with react-hot-loader, plus unclear things with webpack and cache-buster. But I love the idea behind it, more then react-style.
  • jss looks also promising but is not so react oriented as react-style.

Load More Photos

If we wanted to add a button at the bottom of the page called "Load More" and when the user clicks on it, the page loads more photos beneath the actual ones. I am new to React and Fluxible.
Anybody has any idea on how to implement this new feature?

Use ES6 in webpack prod config

It is confusing to see the dev config in ES6, like all the code and then the prod config in ES5.

Any particular reason for that?

npm install errors

Hi,

I have problem with install dependencies. Can you help me ?

bash-3.2# npm install
npm WARN package.json [email protected] No repository field.
npm WARN package.json [email protected] No repository field.
npm WARN cannot run in wd [email protected] npm run build (wd=/Users/tomasknapek/Downloads/isomorphic500-master 2)
npm ERR! peerinvalid The package react does not satisfy its siblings' peerDependencies requirements!
npm ERR! peerinvalid Peer [email protected] wants [email protected]
npm ERR! peerinvalid Peer [email protected] wants [email protected]
npm ERR! peerinvalid Peer [email protected] wants react@>=0.12.0 <=0.13.x
npm ERR! peerinvalid Peer [email protected] wants react@>=0.11.0

authentication

I have a backend nodejs/express server running and I want to do authentication. Once POST to /login I will retrieve a response header which contains a token for my future API calls. In the future whenever I click on other routes I just GET /loggein for updated user info.

This is OK if I don't refresh the browser and the information will be stored in the context. But when the user refreshs the browser the context is lost and user will forever lose this login token so he has to login again.

So I am wondering if there's a recommended login / profile retrieval process? I am doing something like this for now:

context.dispatch(Actions.LOGIN_SUCCESS, {
      user: data.body.user, 
      token: data.header.token
});

Or should I use localStorage? It seems extremely stupid to put validation token inside a localStorage...?
Thanks.

SASS recompilation doesn't work

I'm starting with a clean project.

$ git clone https://github.com/gpbl/isomorphic-react-template.git
Cloning into 'isomorphic-react-template'...
....
Resolving deltas: 100% (767/767), done.
Checking connectivity... done.
$ cd isomorphic-react-template
$ npm install

Then I do npm start and the development server starts. Then, I modify style/main.scss and server logs:

app Changed main.scss +21s

But no changes happen in public/css/main.css. What can be wrong? I tried to replace sass.render with sass.renderSync in dev-tools.js but with no success.

I'm using node v0.12.1 running on OSX 10.10.2

Is it possible to set up an absolute path for import in css and js?

Hey, another question.

The solution when you have to set paths relative to the position of the file you are importing to, looks rather inconvenient, because when you move one file, you have to update not only the import paths of modules that use it, but also the paths of modules it imports itself. It bring an unnecessary level of complexion.

Is it possible to somehow change it, so it looks for the files always starting from the /src folder and down?

Google Analytics route change handling mistake

Hi,
You should also send path with GA 'pageview' request as stated here https://developers.google.com/analytics/devguides/collection/analyticsjs/single-page-applications otherwise all subsequent hits are counted for the entry page.

So this

function trackPageView() {
if (!window.ga) {
return;
}
window.ga("send", "pageview");
}
should become

function trackPageView(path) {
    if (!window.ga) {
        return;
    }
    window.ga("send", "pageview", path);
}

And this

if (!Immutable.is(prevProps.currentRoute, currentRoute)) {
trackPageView();
}
should become

if (!Immutable.is(prevProps.currentRoute, currentRoute)) {
    trackPageView(currentRoute.get('url'));
}

Windows 8 Install won't run (development)

I installed node v0.12.(0.12.4)
I cloned your project
npm install (run as administrator)

install produced some errors (actually lots of them) - but here is an example

F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-lient\node_modules\engine.io-client\node_modules\ws>if not defined npm_config_node_gyp (node "C:\Program Files\nodejs\node_modules\npm\bin\node-gyp-bin\\..\..\node_modules\node-gyp\bin\node-gyp.js" rebuild )
  else (rebuild)Building the projects in this solution one at a time. 
  To enable parallel build, please add the "/m" switch.
  bufferutil.cc
F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\node_modules\nan\nan.h(213): error C2039: 'ThrowException' : is not a member of 'v8' [F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\build\bufferutil.vcxproj]
F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\node_modules\nan\nan.h(213): error C2039: 'New' : is not a member of 'v8::String' [F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\build\bufferutil.vcxproj]
  C:\Users\steve\.node-gyp\0.12.4\deps\v8\include\v8.h(1599) : see declaration of 'v8::String'
F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\node_modules\nan\nan.h(213): error C3861: 'ThrowException': identifier not found [F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\build\bufferutil.vcxproj]
F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\node_modules\nan\nan.h(213): error C3861: 'New': identifier not found [F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\build\bufferutil.vcxproj]
F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\node_modules\nan\nan.h(218): error C2039: 'ThrowException' : is not a member of 'v8' [F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\build\bufferutil.vcxproj]
F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\node_modules\nan\nan.h(218): error C3861: 'ThrowException': identifier not found [F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\build\bufferutil.vcxproj]
F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\node_modules\nan\nan.h(222): error C2039: 'New' : is not a member of 'v8::String' [F:\Projects\isomorphic500-master\node_modules\webpack-dev-server\node_modules\socket.io-client\node_modules\engine.io-client\node_modules\ws\build\bufferutil.vcxproj]
  C:\Users\steve\.node-gyp\0.12.4\deps\v8\include\v8.h(1599) : see declaration of 'v8::String'

The post install build process ran with a few warnings

 > [email protected] build F:\Projects\isomorphic500-master
 > webpack --stats --progress --config ./webpack/prod.config.js

 42% 98/179 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 43% 99/179 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 52% 179/251 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 56% 197/256 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 56% 198/256 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 56% 199/256 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 56% 200/256 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 65% 251/269 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 66% 301/318 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 65% 317/344 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 68% 405/415 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 68% 406/415 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 68% 407/415 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 68% 408/415 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 69% 409/415 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 69% 410/415 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 69% 411/415 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 69% 412/415 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 69% 413/415 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
 69% 414/415 build modulesAutoprefixer's process() method is deprecated and will removed in next major release. Use postcss([autoprefixer]).process() instead
Hash: 93d9b22a32b415831dbe
Version: webpack 1.9.10
Time: 58250ms

                    Asset                            Size      Chunks           Chunk Names
     main-159534d8036482e126a4.css                  5.74 kB       0  [emitted]  main
     3fcabfc9ec25aababcdaa2b056cf32bf.svg           2.05 kB          [emitted]
     locale-it-abbb4c01f9add2390a1f.js              14.8 kB    1, 2  [emitted]  locale-it
     locale-it-no-intl-4377e1ebae56b2bae973.js      1.55 kB       2  [emitted]  locale-it-no-intl
     locale-en-4179b1a80b22685aea97.js              13.4 kB       3  [emitted]  locale-en
     intl-bb08eeb1f614b9790298.js                   27.5 kB       4  [emitted]  intl
     main-159534d8036482e126a4.js                    390 kB       0  [emitted]  main
     main-159534d8036482e126a4.js.map               4.16 MB       0  [emitted]  main
     main-159534d8036482e126a4.css.map               106 bytes    0  [emitted]  main
     locale-it-abbb4c01f9add2390a1f.js.map          37.5 kB    1, 2  [emitted]  locale-it
     locale-it-no-intl-4377e1ebae56b2bae973.js.map   5.7 kB       2  [emitted]  locale-it-no-intl
     locale-en-4179b1a80b22685aea97.js.map          32.4 kB       3  [emitted]  locale-en
     intl-bb08eeb1f614b9790298.js.map                310 kB       4  [emitted]  intl

but nothing else seemed to be an issue so I assumed as the build has run it was OK.

npm start dev 

produced an error of [email protected] start: NODE_ENV=production node index "dev"

so I did it all manually

SET NODE_ENV=development 
SET DEBUG=isomorphic500
node index

I get the message.

Express development  server listening on 3000

I navigated to that address

http://localhost:3000

Node now throws an error into the console

  isomorphic500 Detected locale (from browser) is en +0ms
Error on request GET /
[TypeError: Cannot read property 'script' of undefined]
TypeError: Cannot read property 'script' of undefined
    at renderApp (F:/Projects/isomorphic500-master/src/server/render.js:48:29)
    at F:/Projects/isomorphic500-master/src/server/render.js:76:19
GET / 500 293.241 ms - 22

The browser just displays a blank page with:

Something bad happened

however, when I run

SET NODE_ENV=production
SET DEBUG=
node index

everything seem to work correctly. There appears to be some issue in the development configuration.

Uncaught ReferenceError: webpackJsonp

I'm getting an error when using production as environment mode in windows 8

I've followed the instructions
gulp build
set NODE_ENV=production
node build/app.js

"Uncaught ReferenceError: webpackJsonp is not defined" is displayed in the console

isomorphic500 Intl is not supported, so the polyfill has been loaded

When I run:
$ npm run dev

[email protected] dev /Users/herve76/Desktop/isomorphic500
NODE_ENV=development DEBUG=isomorphic500 node index
isomorphic500 Intl is not supported, so the polyfill has been loaded. +0ms
Express development server listening on 3000
isomorphic500 Webpack development server listening on localhost:3001 +2s
Hash: 28498d7b5b156072b02c
Version: webpack 1.8.11
Time: 7796ms

Why does it say that isomorphic500 Intl is not supported?

Cordially,

Hervé

ELIFECYCLE error on npm run dev/prod

I came across this issue running (Git) bash and command prompt on Windows 8.1:

0 info it worked if it ends with ok
1 verbose cli [ 'c:\\Program Files\\nodejs\\node.exe',
1 verbose cli   'c:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js',
1 verbose cli   'run',
1 verbose cli   'dev' ]
2 info using [email protected]
3 info using [email protected]
4 verbose run-script [ 'predev', 'dev', 'postdev' ]
5 info predev [email protected]
6 info dev [email protected]
7 verbose unsafe-perm in lifecycle true
8 info [email protected] Failed to exec dev script
9 verbose stack Error: [email protected] dev: `NODE_ENV=development DEBUG=isomorphic500 node index`
9 verbose stack Exit status 1
9 verbose stack     at EventEmitter.<anonymous> (c:\Program Files\nodejs\node_modules\npm\lib\utils\lifecycle.js:213:16)
9 verbose stack     at EventEmitter.emit (events.js:110:17)
9 verbose stack     at ChildProcess.<anonymous> (c:\Program Files\nodejs\node_modules\npm\lib\utils\spawn.js:14:12)
9 verbose stack     at ChildProcess.emit (events.js:110:17)
9 verbose stack     at maybeClose (child_process.js:1015:16)
9 verbose stack     at Process.ChildProcess._handle.onexit (child_process.js:1087:5)
10 verbose pkgid [email protected]
11 verbose cwd c:\dev\isomorphic500
12 error Windows_NT 6.3.9600
13 error argv "c:\\Program Files\\nodejs\\node.exe" "c:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js" "run" "dev"
14 error node v0.12.2
15 error npm  v2.7.4
16 error code ELIFECYCLE
17 error [email protected] dev: `NODE_ENV=development DEBUG=isomorphic500 node index`
17 error Exit status 1
18 error Failed at the [email protected] dev script 'NODE_ENV=development DEBUG=isomorphic500 node index'.
18 error This is most likely a problem with the isomorphic500 package,
18 error not with npm itself.
18 error Tell the author that this fails on your system:
18 error     NODE_ENV=development DEBUG=isomorphic500 node index
18 error You can get their info via:
18 error     npm owner ls isomorphic500
18 error There is likely additional logging output above.
19 verbose exit [ 1, true ]

Excerpt from output when running npm run dev:

> NODE_ENV=development DEBUG=isomorphic500 node index

'NODE_ENV' is not recognized as an internal or external command, operable program or batch file.

It looks like it's a problem with npm on Windows, as just running the intended command (NODE_ENV=development DEBUG=isomorphic500 node index) directly in bash works fine.

Just thought I'd provide some feedback concerning the issue I've encountered and the workaround, for reference purposes.

Thanks for the project, it looks well put together! (P.S. Looking forward to fluxible-router integration)

Hitting a 404 page crashes the application

I tested out https://isomorphic500.herokuapp.com/featured/editorss (extra "s") and it seemed to knock the Heroku app off line:

Application Error
An error occurred in the application and your page could not be served. Please try again in a few moments.

If you are the application owner, check your logs for details.

Development crash callstack:

TypeError: Cannot read property 'isLoading' of null
   at [object Object].render (/mnt/isomorphic500/src/Application.js:59:29)
   at [object Object].ReactCompositeComponentMixin._renderValidatedComponentWithoutOwnerOrContext (/mnt/isomorphic500/node_modules/react/lib/ReactCompositeComponent.js:767:34)
   at [object Object].ReactCompositeComponentMixin._renderValidatedComponent (/mnt/isomorphic500/node_modules/react/lib/ReactCompositeComponent.js:793:14)
   at [object Object].wrapper [as _renderValidatedComponent] (/mnt/isomorphic500/node_modules/react/lib/ReactPerf.js:70:21)
   at [object Object].ReactCompositeComponentMixin.mountComponent (/mnt/isomorphic500/node_modules/react/lib/ReactCompositeComponent.js:227:30)
   at [object Object].wrapper [as mountComponent] (/mnt/isomorphic500/node_modules/react/lib/ReactPerf.js:70:21)
   at Object.ReactReconciler.mountComponent (/mnt/isomorphic500/node_modules/react/lib/ReactReconciler.js:38:35)
   at [object Object].ReactCompositeComponentMixin.mountComponent (/mnt/isomorphic500/node_modules/react/lib/ReactCompositeComponent.js:237:34)
   at [object Object].wrapper [as mountComponent] (/mnt/isomorphic500/node_modules/react/lib/ReactPerf.js:70:21)
   at Object.ReactReconciler.mountComponent (/mnt/isomorphic500/node_modules/react/lib/ReactReconciler.js:38:35)

Problems on Windows

First attempt at running on Windows resulted in:

D:\Projects\isomorphic500>npm run dev

[email protected] dev D:\Projects\isomorphic500
NODE_ENV=development DEBUG=isomorphic500 node index
'NODE_ENV' is not recognized as an internal or external command,

operable program or batch file.

I edited the package.json file to comment out the dev string
-> "dev": "", //NODE_ENV=development DEBUG=isomorphic500 node index

Then ran 3 separate lines in the command prompt
SET NODE_ENV=development
SET DEBUG=isomorphic500
node index

Things appeared to run better but my web page at that point said simply "Something bad happened".

I'm not sure how to go about fixing this error.

D:\Projects\isomorphic500>node index
Express development server listening on 3000
isomorphic500 Detected locale (from browser) is en +0ms
Error on request GET /
[TypeError: Cannot read property 'script' of undefined]
TypeError: Cannot read property 'script' of undefined
at renderApp (D:/Projects/isomorphic500/src/server/render.js:48:29)
at D:/Projects/isomorphic500/src/server/render.js:76:19
GET / 500 65.799 ms - 22
isomorphic500 Detected locale (from browser) is en +5s
Error on request GET /
[TypeError: Cannot read property 'script' of undefined]
TypeError: Cannot read property 'script' of undefined
at renderApp (D:/Projects/isomorphic500/src/server/render.js:48:29)
at D:/Projects/isomorphic500/src/server/render.js:76:19
GET / 500 25.604 ms - 22
GET /favicon.ico 200 9.114 ms - -

^C

Next steps for this project

During the last months I've learnt lot of things I'd like to apply to this project, which is starting to get outdated. Instead of a "template", it could became a work-in-progress, full-stack example for people approaching an isomorphic app with React and Flux.

The critical parts I would explore are the architecture design and the development environment:

Architecture

  • isomorphic flux architecture (with fluxible)
  • smart and dumb React components (inspiration)
  • atomic, per-component CSS files
  • an approach to i18n with react-intl
  • use of immutable data (immutable.js)

Dev environment

  • use of babeljs and eslint
  • no build process (skip gulp)
  • a solid webpack config (e.g. assets)
  • components and styles with live reloading
  • use webpack to compile server-side as well
  • tests with Jest

There’s a lot to do!
I need to find an example app that is not as boring as a to-do list or a repo browser. The problem is that it’s hard to find public or freely available API exposing enough data...
... so I thought to make a 500px client (because I love photography), and call this project isomorphic 500.

Let see where it goes… I'd appreciate some feedback and contributions. I've added a gitter chat.

I'm happy this project got so many stargazers :-) Thanks to all!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.