From aee54aec00e95adda4bbd80ba1c5b19985fa60d6 Mon Sep 17 00:00:00 2001
From: Samuel Attard <samuel.r.attard@gmail.com>
Date: Sun, 23 Jul 2017 16:03:16 +1000
Subject: [PATCH 1/5] Enable fancy scroll bars

---
 src/main/main.js | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/main/main.js b/src/main/main.js
index 445d86a..b17cc1f 100644
--- a/src/main/main.js
+++ b/src/main/main.js
@@ -36,6 +36,12 @@ export class App {
         this.onReady()
       }
     })
+
+    // Fancy scrollbars
+    app.commandLine.appendSwitch('enable-smooth-scrolling', '1')
+    app.commandLine.appendSwitch('enable-overlay-scrollbar', '1')
+    // Fix HDPI zoom bug
+    app.commandLine.appendSwitch('enable-use-zoom-for-dsf', 'false')
   }
 
   onReady () {

From cc9d37876337c9210ca56ea7b76cb2bd314c88f2 Mon Sep 17 00:00:00 2001
From: Samuel Attard <samuel.r.attard@gmail.com>
Date: Sun, 23 Jul 2017 16:03:39 +1000
Subject: [PATCH 2/5] Enforce minimum window height to keep things looking
 right

---
 src/main/window-manager.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/main/window-manager.js b/src/main/window-manager.js
index 6703409..3390f3b 100644
--- a/src/main/window-manager.js
+++ b/src/main/window-manager.js
@@ -19,7 +19,8 @@ class WindowManager {
       x: mainWindowState.x,
       y: mainWindowState.y,
       width: mainWindowState.width,
-      height: mainWindowState.height
+      height: mainWindowState.height,
+      minHeight: 450
     })
 
     mainWindowState.manage(browserWindow)

From b0d73cb890f215baf3e192259da7cf98e5c1f555 Mon Sep 17 00:00:00 2001
From: Samuel Attard <samuel.r.attard@gmail.com>
Date: Sun, 23 Jul 2017 16:04:41 +1000
Subject: [PATCH 3/5] Setup react dev tools and react HMR

---
 package.json                                  |  1 +
 spec/__mocks__/electron-compile.js            |  5 +++
 spec/__mocks__/electron-devtools-installer.js | 10 +++++
 spec/__mocks__/electron.js                    | 14 ++++++-
 spec/main/developer-spec.js                   | 39 +++++++++++++++++--
 spec/setup.js                                 |  8 +++-
 src/main/developer.js                         | 15 +++++++
 7 files changed, 85 insertions(+), 7 deletions(-)
 create mode 100644 spec/__mocks__/electron-compile.js
 create mode 100644 spec/__mocks__/electron-devtools-installer.js

diff --git a/package.json b/package.json
index e0b8cdc..38b31ab 100644
--- a/package.json
+++ b/package.json
@@ -103,6 +103,7 @@
     "babel-preset-node7": "^1.5.0",
     "babel-register": "^6.24.1",
     "devtron": "^1.4.0",
+    "electron-devtools-installer": "^2.2.0",
     "electron-forge": "^3.0.5",
     "electron-prebuilt-compile": "1.7.4",
     "electron-process-manager": "0.0.4",
diff --git a/spec/__mocks__/electron-compile.js b/spec/__mocks__/electron-compile.js
new file mode 100644
index 0000000..5fac68f
--- /dev/null
+++ b/spec/__mocks__/electron-compile.js
@@ -0,0 +1,5 @@
+import sinon from 'sinon'
+
+export const electronCompileMock = {
+  enableLiveReload: sinon.spy()
+}
diff --git a/spec/__mocks__/electron-devtools-installer.js b/spec/__mocks__/electron-devtools-installer.js
new file mode 100644
index 0000000..b12e374
--- /dev/null
+++ b/spec/__mocks__/electron-devtools-installer.js
@@ -0,0 +1,10 @@
+import sinon from 'sinon'
+
+const installStub = sinon.stub()
+installStub.returns(Promise.resolve())
+
+export const electronDevtoolsInstallerMock = {
+  __esModule: true,
+  default: installStub,
+  REACT_DEVELOPER_TOOLS: 'react'
+}
diff --git a/spec/__mocks__/electron.js b/spec/__mocks__/electron.js
index 6987e48..94e1015 100644
--- a/spec/__mocks__/electron.js
+++ b/spec/__mocks__/electron.js
@@ -52,12 +52,19 @@ class BrowserWindow extends EventEmitter {
   }
 }
 
+class CommandLine {
+  constructor () {
+    this.appendSwitch = sinon.stub()
+  }
+}
+
 class App extends EventEmitter {
   constructor () {
     super()
 
     this.getName = sinon.stub()
-    this.getPah = sinon.stub()
+    this.getPath = sinon.stub()
+    this.commandLine = new CommandLine()
   }
 }
 
