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; - } -}