@@ -90,7 +97,8 @@ export const electronMock = {
     getCurrentWindow: sinon.stub(),
     require: sinon.stub(),
     Menu: MockMenu,
-    MenuItem: MockMenuItem
+    MenuItem: MockMenuItem,
+    app: new App()
   },
   ipcRenderer: {
     send: sinon.stub()
@@ -104,3 +112,5 @@ export const electronMock = {
   screen: new Screen(),
   BrowserWindow
 }
+
+export const electronMainMock = Object.assign({}, electronMock, { remote: null })
diff --git a/spec/main/developer-spec.js b/spec/main/developer-spec.js
index 4bc3177..593417b 100644
--- a/spec/main/developer-spec.js
+++ b/spec/main/developer-spec.js
@@ -2,12 +2,12 @@ import test from 'ava'
 import mockery from 'mockery'
 import sinon from 'sinon'
 
-import { electronMock } from '../../spec/__mocks__/electron'
+import { electronMainMock } from '../../spec/__mocks__/electron'
 
 require('../setup').setup(test)
 
 test('it installs devtron if not installed', (t) => {
-  mockery.registerMock('electron', electronMock)
+  mockery.registerMock('electron', electronMainMock)
   mockery.registerMock('devtron', { install: sinon.spy() })
 
   const { BrowserWindow } = require('electron')
@@ -23,7 +23,7 @@ test('it installs devtron if not installed', (t) => {
 })
 
 test('does not it installs devtron if already installed', (t) => {
-  mockery.registerMock('electron', electronMock)
+  mockery.registerMock('electron', electronMainMock)
   mockery.registerMock('devtron', { install: sinon.spy() })
 
   const { BrowserWindow } = require('electron')
@@ -32,8 +32,39 @@ test('does not it installs devtron if already installed', (t) => {
   const { DeveloperFeatures } = require('../../src/main/developer')
 
   //eslint-disable-next-line
-  const developerFeatures = new DeveloperFeatures()
+  new DeveloperFeatures()
 
   const devtron = require('devtron')
   t.is(devtron.install.callCount, 0)
 })
+
+test('it enables react HMR', (t) => {
+  mockery.registerMock('electron', electronMainMock)
+  mockery.registerMock('devtron', { install: sinon.spy() })
+  mockery.registerMock('electron-compile', { enableLiveReload: sinon.spy() })
+
+  const { DeveloperFeatures } = require('../../src/main/developer')
+  //eslint-disable-next-line
+  new DeveloperFeatures()
+
+  const electronCompile = require('electron-compile')
+
+  t.is(electronCompile.enableLiveReload.callCount, 1)
+  t.is(electronCompile.enableLiveReload.firstCall.args[0].strategy, 'react-hmr')
+})
+
+test('it attempts to install react dev tools', (t) => {
+  mockery.registerMock('electron', electronMainMock)
+  mockery.registerMock('devtron', { install: sinon.spy() })
+  mockery.registerMock('electron-devtools-installer', { __esModule: true, default: sinon.stub(), REACT_DEVELOPER_TOOLS: 'react-tools' })
+
+  const electronDevtoolsInstaller = require('electron-devtools-installer')
+  electronDevtoolsInstaller.default.returns(Promise.resolve())
+
+  const { DeveloperFeatures } = require('../../src/main/developer')
+  //eslint-disable-next-line
+  new DeveloperFeatures()
+
+  t.is(electronDevtoolsInstaller.default.callCount, 1)
+  t.is(electronDevtoolsInstaller.default.firstCall.args[0], 'react-tools')
+})
diff --git a/spec/setup.js b/spec/setup.js
index 56b2fa6..9ebc6d3 100644
--- a/spec/setup.js
+++ b/spec/setup.js
@@ -1,12 +1,16 @@
 import mockery from 'mockery'
 
 import { logMock } from './__mocks__/electron-log'
+import { electronDevtoolsInstallerMock } from './__mocks__/electron-devtools-installer'
+import { electronCompileMock } from './__mocks__/electron-compile'
 import { electronMock } from './__mocks__/electron'
 import { windowStateMock } from './__mocks__/electron-window-state'
 
 export function setup (test) {
   test.before((t) => {
-    mockery.enable()
+    mockery.enable({
+      useCleanCache: true
+    })
     mockery.warnOnUnregistered(false)
   })
 
@@ -16,6 +20,8 @@ export function setup (test) {
     mockery.registerMock('electron-window-state', windowStateMock)
     mockery.registerMock('electron', electronMock)
     mockery.registerMock('electron-log', logMock)
+    mockery.registerMock('electron-devtools-installer', electronDevtoolsInstallerMock)
+    mockery.registerMock('electron-compile', electronCompileMock)
   })
 
   test.after((t) => {
diff --git a/src/main/developer.js b/src/main/developer.js
index 5114e9b..473fa45 100644
--- a/src/main/developer.js
+++ b/src/main/developer.js
@@ -1,12 +1,22 @@
 import { BrowserWindow } from 'electron'
 import devtron from 'devtron'
+import * as electronCompile from 'electron-compile'
+import installDevTools, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'
 
 import { logger } from '../logger'
 
 export class DeveloperFeatures {
   constructor () {
     this.extensions = BrowserWindow.getDevToolsExtensions()
+    this.enableHMR()
     this.installDevtron()
+    this.installReactTools()
+  }
+
+  enableHMR () {
+    electronCompile.enableLiveReload({
+      strategy: 'react-hmr'
+    })
   }
 
   installDevtron () {
@@ -17,4 +27,9 @@ export class DeveloperFeatures {
       devtron.install()
     }
   }
+
+  installReactTools () {
+    installDevTools(REACT_DEVELOPER_TOOLS)
+      .catch((err) => logger.error('Failed to install React dev tools', err))
+  }
 }

From 4d50bc2b57aafa8b9af926781ac8488e39c83585 Mon Sep 17 00:00:00 2001
From: Samuel Attard <samuel.r.attard@gmail.com>
Date: Sun, 23 Jul 2017 16:05:19 +1000
Subject: [PATCH 4/5] Add config for react transpiling

---
 .compilerc   |  6 ++++--
 package.json | 10 ++++++++++
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/.compilerc b/.compilerc
index 484258f..b019e33 100644
--- a/.compilerc
+++ b/.compilerc
@@ -2,7 +2,8 @@
   "env": {
     "development": {
       "application/javascript": {
-        "presets": ["node7"],
+        "presets": ["node7", "react"],
+        "plugins": ["transform-class-properties"],
         "sourceMaps": "inline"
       },
       "text/less": {
@@ -11,7 +12,8 @@
     },
     "production": {
       "application/javascript": {
-        "presets": ["node7"],
+        "presets": ["node7", "react"],
+        "plugins": ["transform-class-properties"],
         "sourceMaps": "none"
       }
     }
diff --git a/package.json b/package.json
index 38b31ab..dcf2eaa 100644
--- a/package.json
+++ b/package.json
@@ -84,6 +84,9 @@
       "node7"
     ]
   },
+  "standard": {
+    "parser": "babel-eslint"
+  },
   "dependencies": {
     "electron-compile": "^6.4.1",
     "electron-log": "^2.2.7",
@@ -94,13 +97,19 @@
     "once": "^1.4.0",
     "osenv": "^0.1.4",
     "prismjs": "^1.6.0",
+    "prop-types": "^15.5.10",
+    "react": "^15.6.1",
+    "react-dom": "^15.6.1",
     "request": "^2.81.0",
     "semver": "^5.3.0",
     "yo-yo": "^1.4.1"
   },
   "devDependencies": {
     "ava": "^0.21.0",
+    "babel-eslint": "^7.2.3",
+    "babel-plugin-transform-class-properties": "^6.24.1",
     "babel-preset-node7": "^1.5.0",
+    "babel-preset-react": "^6.24.1",
     "babel-register": "^6.24.1",
     "devtron": "^1.4.0",
     "electron-devtools-installer": "^2.2.0",
@@ -108,6 +117,7 @@
     "electron-prebuilt-compile": "1.7.4",
     "electron-process-manager": "0.0.4",
     "mockery": "^2.1.0",
+    "react-hot-loader": "^3.0.0-beta.6",
     "sinon": "^2.3.8",
     "spectron": "^3.7.2",
     "standard": "^10.0.2"

From 69c4d6992e15376a0cfd2b79b902b49b17007806 Mon Sep 17 00:00:00 2001
From: Samuel Attard <samuel.r.attard@gmail.com>
Date: Sun, 23 Jul 2017 16:06:33 +1000
Subject: [PATCH 5/5] :tada: Convert the front end to react and modernize the
 existing lib files

---
 package.json                                  |   3 +-
 src/renderer/components/App.jsx               |  98 ++++++
 .../components/InstalledNodeVersion.jsx       |  45 +++
 src/renderer/components/InstallingOverlay.jsx |  51 +++
 src/renderer/components/LeftPanel.jsx         |  86 ++++++
 src/renderer/components/RandomExample.jsx     |  39 +++
 src/renderer/components/RightPanel.jsx        |  35 +++
 src/renderer/components/icons/Error.jsx       |   8 +
 src/renderer/components/icons/NodeWhite.jsx   |  10 +
 src/renderer/components/icons/Spinner.jsx     |   8 +
 src/renderer/components/icons/index.js        |   3 +
 src/renderer/components/images/NodeLogo.jsx   |  15 +
 .../components/images/NodeSchoolLogo.jsx      |  15 +
 src/renderer/index.html                       |  86 +-----
 src/renderer/less/core.less                   |  44 +++
 src/renderer/less/installing-overlay.less     |  40 +++
 src/renderer/less/left-panel.less             |  70 +++++
 src/renderer/less/panels.less                 |   9 +
 src/renderer/less/right-panel.less            |  46 +++
 src/renderer/less/spinner.less                |  89 ++++++
 src/renderer/lib/Installer.js                 | 115 +++++++
 src/renderer/lib/examples.js                  |   3 +-
 src/renderer/lib/install.js                   |  60 ----
 src/renderer/lib/load.js                      |  16 -
 src/renderer/lib/versions.js                  |  78 +++--
 src/renderer/renderer.js                      | 105 -------
 static/style.css                              | 290 ------------------
 27 files changed, 882 insertions(+), 585 deletions(-)
 create mode 100644 src/renderer/components/App.jsx
 create mode 100644 src/renderer/components/InstalledNodeVersion.jsx
 create mode 100644 src/renderer/components/InstallingOverlay.jsx
 create mode 100644 src/renderer/components/LeftPanel.jsx
 create mode 100644 src/renderer/components/RandomExample.jsx
 create mode 100644 src/renderer/components/RightPanel.jsx
 create mode 100644 src/renderer/components/icons/Error.jsx
 create mode 100644 src/renderer/components/icons/NodeWhite.jsx
 create mode 100644 src/renderer/components/icons/Spinner.jsx
 create mode 100644 src/renderer/components/icons/index.js
 create mode 100644 src/renderer/components/images/NodeLogo.jsx
 create mode 100644 src/renderer/components/images/NodeSchoolLogo.jsx
 create mode 100644 src/renderer/less/core.less
 create mode 100644 src/renderer/less/installing-overlay.less
 create mode 100644 src/renderer/less/left-panel.less
 create mode 100644 src/renderer/less/panels.less
 create mode 100644 src/renderer/less/right-panel.less
 create mode 100644 src/renderer/less/spinner.less
 create mode 100644 src/renderer/lib/Installer.js
 delete mode 100644 src/renderer/lib/install.js
 delete mode 100644 src/renderer/lib/load.js
 delete mode 100644 src/renderer/renderer.js
 delete mode 100644 static/style.css

diff --git a/package.json b/package.json
index dcf2eaa..ac13438 100644
--- a/package.json
+++ b/package.json
@@ -94,12 +94,13 @@
     "electron-window-state": "^4.1.1",
     "flexboxgrid": "^6.3.1",
     "font-awesome": "^4.7.0",
-    "once": "^1.4.0",
     "osenv": "^0.1.4",
     "prismjs": "^1.6.0",
     "prop-types": "^15.5.10",
     "react": "^15.6.1",
     "react-dom": "^15.6.1",
+    "react-fontawesome": "^1.6.1",
+    "react-prism": "^4.3.0",
     "request": "^2.81.0",
     "semver": "^5.3.0",
     "yo-yo": "^1.4.1"
diff --git a/src/renderer/components/App.jsx b/src/renderer/components/App.jsx
new file mode 100644
index 0000000..f8d28f4
--- /dev/null
+++ b/src/renderer/components/App.jsx
@@ -0,0 +1,98 @@
+import React from 'react'
+
+import { InstallingOverlay } from './InstallingOverlay'
+import { LeftPanel } from './LeftPanel'
+import { RightPanel } from './RightPanel'
+
+import { Installer } from '../lib/Installer'
+import getInstalledVersion from '../lib/check-node'
+import getVersions from '../lib/versions'
+
+const INSTALL_STATUS = {
+  NOT_INSTALLING: 0,
+  INSTALLING: 1,
+  SUCCESS: 2,
+  ERROR: 3
+}
+
+export class App extends React.Component {
+  state = {
+    installStatus: INSTALL_STATUS.NOT_INSTALLING,
+    installError: null,
+    versions: null,
+    currentVersion: null
+  }
+
+  componentDidMount () {
+    this.loadVersionList()
+    getInstalledVersion((err, version) => {
+      if (!err) {
+        this.setState({
+          currentVersion: version
+        })
+      }
+    })
+  }
+
+  cancelInstall = () => {
+    if (this._currentInstalller) {
+      this._currentInstalller.cancel()
+    }
+    this.setState({
+      installStatus: INSTALL_STATUS.NOT_INSTALLING,
+      installError: null
+    })
+  }
+
+  installVersion = (version) => {
+    this.setState({
+      installStatus: INSTALL_STATUS.INSTALLING
+    })
+    const installer = new Installer(version)
+    this._currentInstalller = installer
+    installer.on('error', (err) => {
+      console.error(err)
+      this.setState({
+        installStatus: INSTALL_STATUS.ERROR,
+        installError: err
+      })
+    })
+    installer.on('done', () => {
+      this.setState({
+        currentVersion: version,
+        installStatus: INSTALL_STATUS.SUCCESS
+      })
+      delete this._currentInstalller
+    })
+    installer.install()
+  }
+
+  loadVersionList () {
+    window.fetch('https://nodejs.org/dist/index.json')
+      .then(r => r.json())
+      .then(getVersions)
+      .then((versions) => {
+        this.setState({
+          versions
+        })
+      })
+  }
+
+  render () {
+    return (
+      <div id='main'>
+        <InstallingOverlay
+          installing={this.state.installStatus === INSTALL_STATUS.INSTALLING}
+          error={this.state.installError}
+          onCancel={this.cancelInstall}
+        />
+        <LeftPanel
+          versions={this.state.versions}
+          currentVersion={this.state.currentVersion}
+          installVersion={this.installVersion}
+        />
+        <RightPanel />
+      </div>
+    )
+  }
+}
diff --git a/src/renderer/components/InstalledNodeVersion.jsx b/src/renderer/components/InstalledNodeVersion.jsx
new file mode 100644
index 0000000..244b06d
--- /dev/null
+++ b/src/renderer/components/InstalledNodeVersion.jsx
@@ -0,0 +1,45 @@
+import React from 'react'
+
+import { ErrorIcon, SpinnerIcon } from './icons'
+
+import getInstalledVersion from '../lib/check-node'
+
+export class InstalledNodeVersion extends React.Component {
+  state = {
+    loading: true,
+    error: false,
+    version: null
+  }
+
+  componentDidMount () {
+    getInstalledVersion((err, version) => {
+      if (err) {
+        this.setState({
+          error: true,
+          loading: false
+        })
+      } else {
+        this.setState({
+          loading: false,
+          version
+        })
+      }
+    })
+  }
+
+  render () {
+    return (
+      <span>
+        {
+          this.state.loading
+          ? <SpinnerIcon />
+          : (
+            this.state.error
+            ? <ErrorIcon />
+            : this.state.version
+          )
+        }
+      </span>
+    )
+  }
+}
diff --git a/src/renderer/components/InstallingOverlay.jsx b/src/renderer/components/InstallingOverlay.jsx
new file mode 100644
index 0000000..ef601ae
--- /dev/null
+++ b/src/renderer/components/InstallingOverlay.jsx
@@ -0,0 +1,51 @@
+import React from 'react'
+import { any, bool, func } from 'prop-types'
+
+export class InstallingOverlay extends React.Component {
+  static propTypes = {
+    installing: bool.isRequired,
+    error: any,
+    onCancel: func.isRequired
+  }
+
+  render () {
+    if (!this.props.installing && !this.props.error) {
+      return null
+    }
+    return (
+      <div className='row installing'>
+        <div className='col-xs-12'>
+          <h1 className='color-green'>Installing Node.js</h1>
+        </div>
+        {
+          this.props.installing
+          ? (
+            <div className='col-xs-12'>
+              <div className='sk-folding-cube'>
+                <div className='sk-cube1 sk-cube' />
+                <div className='sk-cube2 sk-cube' />
+                <div className='sk-cube4 sk-cube' />
+                <div className='sk-cube3 sk-cube' />
+              </div>
+            </div>
+          )
+          : null
+        }
+        <div className='col-xs-offset-3 col-xs-6 error-message'>
+          {
+            this.props.error
+            ? <p className='color-red error-text'>{this.props.error.message}</p>
+            : null
+          }
+          <a href='#' className='color-red error-button' onClick={this.props.onCancel}>
+            {
+              this.props.installing
+              ? 'Cancel Install'
+              : 'Return to installer'
+            }
+          </a>
+        </div>
+      </div>
+    )
+  }
+}
diff --git a/src/renderer/components/LeftPanel.jsx b/src/renderer/components/LeftPanel.jsx
new file mode 100644
index 0000000..8493cff
--- /dev/null
+++ b/src/renderer/components/LeftPanel.jsx
@@ -0,0 +1,86 @@
+import { shell } from 'electron'
+import { any, func, string } from 'prop-types'
+import React from 'react'
+import semver from 'semver'
+
+import { InstalledNodeVersion } from './InstalledNodeVersion'
+import { NodeLogo } from './images/NodeLogo'
+import { NodeWhiteIcon, SpinnerIcon } from './icons'
+
+export class LeftPanel extends React.Component {
+  static propTypes = {
+    currentVersion: string,
+    versions: any,
+    installVersion: func.isRequired
+  }
+
+  hasUpdate () {
+    if (!this.props.currentVersion) return false
+    if (!this.props.versions) return false
+    return semver.gt(this.props.versions.latest.version, this.props.currentVersion)
+  }
+
+  installLatest = () => {
+    if (!this.props.versions) return
+    this.props.installVersion(this.props.versions.latest.version)
+  }
+
+  installLatestLTS = () => {
+    if (!this.props.versions) return
+    this.props.installVersion(this.props.versions.latestLTS.version)
+  }
+
+  launchNodeWebsite (event) {
+    shell.openExternal('https://nodejs.org/en/')
+    event.preventDefault()
+  }
+
+  render () {
+    const loadingVersionList = !this.props.versions
+    return (
+      <div className='left-panel background-green'>
+        <div className='row'>
+          <div className='col-xs-12 node-image-container'>
+            <NodeLogo onClick={this.launchNodeWebsite} />
+          </div>
+        </div>
+        <div className='row'>
+          <div className='col-xs-12'>
+            <p className='installed-version'>
+              Installed version: <InstalledNodeVersion /></p>
+          </div>
+          {
+            this.hasUpdate()
+            ? (
+              <div className='col-xs-12'>
+                <a href='#' id='update-to' className='version-button' onClick={this.installLatestLTS}>
+                  <NodeWhiteIcon />
+                  Update to: <span>{this.props.versions.latest.version}</span>
+                </a>
+              </div>
+            )
+            : null
+          }
+        </div>
+        <div className='spacer' />
+        <div className='row'>
+          <div className='col-xs-12'>
+            <p>Latest versions:</p>
+          </div>
+          <div className='col-xs-12'>
+            <a href='#' className='version-button' onClick={this.installLatestLTS}>
+              <NodeWhiteIcon />
+              Install stable: <span>{loadingVersionList ? <SpinnerIcon /> : this.props.versions.latestLTS.version}</span>
+            </a>
+          </div>
+          <div className='col-xs-12'>
+            <a href='#' className='version-button' onClick={this.installLatest}>
+              <NodeWhiteIcon />
+              Install current: <span>{loadingVersionList ? <SpinnerIcon /> : this.props.versions.latest.version}</span>
+            </a>
+          </div>
+        </div>
+      </div>
+    )
+  }
+}
diff --git a/src/renderer/components/RandomExample.jsx b/src/renderer/components/RandomExample.jsx
new file mode 100644
index 0000000..382be20
--- /dev/null
+++ b/src/renderer/components/RandomExample.jsx
@@ -0,0 +1,39 @@
+import React from 'react'
+import { PrismCode } from 'react-prism'
+
+import getExample from '../lib/examples'
+
+export class RandomExample extends React.Component {
+  state = {
+    example: null
+  }
+
+  componentDidMount () {
+    getExample((err, example) => {
+      // Not possible to get err currently
+      if (err) return
+
+      this.setState({
+        example
+      })
+    })
+  }
+
+  render () {
+    if (!this.state.example) {
+      return null
+    }
+    return (
+      <div className='row'>
+        <div className='col-xs-12'>
+          <h4 id='code-title' className='title color-green'>{this.state.example.title}</h4>
+          <pre>
+            <PrismCode className='language-javascript'>
+              {this.state.example.code}
+            </PrismCode>
+          </pre>
+        </div>
+      </div>
+    )
+  }
+}
diff --git a/src/renderer/components/RightPanel.jsx b/src/renderer/components/RightPanel.jsx
new file mode 100644
index 0000000..b266f33
--- /dev/null
+++ b/src/renderer/components/RightPanel.jsx
@@ -0,0 +1,35 @@
+import { shell } from 'electron'
+import React from 'react'
+
+import { RandomExample } from './RandomExample'
+import { NodeSchoolLogo } from './images/NodeSchoolLogo'
+
+export class RightPanel extends React.Component {
+  launchNodeSchoolWebsite (event) {
+    shell.openExternal('https://nodeschool.io/')
+    event.preventDefault()
+  }
+
+  render () {
+    return (
+      <div className='right-panel'>
+        <div className='row'>
+          <div className='col-xs-12 school-image-container'>
+            <NodeSchoolLogo />
+          </div>
+        </div>
+        <div className='row'>
+          <div className='col-xs-12'>
+            <h3 className='title color-green'>Educational resources:</h3>
+            <p className='text'>
+              Open source workshops that teach web software skills.<br />
+              Do them on your own or at a workshop nearby.<br />
+              <a href='#' onClick={this.launchNodeSchoolWebsite}>https://nodeschool.io/</a>
+            </p>
+          </div>
+        </div>
+        <RandomExample />
+      </div>
+    )
+  }
+}
diff --git a/src/renderer/components/icons/Error.jsx b/src/renderer/components/icons/Error.jsx
new file mode 100644
index 0000000..810db95
--- /dev/null
+++ b/src/renderer/components/icons/Error.jsx
@@ -0,0 +1,8 @@
+import React from 'react'
+import FontAwesome from 'react-fontawesome'
+
+export class Error extends React.Component {
+  render () {
+    return <FontAwesome name='exclamation-triangle' />
+  }
+}
diff --git a/src/renderer/components/icons/NodeWhite.jsx b/src/renderer/components/icons/NodeWhite.jsx
new file mode 100644
index 0000000..521842e
--- /dev/null
+++ b/src/renderer/components/icons/NodeWhite.jsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import path from 'path'
+
+const logoPath = path.resolve(__dirname, '../../../../static/images/lgoonodejswhite.png')
+
+export class NodeWhite extends React.Component {
+  render () {
+    return <img className='node-white-icon' src={logoPath} alt='' />
+  }
+}
diff --git a/src/renderer/components/icons/Spinner.jsx b/src/renderer/components/icons/Spinner.jsx
new file mode 100644
index 0000000..d5c42aa
--- /dev/null
+++ b/src/renderer/components/icons/Spinner.jsx
@@ -0,0 +1,8 @@
+import React from 'react'
+import FontAwesome from 'react-fontawesome'
+
+export class Spinner extends React.Component {
+  render () {
+    return <FontAwesome name='spinner' pulse spin fixedWidth />
+  }
+}
diff --git a/src/renderer/components/icons/index.js b/src/renderer/components/icons/index.js
new file mode 100644
index 0000000..4f1bb4b
--- /dev/null
+++ b/src/renderer/components/icons/index.js
@@ -0,0 +1,3 @@
+export { Error as ErrorIcon } from './Error'
+export { NodeWhite as NodeWhiteIcon } from './NodeWhite'
+export { Spinner as SpinnerIcon } from './Spinner'
diff --git a/src/renderer/components/images/NodeLogo.jsx b/src/renderer/components/images/NodeLogo.jsx
new file mode 100644
index 0000000..69b3a06
--- /dev/null
+++ b/src/renderer/components/images/NodeLogo.jsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { func } from 'prop-types'
+import path from 'path'
+
+const logoPath = path.resolve(__dirname, '../../../../static/images/nodejs-new-white-bw.png')
+
+export class NodeLogo extends React.Component {
+  static propTypes = {
+    onClick: func
+  }
+
+  render () {
+    return <img className='node-logo' src={logoPath} alt='node logo' onClick={this.props.onClick} />
+  }
+}
diff --git a/src/renderer/components/images/NodeSchoolLogo.jsx b/src/renderer/components/images/NodeSchoolLogo.jsx
new file mode 100644
index 0000000..d2e1f0e
--- /dev/null
+++ b/src/renderer/components/images/NodeSchoolLogo.jsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { func } from 'prop-types'
+import path from 'path'
+
+const logoPath = path.resolve(__dirname, '../../../../static/images/schoolhouse.svg')
+
+export class NodeSchoolLogo extends React.Component {
+  static propTypes = {
+    onClick: func
+  }
+
+  render () {
+    return <img src={logoPath} alt='node school logo' onClick={this.props.onClick} />
+  }
+}
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 5ca855b..07a6e46 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -6,77 +6,23 @@
     <link rel="stylesheet" href="../../node_modules/flexboxgrid/dist/flexboxgrid.min.css" type="text/css">
     <link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" type="text/css">
     <link rel="stylesheet" href="../../node_modules/prismjs/themes/prism.css" type="text/css">
-    <link rel="stylesheet" type="text/css" href="../../static/style.css"/>
+    <link rel="stylesheet" type="text/css" href="./less/core.less"/>
   </head>
   <body>
-    <div id="main" class="row">
-      <div class="col-xs-4 left-panel background-green">
-        <div class="row">
-          <div class="col-xs-12 node-image-container">
-            <img id="node-logo" src="../../static/images/nodejs-new-white-bw.png" alt="node logo" onclick="require('electron').shell.openExternal('https://nodejs.org/en/')"/>
-          </div>
-        </div>
-        <div class="row">
-          <div class="col-xs-12">
-            <p id="installed-version">Installed version: <span><i class="fa fa-spinner fa-pulse fa-fw"></i></span></p>
-          </div>
-          <div class="col-xs-12">
-            <a href="#" id="update-to" class="version-button"><img src="../../static/images/lgoonodejswhite.png" alt="" /> Update to: <span></span></a>
-          </div>
-        </div>
-        <div class="row bottom">
-          <div class="col-xs-12">
-            <p>Latest versions:</p>
-          </div>
-          <div class="col-xs-12">
-            <a href="#" id="install-stable" class="version-button"><img src="../../static/images/lgoonodejswhite.png" alt="" /> Install stable: <span><i class="fa fa-spinner fa-pulse fa-fw"></i></span></a>
-          </div>
-          <div class="col-xs-12">
-            <a href="#" id="install-latest" class="version-button"><img src="../../static/images/lgoonodejswhite.png" alt="" /> Install current: <span><i class="fa fa-spinner fa-pulse fa-fw"></i></span></a>
-          </div>
-        </div>
-      </div>
-      <div class="col-xs-8 right-panel">
-        <div class="row">
-          <div class="col-xs-12 school-image-container">
-            <img src="../../static/images/schoolhouse.svg" alt="" />
-          </div>
-        </div>
-        <div class="row">
-          <div class="col-xs-12">
-            <h3 class="title color-green">Educational resources:</h3>
-            <p class="text">Open source workshops that teach web software skills.<br />Do them on your own or at a workshop nearby.<br /><a href="#" onclick="require('electron').shell.openExternal('http://nodeschool.io/')">http://nodeschool.io/</a></p>
-          </div>
-        </div>
-        <div class="row">
-          <div class="col-xs-12">
-              <h4 id="code-title" class="title color-green">A simple http file server:</h4>
-            <pre id="code-container">
-              <code id="code-example" class="language-javascript">
-              </code>
-            </pre>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div id="installing" class="row">
-      <div class="col-xs-12">
-        <h1 class="color-green">Installing Node.js</h1>
-      </div>
-      <div class="col-xs-12">
-        <div class="sk-folding-cube">
-          <div class="sk-cube1 sk-cube"></div>
-          <div class="sk-cube2 sk-cube"></div>
-          <div class="sk-cube4 sk-cube"></div>
-          <div class="sk-cube3 sk-cube"></div>
-        </div>
-      </div>
-      <div class="col-xs-offset-3 col-xs-6 error-message">
-          <p id="error-text" class="color-red"></p>
-          <a id="error-button" href="#" class="color-red">Return to installer</a>
-      </div>
-    </div>
-    <script src="../../node_modules/prismjs/prism.js"></script>
-    <script src="./renderer.js"></script>
+    <div id="app"></div>
+    <script>
+    import React from 'react'
+    import ReactDOM from 'react-dom'
+    import 'prismjs'
+
+    const render = () => {
+      const { App } = require('./components/App')
+      ReactDOM.render(<App />, document.querySelector('#app'))
+    }
+    render()
+    if (module.hot) {
+      module.hot.accept(render)
+    }
+    </script>
   </body>
 </html>
diff --git a/src/renderer/less/core.less b/src/renderer/less/core.less
new file mode 100644
index 0000000..00b4400
--- /dev/null
+++ b/src/renderer/less/core.less
@@ -0,0 +1,44 @@
+* {
+  -webkit-user-select: none;
+  user-select: none;
+}
+
+html, body, #app, #main, #installing {
+  height: 100%;
+  width: 100%;
+  margin: 0;
+  padding: 0;
+  font-family: 'Open Sans', 'Helvetica', sans-serif;
+  font-weight: lighter;
+  background-color: #f7f7f7;
+  overflow: hidden;
+}
+
+#main {
+  display: flex;
+}
+
+a {
+  text-decoration: none;
+  color: #277cd0;
+}
+
+.color-green {
+  color: #56af57;
+}
+
+.background-green {
+  background-color: #56af57;
+}
+
+.background-red {
+  background-color: #D32F2F;
+}
+
+.color-red {
+  color: #D32F2F;
+}
+
+@import "./panels.less";
+@import "./installing-overlay.less";
+@import "./spinner.less";
\ No newline at end of file
diff --git a/src/renderer/less/installing-overlay.less b/src/renderer/less/installing-overlay.less
new file mode 100644
index 0000000..de847ef
--- /dev/null
+++ b/src/renderer/less/installing-overlay.less
@@ -0,0 +1,40 @@
+.installing {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 2;
+
+  div {
+    text-align: center;
+
+    h1 {
+      font-weight: lighter;
+      font-size: 3em;
+      margin: 100px 0;
+    }
+  }
+
+  .error-message {
+    padding: 20px;
+    border-radius: 3px;
+
+    .error-text {
+      user-select: all;
+      font-size: 1.2em;
+      margin: 0;
+      padding: 0;
+    }
+
+    .error-button {
+      display: block;
+      border: 1px #D32F2F solid;
+      border-radius: 3px;
+      padding: 10px 15px;
+      width: 200px;
+      margin: 0 auto;
+      margin-top: 30px;
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/renderer/less/left-panel.less b/src/renderer/less/left-panel.less
new file mode 100644
index 0000000..eaf8b29
--- /dev/null
+++ b/src/renderer/less/left-panel.less
@@ -0,0 +1,70 @@
+.left-panel {
+  display: flex;
+  flex-direction: column;
+  width: 320px;
+
+  > .row {
+    margin-bottom: 30px;
+  }
+
+  p {
+    color: #fff;
+    font-size: 1.2em;
+    margin: 0;
+  }
+
+  .installed-version {
+    font-size: 1.3em;
+  }
+
+  .node-image-container {
+    text-align: center;
+
+    img {
+      width: 80%;
+      height: auto;
+      cursor: pointer;
+    }
+  }
+
+  .installed-version,
+  .update-to,
+  .version-button {
+    span {
+      font-weight: normal;
+    }
+  }
+
+  .update-to {
+    display: none;
+  }
+
+  .spacer {
+    flex: 1;
+  }
+
+  .version-button {
+    display: inline-block;
+    border: 1px #fff solid;
+    border-radius: 3px;
+    padding: 5px 10px;
+    width: 250px;
+    font-size: 1.3em;
+    margin-top: 10px;
+
+    img {
+      width: 12px;
+      height: auto;
+      margin-right: 10px;
+    }
+  }
+
+  .version-button,
+  .version-button:link,
+  .version-button:visited,
+  .version-button:hover,
+  .version-button:active {
+    color: #fff;
+    text-decoration: none;
+  }
+}
\ No newline at end of file
diff --git a/src/renderer/less/panels.less b/src/renderer/less/panels.less
new file mode 100644
index 0000000..99fed88
--- /dev/null
+++ b/src/renderer/less/panels.less
@@ -0,0 +1,9 @@
+.left-panel, .right-panel {
+  height: 100%;
+  margin: 0;
+  padding: 30px 30px;
+  box-sizing: border-box;
+}
+
+@import "./left-panel.less";
+@import "./right-panel.less";
\ No newline at end of file
diff --git a/src/renderer/less/right-panel.less b/src/renderer/less/right-panel.less
new file mode 100644
index 0000000..1cba304
--- /dev/null
+++ b/src/renderer/less/right-panel.less
@@ -0,0 +1,46 @@
+.right-panel {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+
+  > .row {
+    &:last-child {
+      flex: 1;
+      > div {
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+      }
+    }
+  }
+
+  .school-image-container {
+    text-align: center;
+  }
+
+  .school-image-container img {
+    width: 280px;
+    height: auto
+  }
+
+  .title {
+    font-weight: normal;
+  }
+
+  .text {
+    line-height: 30px;
+  }
+
+  pre, code, code span {
+    -webkit-user-select: all;
+    user-select: all;
+  }
+
+  pre {
+    overflow: auto;
+    padding: 0em 1em;
+    font-size: 0.9em;
+    height: auto;
+    max-height: 320px;
+  }
+}
\ No newline at end of file
diff --git a/src/renderer/less/spinner.less b/src/renderer/less/spinner.less
new file mode 100644
index 0000000..a95d901
--- /dev/null
+++ b/src/renderer/less/spinner.less
@@ -0,0 +1,89 @@
+/* http://tobiasahlin.com/spinkit/ */
+.sk-folding-cube {
+  margin: 20px auto;
+  width: 80px;
+  height: 80px;
+  position: relative;
+  -webkit-transform: rotateZ(45deg);
+  transform: rotateZ(45deg);
+}
+
+.sk-folding-cube .sk-cube {
+  float: left;
+  width: 50%;
+  height: 50%;
+  position: relative;
+  -webkit-transform: scale(1.1);
+  -ms-transform: scale(1.1);
+  transform: scale(1.1); 
+}
+.sk-folding-cube .sk-cube:before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: #56af57;
+  -webkit-animation: sk-foldCubeAngle 2.4s infinite linear both;
+  animation: sk-foldCubeAngle 2.4s infinite linear both;
+  -webkit-transform-origin: 100% 100%;
+  -ms-transform-origin: 100% 100%;
+  transform-origin: 100% 100%;
+}
+.sk-folding-cube .sk-cube2 {
+  -webkit-transform: scale(1.1) rotateZ(90deg);
+  transform: scale(1.1) rotateZ(90deg);
+}
+.sk-folding-cube .sk-cube3 {
+  -webkit-transform: scale(1.1) rotateZ(180deg);
+  transform: scale(1.1) rotateZ(180deg);
+}
+.sk-folding-cube .sk-cube4 {
+  -webkit-transform: scale(1.1) rotateZ(270deg);
+  transform: scale(1.1) rotateZ(270deg);
+}
+.sk-folding-cube .sk-cube2:before {
+  -webkit-animation-delay: 0.3s;
+  animation-delay: 0.3s;
+}
+.sk-folding-cube .sk-cube3:before {
+  -webkit-animation-delay: 0.6s;
+  animation-delay: 0.6s; 
+}
+.sk-folding-cube .sk-cube4:before {
+  -webkit-animation-delay: 0.9s;
+  animation-delay: 0.9s;
+}
+
+@-webkit-keyframes sk-foldCubeAngle {
+  0%, 10% {
+    -webkit-transform: perspective(140px) rotateX(-180deg);
+    transform: perspective(140px) rotateX(-180deg);
+    opacity: 0; 
+  } 25%, 75% {
+    -webkit-transform: perspective(140px) rotateX(0deg);
+    transform: perspective(140px) rotateX(0deg);
+    opacity: 1; 
+  } 90%, 100% {
+    -webkit-transform: perspective(140px) rotateY(180deg);
+    transform: perspective(140px) rotateY(180deg);
+    opacity: 0; 
+  } 
+}
+
+@keyframes sk-foldCubeAngle {
+  0%, 10% {
+    -webkit-transform: perspective(140px) rotateX(-180deg);
+    transform: perspective(140px) rotateX(-180deg);
+    opacity: 0; 
+  } 25%, 75% {
+    -webkit-transform: perspective(140px) rotateX(0deg);
+    transform: perspective(140px) rotateX(0deg);
+    opacity: 1; 
+  } 90%, 100% {
+    -webkit-transform: perspective(140px) rotateY(180deg);
+    transform: perspective(140px) rotateY(180deg);
+    opacity: 0; 
+  }
+}
diff --git a/src/renderer/lib/Installer.js b/src/renderer/lib/Installer.js
new file mode 100644
index 0000000..bade589
--- /dev/null
+++ b/src/renderer/lib/Installer.js
@@ -0,0 +1,115 @@
+import { EventEmitter } from 'events'
+import fs from 'fs'
+import osenv from 'osenv'
+import path from 'path'
+import request from 'request'
+import semver from 'semver'
+import Sudoer from 'electron-sudo'
+
+export class Installer extends EventEmitter {
+  constructor (version) {
+    super()
+    this._rawVersion = version
+    this.version = semver.valid(version)
+    this._cancel = false
+  }
+
+  _emitError (msg) {
+    this.emit('error', new Error(msg))
+  }
+
+  _getDownloadAndInstallInfo () {
+    const base = 'https://nodejs.org/dist'
+
+    switch (process.platform) {
+      case 'darwin':
+        return {
+          url: version => `${base}/v${version}/node-v${version}.pkg`,
+          install: path => `installer -pkg ${path} -target /`
+        }
+      case 'linux':
+        return {
+          url: version => `${base}/v${version}/node-v${version}-linux-${process.arch}.tar.gz`,
+          install: path => `tar --strip=1 -C /usr/local -oxf ${path}`
+        }
+      case 'win32':
+        return {
+          url: version => `${base}/v${version}/node-v${version}-${process.arch}.msi`,
+          install: path => `msiexec /qb /i ${path}`
+        }
+      default:
+        return null
+    }
+  }
+
+  cancel () {
+    this._cancel = true
+    this.emit('cancel')
+  }
+
+  install () {
+    const info = this._getDownloadAndInstallInfo()
+    if (!info) {
+      return this._emitError(`The installer doesn't current support the ${process.platform} platform`)
+    }
+    if (!this.version) {
+      return this._emitError(`The provided version: ${this._rawVersion} is not a valid version`)
+    }
+    const downloadUrl = info.url(this.version)
+    const fileName = path.basename(downloadUrl)
+    const downloadPath = path.resolve(osenv.tmpdir(), fileName)
+
+    const cleanup = () => fs.unlink(downloadPath, () => {})
+
+    const file = fs.createWriteStream(downloadPath)
+
+    this.on('cancel', () => {
+      try {
+        file.close()
+      } catch (err) {
+        // Ignore
+      }
+    })
+
+    request(downloadUrl)
+      .on('error', (err) => {
+        fs.unlink(downloadPath)
+        this.emit('error', err)
+      })
+      .pipe(file)
+      // TODO: Emit download progress to show progress on screen
+      .on('close', () => {
+        if (this._cancel) return
+
+        const installCommand = info.install(downloadPath)
+
+        const sudoOptions = {
+          name: 'Install Node',
+          process: {
+            on: (ps) => {
+              ps.stdout.pipe(process.stdout)
+              ps.stderr.pipe(process.stderr)
+            }
+          }
+        }
+
+        const sudo = new Sudoer(sudoOptions)
+        if (process.platform === 'win32') {
+          // Safety wipe
+          const elevatePath = path.resolve(osenv.tmpdir(), 'elevate.exe')
+          if (fs.existsSync(elevatePath)) fs.unlinkSync(elevatePath)
+          sudo.bundled = path.resolve(__dirname, '../../../node_modules/electron-sudo', sudo.bundled)
+        }
+
+        sudo.exec(installCommand, sudoOptions)
+          .then(() => {
+            cleanup()
+            this.emit('done')
+          })
+          .catch((err) => {
+            cleanup()
+            this.emit('error', err)
+          })
+      })
+  }
+}
diff --git a/src/renderer/lib/examples.js b/src/renderer/lib/examples.js
index fa4aaf1..ae376ce 100644
--- a/src/renderer/lib/examples.js
+++ b/src/renderer/lib/examples.js
@@ -42,6 +42,5 @@ fs.readFile(file, function (err, contents) {
 `}]
 
 module.exports = (callback) => {
-  const { title, code } = examples[Math.floor(Math.random() * examples.length)]
-  callback(title, code)
+  callback(null, examples[Math.floor(Math.random() * examples.length)])
 }
diff --git a/src/renderer/lib/install.js b/src/renderer/lib/install.js
deleted file mode 100644
index e4850a7..0000000
--- a/src/renderer/lib/install.js
+++ /dev/null
@@ -1,60 +0,0 @@
-const request = require('request')
-const semver = require('semver')
-const osenv = require('osenv')
-const path = require('path')
-const once = require('once')
-const fs = require('fs')
-const sudo = require('electron-sudo')
-
-const checkNode = require('./check-node')
-
-function downloadAndInstallInfo () {
-  const base = 'https://nodejs.org/dist'
-
-  switch (process.platform) {
-    case 'darwin':
-      return {
-        url: version => `${base}/v${version}/node-v${version}.pkg`,
-        install: path => `installer -pkg ${path} -target /`
-      }
-    case 'linux':
-      return {
-        url: version => `${base}/v${version}/node-v${version}-linux-${process.arch}.tar.gz`,
-        install: path => `tar --strip=1 -C /usr/local -oxf ${path}`
-      }
-    case 'win32':
-      return {
-        url: version => `${base}/v${version}/node-v${version}-${process.arch}.msi`,
-        install: path => `msiexec /qb /i ${path}`
-      }
-  }
-}
-
-module.exports = function install (version, cb) {
-  version = semver.valid(version)
-  cb = once(cb)
-  const info = downloadAndInstallInfo()
-  const u = info.url(version)
-  const filename = path.basename(u)
-  const p = path.join(osenv.tmpdir(), filename)
-  console.log(u, p)
-  const file = fs.createWriteStream(p)
-  request(u).on('error', cb).pipe(file).on('close', () => {
-    const command = info.install(p)
-
-    const sudoOpts = {
-      name: 'Install Node',
-      // icns: '/path/to/icns/file' // (optional, only for MacOS),
-      process: {
-        on: (ps) => {
-          ps.stdout.pipe(process.stdout)
-          ps.stderr.pipe(process.stderr)
-        }
-      }
-    }
-    sudo.exec(command, sudoOpts, (err) => {
-      if (err) return cb(err, null)
-      checkNode(cb)
-    })
-  })
-}
diff --git a/src/renderer/lib/load.js b/src/renderer/lib/load.js
deleted file mode 100644
index d8f6112..0000000
--- a/src/renderer/lib/load.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// This file is required by the index.html file and will
-// be executed in the renderer process for that window.
-// All of the Node.js APIs are available in this process.
-
-const request = require('request').defaults({ json: true })
-const versions = require('./versions')
-
-module.exports = (cb) => {
-  request('https://nodejs.org/dist/index.json', (err, resp, index) => {
-    if (err) return cb(err)
-    if (resp.statusCode !== 200) {
-      return cb(new Error('Status not 200, ' + resp.statusCode))
-    }
-    cb(null, versions(index))
-  })
-}
diff --git a/src/renderer/lib/versions.js b/src/renderer/lib/versions.js
index 555a359..da4cd1a 100644
--- a/src/renderer/lib/versions.js
+++ b/src/renderer/lib/versions.js
@@ -1,4 +1,4 @@
-const semver = require('semver')
+import semver from 'semver'
 
 function forceSort (dict) {
   function sorter (v1, v2) {
@@ -11,50 +11,46 @@ function forceSort (dict) {
   return keys.map((k) => dict[k])
 }
 
-function Versions (index) {
-  this.index = index
-  this.majors = {}
-  this.lts = {}
-  this.load()
-}
+class Versions {
+  constructor (raw) {
+    this.raw = raw
+    this.majors = {}
+    this.lts = {}
+    this.load()
+  }
+
+  load () {
+    const raw = this.raw
+    for (const version of raw) {
+      const major = semver.major(version.version)
 
-Versions.prototype.load = function () {
-  const index = this.index
-  index.forEach((v) => {
-    var m = semver.major(v.version)
-    if (!this.majors[m]) this.majors[m] = {}
-    this.majors[m][v.version] = v
-    if (v.lts) {
-      if (!this.lts[m]) this.lts[m] = {}
-      this.lts[m][v.version] = v
+      if (!this.majors[major]) this.majors[major] = {}
+      this.majors[major][version.version] = version
+      if (version.lts) {
+        if (!this.lts[major]) this.lts[major] = {}
+        this.lts[major][version.version] = version
+      }
+    }
+
+    this._latest = 0
+    for (const majorN in this.majors) {
+      this.majors[majorN] = forceSort(this.majors[majorN])
+      if (parseInt(majorN, 10) > this._latest) this._latest = parseInt(majorN, 10)
+    }
+    this._latestLTS = 0
+    for (let k in this.lts) {
+      this.lts[k] = forceSort(this.lts[k])
+      if (parseInt(k) > this._latestLTS) this._latestLTS = parseInt(k)
     }
-  })
-  this._latest = 0
-  for (let k in this.majors) {
-    this.majors[k] = forceSort(this.majors[k])
-    if (parseInt(k) > this._latest) this._latest = parseInt(k)
-  }
-  this._latestLTS = 0
-  for (let k in this.lts) {
-    this.lts[k] = forceSort(this.lts[k])
-    if (parseInt(k) > this._latestLTS) this._latestLTS = parseInt(k)
   }
-}
 
-Versions.prototype.latest = function (version) {
-  let major
-  if (!version) major = this._latest
-  else major = semver.major(version)
-  if (!this.majors[major]) major = this._latest
-  return this.majors[major][0]
-}
+  get latest () {
+    return this.majors[this._latest][0]
+  }
 
-Versions.prototype.latestLTS = function (version) {
-  let major
-  if (!version) major = this._latestLTS
-  else major = semver.major(version)
-  if (!this.lts[major]) major = this._latestLTS
-  return this.lts[major][0]
+  get latestLTS () {
+    return this.lts[this._latestLTS][0]
+  }
 }
 
-module.exports = index => new Versions(index)
+module.exports = raw => new Versions(raw)
diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js
deleted file mode 100644
index 41a6b49..0000000
--- a/src/renderer/renderer.js
+++ /dev/null
@@ -1,105 +0,0 @@
-'use strict'
-/* globals alert, Prism */
-
-import semver from 'semver'
-
-import getInstalledVersion from './lib/check-node'
-import loadVersions from './lib/load'
-import installNode from './lib/install'
-import getExample from './lib/examples'
-
-// utility
-const errIcon = '<i class="fa fa-exclamation-triangle"></i>'
-const domElement = e => document.querySelector(e)
-const writeHTML = (e, h) => { domElement(e).innerHTML = h }
-const compare = (a, l) => semver.lt(a, l)
-const major = v => v.split('.')[0]
-
-const installing = {
-  run: false,
-  start () {
-    domElement('#installing').style.display = 'block'
-    this.run = true
-  },
-  done () {
-    domElement('#installing').style.display = 'none'
-    this.run = false
-  }
-}
-
-// Sets the text of the 'installed version' label
-getInstalledVersion((err, version) => {
-  writeHTML('#installed-version span', err ? errIcon : `v${version}`)
-})
-
-// Sets the text of the 'install version' buttons
-loadVersions((err, versions) => {
-  // get versions
-  const stable = versions.latestLTS().version
-  const latest = versions.latest().version
-
-  // write versions into buttons
-  writeHTML('#install-stable span', err ? errIcon : stable)
-  writeHTML('#install-latest span', err ? errIcon : latest)
-
-  if (err) return
-
-  // checks if needs to update nodejs
-  getInstalledVersion((err2, actual) => {
-    if (err2) return
-    const updateButton = domElement('#update-to')
-    // checks if stable is up to date
-    if (major(actual) === major(stable) && compare(actual, stable)) {
-      updateButton.style.display = 'inline-block'
-      updateButton.children[1].innerHTML = stable
-    // checks if latest is up to date
-    } else if (major(actual) <= major(latest) && compare(actual, latest)) {
-      updateButton.style.display = 'inline-block'
-      updateButton.children[1].innerHTML = latest
-    }
-    // event listeners are attached after that the loadVersions retrieves the node versions
-    domElement('#install-stable').addEventListener('click', installEvent)
-    domElement('#install-latest').addEventListener('click', installEvent)
-    domElement('#update-to').addEventListener('click', installEvent)
-  })
-})
-
-// Install events listener for 'install version' buttons
-function installEvent (e) {
-  if (!installing.run) {
-    installing.start()
-    // gets version number from button text
-    const version = this.children[1].innerHTML.slice(1)
-    installNode(version, (err, v) => {
-      if (err) {
-        console.log(err)
-        domElement('#installing .error-message').style.display = 'block'
-        domElement('.sk-folding-cube').style.display = 'none'
-        writeHTML('#error-text', err.message)
-        return
-      }
-      console.log('Done!', v)
-      writeHTML('#installed-version span', `v${version}`)
-      domElement('#update-to').style.display = 'none'
-      installing.done()
-    })
-  } else {
-    alert('Already performing an installation!')
-  }
-}
-
-function installErrorEvent (e) {
-  domElement('#installing .error-message').style.display = 'none'
-  domElement('.sk-folding-cube').style.display = 'block'
-  writeHTML('#error-text', '')
-  installing.done()
-}
-
-// Adds a random code example and refreshes Prism lib
-getExample((title, code) => {
-  writeHTML('#code-title', title)
-  writeHTML('#code-example', code)
-  Prism.highlightElement(domElement('#code-example'))
-})
-
-domElement('#error-button').addEventListener('click', installErrorEvent)
diff --git a/static/style.css b/static/style.css
deleted file mode 100644
index 15705d3..0000000
--- a/static/style.css
+++ /dev/null
@@ -1,290 +0,0 @@
-/***************/
-/*** GENERAL ***/
-/***************/
-
-* {
-  -webkit-user-select: none;
-  user-select: none;
-}
-
-html, body, #main, #installing {
-  height: 100%;
-  width: 100%;
-  margin: 0;
-  padding: 0;
-  font-family: 'Open Sans', 'Helvetica', sans-serif;
-  font-weight: lighter;
-  background-color: #f7f7f7;
-  overflow: hidden;
-}
-
-a {
-  text-decoration: none;
-  color: #277cd0;
-}
-
-.left-panel, .right-panel {
-  height: 100%;
-  margin: 0;
-  padding: 30px 30px;
-}
-
-.color-green {
-  color: #56af57;
-}
-
-.background-green {
-  background-color: #56af57;
-}
-
-.background-red {
-  background-color: #D32F2F;
-}
-
-.color-red {
-  color: #D32F2F;
-}
-
-/******************/
-/*** LEFT PANEL ***/
-/******************/
-
-.left-panel>.row {
-  margin-bottom: 30px;
-}
-
-.left-panel p {
-  color: #fff;
-  font-size: 1.2em;
-  margin: 0;
-}
-
-#installed-version {
-  font-size: 1.3em;
-}
-
-.node-image-container {
-  text-align: center;
-}
-
-#node-logo {
-  width: 80%;
-  height: auto;
-  cursor: pointer;
-}
-
-#installed-version span,
-#update-to span,
-#install-stable span,
-#install-latest span {
-  font-weight: normal;
-}
-
-#update-to {
-  display: none;
-}
-
-.bottom {
-  position: absolute;
-  bottom: 30px;
-}
-
-.version-button {
-  display: inline-block;
-  border: 1px #fff solid;
-  border-radius: 3px;
-  padding: 5px 10px;
-  width: 250px;
-  font-size: 1.3em;
-  margin-top: 10px;
-}
-
-.version-button img {
-  width: 12px;
-  height: auto;
-  margin-right: 10px;
-}
-
-.version-button,
-.version-button:link,
-.version-button:visited,
-.version-button:hover,
-.version-button:active {
-  color: #fff;
-  text-decoration: none;
-}
-
-/*******************/
-/*** RIGHT PANEL ***/
-/*******************/
-.school-image-container {
-  text-align: center;
-}
-
-.school-image-container img {
-  width: 280px;
-  height: auto
-}
-
-.title {
-  font-weight: normal;
-  margin-bottom: -10px;
-}
-
-.text {
-  line-height: 30px;
-}
-
-pre, code, code span {
-  -webkit-user-select: all;
-  user-select: all;
-}
-
-#code-container {
-  height: auto;
-  max-height: 320px;
-}
-
-pre[class*="language-"] {
-  overflow: scroll !important;
-  padding: 0em 1em;
-  font-size: 0.9em;
-}
-
-/******************/
-/*** INSTALLING ***/
-/******************/
-#installing {
-  position: fixed;
-  top: 0;
-  left: 0;
-  display: none;
-  z-index: 9999999;
-}
-
-#installing div {
-  text-align: center;
-}
-
-#installing div h1 {
-  font-weight: lighter;
-  font-size: 3em;
-  margin: 100px 0px;
-}
-
-#installing .error-message {
-  display: none;
-  padding: 20px;
-  border-radius: 3px;
-}
-
-#error-text {
-  -webkit-user-select: all;
-  user-select: all;
-  font-size: 1.2em;
-  margin: 0;
-  padding: 0;
-}
-
-#error-button {
-  display: block;
-  border: 1px #D32F2F solid;
-  border-radius: 3px;
-  padding: 10px 15px;
-  width: 200px;
-  margin: 0 auto;
-  margin-top: 30px;
-}
-
-/***************/
-/*** SPINNER ***/
-/***************/
-/* http://tobiasahlin.com/spinkit/ */
-.sk-folding-cube {
-  margin: 20px auto;
-  width: 80px;
-  height: 80px;
-  position: relative;
-  -webkit-transform: rotateZ(45deg);
-  transform: rotateZ(45deg);
-}
-
-.sk-folding-cube .sk-cube {
-  float: left;
-  width: 50%;
-  height: 50%;
-  position: relative;
-  -webkit-transform: scale(1.1);
-  -ms-transform: scale(1.1);
-  transform: scale(1.1); 
-}
-.sk-folding-cube .sk-cube:before {
-  content: '';
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  background-color: #56af57;
-  -webkit-animation: sk-foldCubeAngle 2.4s infinite linear both;
-  animation: sk-foldCubeAngle 2.4s infinite linear both;
-  -webkit-transform-origin: 100% 100%;
-  -ms-transform-origin: 100% 100%;
-  transform-origin: 100% 100%;
-}
-.sk-folding-cube .sk-cube2 {
-  -webkit-transform: scale(1.1) rotateZ(90deg);
-  transform: scale(1.1) rotateZ(90deg);
-}
-.sk-folding-cube .sk-cube3 {
-  -webkit-transform: scale(1.1) rotateZ(180deg);
-  transform: scale(1.1) rotateZ(180deg);
-}
-.sk-folding-cube .sk-cube4 {
-  -webkit-transform: scale(1.1) rotateZ(270deg);
-  transform: scale(1.1) rotateZ(270deg);
-}
-.sk-folding-cube .sk-cube2:before {
-  -webkit-animation-delay: 0.3s;
-  animation-delay: 0.3s;
-}
-.sk-folding-cube .sk-cube3:before {
-  -webkit-animation-delay: 0.6s;
-  animation-delay: 0.6s; 
-}
-.sk-folding-cube .sk-cube4:before {
-  -webkit-animation-delay: 0.9s;
-  animation-delay: 0.9s;
-}
-
-@-webkit-keyframes sk-foldCubeAngle {
-  0%, 10% {
-    -webkit-transform: perspective(140px) rotateX(-180deg);
-    transform: perspective(140px) rotateX(-180deg);
-    opacity: 0; 
-  } 25%, 75% {
-    -webkit-transform: perspective(140px) rotateX(0deg);
-    transform: perspective(140px) rotateX(0deg);
-    opacity: 1; 
-  } 90%, 100% {
-    -webkit-transform: perspective(140px) rotateY(180deg);
-    transform: perspective(140px) rotateY(180deg);
-    opacity: 0; 
-  } 
-}
-
-@keyframes sk-foldCubeAngle {
-  0%, 10% {
-    -webkit-transform: perspective(140px) rotateX(-180deg);
-    transform: perspective(140px) rotateX(-180deg);
-    opacity: 0; 
-  } 25%, 75% {
-    -webkit-transform: perspective(140px) rotateX(0deg);
-    transform: perspective(140px) rotateX(0deg);
-    opacity: 1; 
-  } 90%, 100% {
-    -webkit-transform: perspective(140px) rotateY(180deg);
-    transform: perspective(140px) rotateY(180deg);
-    opacity: 0; 
-  }
-}