diff --git a/.travis.yml b/.travis.yml index 1fe8f706..5240eef1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ notifications: email: false sudo: false node_js: - - "0.12" + - "4.2.6" cache: directories: - node_modules @@ -12,5 +12,4 @@ before_script: script: - npm run lint - - npm test - - npm run build \ No newline at end of file + - npm test \ No newline at end of file diff --git a/README.md b/README.md index 3acf221e..f24d579e 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -[![Build Status](https://travis-ci.org/cmmcleod/coriolis.svg?branch=master)](https://travis-ci.org/cmmcleod/coriolis) [![Tasks in Ready](https://badge.waffle.io/cmmcleod/coriolis.png?label=ready&title=Ready)](https://waffle.io/cmmcleod/coriolis) [![Tasks in Progress](https://badge.waffle.io/cmmcleod/coriolis.svg?label=in%20progress&title=In%20Progress)](http://waffle.io/cmmcleod/coriolis) - +![Latest Release](https://img.shields.io/github/release/cmmcleod/coriolis.svg) [![Build Status](https://travis-ci.org/cmmcleod/coriolis.svg?branch=master)](https://travis-ci.org/cmmcleod/coriolis) [![Tasks in Ready](https://badge.waffle.io/cmmcleod/coriolis.png?label=ready&title=Ready)](https://waffle.io/cmmcleod/coriolis) [![Tasks in Progress](https://badge.waffle.io/cmmcleod/coriolis.svg?label=in%20progress&title=In%20Progress)](http://waffle.io/cmmcleod/coriolis) [![Chat to us on HipChat](https://img.shields.io/badge/HipChat-Coriolis-white.svg?style=social)](https://www.hipchat.com/gfYQiZcmy) ## About @@ -10,12 +9,12 @@ Coriolis was created using assets and imagery from Elite: Dangerous, with the pe ## Contributing -Chat to us on [HipChat](https://www.hipchat.com/gfYQiZcmy)! - Please [submit issues](https://github.com/cmmcleod/coriolis/issues), or better yet [pull requests](https://github.com/cmmcleod/coriolis/pulls) for any corrections or additions to the database or the code. ### Feature Requests, Suggestions & Bugs +Chat to us on [HipChat](https://www.hipchat.com/gfYQiZcmy)! + All such requests are managed and tracked through [issues](https://github.com/cmmcleod/coriolis/issues). An overview of these can be found [here](https://waffle.io/cmmcleod/coriolis). ## Development @@ -23,7 +22,7 @@ All such requests are managed and tracked through [issues](https://github.com/cm See the [Developer's Guide](https://github.com/cmmcleod/coriolis/wiki/Developer's-Guide) in the wiki. -### Ship and Component Database +### Ship and Module Database See the [Data wiki](https://github.com/cmmcleod/coriolis-data/wiki) for details on structure, etc. diff --git a/__tests__/fixtures/anaconda-test-detailed-export-v3.json b/__tests__/fixtures/anaconda-test-detailed-export-v3.json index 397443bd..58e63ac4 100644 --- a/__tests__/fixtures/anaconda-test-detailed-export-v3.json +++ b/__tests__/fixtures/anaconda-test-detailed-export-v3.json @@ -1,11 +1,11 @@ { "$schema": "http://cdn.coriolis.io/schemas/ship-loadout/3.json#", - "name": "Test", + "name": "Test My Ship", "ship": "Anaconda", "references": [ { "name": "Coriolis.io", - "url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test?bn=Test", + "url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test%20My%20Ship", "code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA", "shipId": "anaconda" } @@ -281,8 +281,8 @@ "unladenRange": 18.49, "fullTankRange": 18.12, "ladenRange": 16.39, - "unladenTotalRange": 73.21, - "ladenTotalRange": 66.15, + "unladenFastestRange": 73.21, + "ladenFastestRange": 66.15, "maxJumpCount": 4, "shieldStrength": 833 } diff --git a/__tests__/fixtures/valid-detailed-export.json b/__tests__/fixtures/valid-detailed-export.json index 42e0f475..e4ffe78d 100644 --- a/__tests__/fixtures/valid-detailed-export.json +++ b/__tests__/fixtures/valid-detailed-export.json @@ -117,8 +117,8 @@ "unladenRange": 32.48, "fullTankRange": 30.27, "ladenRange": 19.61, - "unladenTotalRange": 176.71, - "ladenTotalRange": 112.92, + "unladenFastestRange": 176.71, + "ladenFastestRange": 112.92, "maxJumpCount": 6, "shieldStrength": 86.49 } @@ -251,8 +251,8 @@ "unladenRange": 30.24, "fullTankRange": 28.32, "ladenRange": 19.8, - "unladenTotalRange": 164.89, - "ladenTotalRange": 114.03, + "unladenFastestRange": 164.89, + "ladenFastestRange": 114.03, "maxJumpCount": 6, "shieldStrength": 149.2 } @@ -381,8 +381,8 @@ "unladenRange": 31.71, "fullTankRange": 29.61, "ladenRange": 23.58, - "unladenTotalRange": 172.68, - "ladenTotalRange": 136.46, + "unladenFastestRange": 172.68, + "ladenFastestRange": 136.46, "maxJumpCount": 6, "shieldStrength": 86.49 } @@ -513,8 +513,8 @@ "unladenRange": 26.41, "fullTankRange": 24.97, "ladenRange": 17.36, - "unladenTotalRange": 172.04, - "ladenTotalRange": 118.55, + "unladenFastestRange": 172.04, + "ladenFastestRange": 118.55, "maxJumpCount": 7, "shieldStrength": 89.07 } @@ -655,8 +655,8 @@ "unladenRange": 25.77, "fullTankRange": 24.39, "ladenRange": 17.98, - "unladenTotalRange": 167.93, - "ladenTotalRange": 122.84, + "unladenFastestRange": 167.93, + "ladenFastestRange": 122.84, "maxJumpCount": 7, "shieldStrength": 125.07 } @@ -793,8 +793,8 @@ "unladenRange": 19.27, "fullTankRange": 18.95, "ladenRange": 15.43, - "unladenTotalRange": 67.34, - "ladenTotalRange": 54.75, + "unladenFastestRange": 67.34, + "ladenFastestRange": 54.75, "maxJumpCount": 4, "shieldStrength": 111.07 } @@ -956,8 +956,8 @@ "unladenRange": 27.1, "fullTankRange": 25.58, "ladenRange": 21.94, - "unladenTotalRange": 176.39, - "ladenTotalRange": 150.58, + "unladenFastestRange": 176.39, + "ladenFastestRange": 150.58, "maxJumpCount": 7, "shieldStrength": 253.58 } @@ -1098,8 +1098,8 @@ "unladenRange": 26.01, "fullTankRange": 25.42, "ladenRange": 17.19, - "unladenTotalRange": 90.67, - "ladenTotalRange": 61.04, + "unladenFastestRange": 90.67, + "ladenFastestRange": 61.04, "maxJumpCount": 4, "shieldStrength": 191.82 } @@ -1262,8 +1262,8 @@ "unladenRange": 15.19, "fullTankRange": 14.99, "ladenRange": 14.99, - "unladenTotalRange": 53.15, - "ladenTotalRange": 53.15, + "unladenFastestRange": 53.15, + "ladenFastestRange": 53.15, "maxJumpCount": 4, "shieldStrength": 489.6 } @@ -1362,8 +1362,8 @@ "unladenRange": 24.25, "fullTankRange": 23.73, "ladenRange": 23.73, - "unladenTotalRange": 84.56, - "ladenTotalRange": 84.56, + "unladenFastestRange": 84.56, + "ladenFastestRange": 84.56, "maxJumpCount": 4, "shieldStrength": 0 } @@ -1501,8 +1501,8 @@ "unladenRange": 19.51, "fullTankRange": 18.58, "ladenRange": 13.09, - "unladenTotalRange": 152.32, - "ladenTotalRange": 106.49, + "unladenFastestRange": 152.32, + "ladenFastestRange": 106.49, "maxJumpCount": 8, "shieldStrength": 170.57 } @@ -1639,8 +1639,8 @@ "unladenRange": 28.25, "fullTankRange": 26.6, "ladenRange": 16.67, - "unladenTotalRange": 183.67, - "ladenTotalRange": 113.69, + "unladenFastestRange": 183.67, + "ladenFastestRange": 113.69, "maxJumpCount": 7, "shieldStrength": 214.26 } @@ -1810,8 +1810,8 @@ "unladenRange": 20.32, "fullTankRange": 19.46, "ladenRange": 14.65, - "unladenTotalRange": 133.17, - "ladenTotalRange": 99.65, + "unladenFastestRange": 133.17, + "ladenFastestRange": 99.65, "maxJumpCount": 7, "shieldStrength": 486.35 } @@ -1989,8 +1989,8 @@ "unladenRange": 14.32, "fullTankRange": 13.89, "ladenRange": 13.46, - "unladenTotalRange": 94.42, - "ladenTotalRange": 91.49, + "unladenFastestRange": 94.42, + "ladenFastestRange": 91.49, "maxJumpCount": 7, "shieldStrength": 645.57 } @@ -2219,8 +2219,8 @@ "unladenRange": 16.99, "fullTankRange": 16.68, "ladenRange": 15.91, - "unladenTotalRange": 67.35, - "ladenTotalRange": 64.2, + "unladenFastestRange": 67.35, + "ladenFastestRange": 64.2, "maxJumpCount": 4, "shieldStrength": 952 } @@ -2374,8 +2374,8 @@ "unladenRange": 39.26, "fullTankRange": 37.65, "ladenRange": 21.21, - "unladenTotalRange": 153.79, - "ladenTotalRange": 85.82, + "unladenFastestRange": 153.79, + "ladenFastestRange": 85.82, "maxJumpCount": 4, "shieldStrength": 372.98 } @@ -2530,8 +2530,8 @@ "unladenRange": 38.44, "fullTankRange": 36.89, "ladenRange": 21.04, - "unladenTotalRange": 150.62, - "ladenTotalRange": 85.16, + "unladenFastestRange": 150.62, + "ladenFastestRange": 85.16, "maxJumpCount": 4, "shieldStrength": 372.98 } @@ -2698,8 +2698,8 @@ "unladenRange": 38.04, "fullTankRange": 30.11, "ladenRange": 23.03, - "unladenTotalRange": 675.7, - "ladenTotalRange": 501.97, + "unladenFastestRange": 675.7, + "ladenFastestRange": 501.97, "maxJumpCount": 20, "shieldStrength": 372.98 } @@ -2916,8 +2916,8 @@ "unladenRange": 18.49, "fullTankRange": 18.12, "ladenRange": 16.39, - "unladenTotalRange": 73.21, - "ladenTotalRange": 66.15, + "unladenFastestRange": 73.21, + "ladenFastestRange": 66.15, "maxJumpCount": 4, "shieldStrength": 833 } @@ -3044,8 +3044,8 @@ "unladenRange": 35.99, "fullTankRange": 33.36, "ladenRange": 33.36, - "unladenTotalRange": 232.28, - "ladenTotalRange": 232.28, + "unladenFastestRange": 232.28, + "ladenFastestRange": 232.28, "maxJumpCount": 7, "shieldStrength": 92.25 } @@ -3181,8 +3181,8 @@ "unladenRange": 15.06, "fullTankRange": 14.86, "ladenRange": 14.86, - "unladenTotalRange": 42.5, - "ladenTotalRange": 42.5, + "unladenFastestRange": 42.5, + "ladenFastestRange": 42.5, "maxJumpCount": 3, "shieldStrength": 548.74 } @@ -3335,8 +3335,8 @@ "unladenRange": 12.51, "fullTankRange": 12.38, "ladenRange": 12.38, - "unladenTotalRange": 35.35, - "ladenTotalRange": 35.35, + "unladenFastestRange": 35.35, + "ladenFastestRange": 35.35, "maxJumpCount": 3, "shieldStrength": 760.16 } @@ -3453,8 +3453,8 @@ "unladenRange": 17.12, "fullTankRange": 16.71, "ladenRange": 16.71, - "unladenTotalRange": 42.4, - "ladenTotalRange": 42.4, + "unladenFastestRange": 42.4, + "ladenFastestRange": 42.4, "maxJumpCount": 3, "shieldStrength": 102 } @@ -3611,8 +3611,8 @@ "unladenRange": 8.43, "fullTankRange": 8.09, "ladenRange": 7.25, - "unladenTotalRange": 81.5, - "ladenTotalRange": 72.9, + "unladenFastestRange": 81.5, + "ladenFastestRange": 72.9, "maxJumpCount": 10, "shieldStrength": 299.48 } diff --git a/__tests__/test-import.js b/__tests__/test-import.js index 01ce652a..ecc40952 100644 --- a/__tests__/test-import.js +++ b/__tests__/test-import.js @@ -10,6 +10,7 @@ import { getLanguage } from '../src/app/i18n/Language'; describe('Import Modal', function() { + let MockRouter = require('../src/app/Router'); const Persist = require('../src/app/stores/Persist').default; const ModalImport = require('../src/app/components/ModalImport').default; const mockContext = { @@ -24,12 +25,15 @@ describe('Import Modal', function() { onWindowResize: jest.genMockFunction() }; + MockRouter.go = jest.genMockFunction(); + let modal, render, ContextProvider = Utils.createContextProvider(mockContext); /** * Clear saved builds, and reset React DOM */ function reset() { + MockRouter.go.mockClear(); Persist.deleteAll(); render = TU.renderIntoDocument(); modal = TU.findRenderedComponentWithType(render, ModalImport); @@ -135,16 +139,9 @@ describe('Import Modal', function() { expect(modal.state.importValid).toBeTruthy(); expect(modal.state.errorMsg).toEqual(null); - clickProceed(); - - expect(modal.state.processed).toBeTruthy(); - - clickImport(); - - expect(Persist.getBuilds()).toEqual({ - anaconda: { 'Test': '48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA' } - }); + expect(MockRouter.go.mock.calls.length).toBe(1); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test%20My%20Ship'); }); it('catches an invalid build', function() { @@ -152,7 +149,7 @@ describe('Import Modal', function() { pasteText(JSON.stringify(importData).replace('components', 'comps')); expect(modal.state.importValid).toBeFalsy(); - expect(modal.state.errorMsg).toEqual('Anaconda Build "Test": Invalid data'); + expect(modal.state.errorMsg).toEqual('Anaconda Build "Test My Ship": Invalid data'); }); }); @@ -188,16 +185,13 @@ describe('Import Modal', function() { for (let i = 0; i < imports.length; i++ ) { reset(); - pasteText(imports[i].buildText); + let fixture = imports[i]; + pasteText(fixture.buildText); expect(modal.state.importValid).toBeTruthy(); - expect(modal.state.errorMsg).toEqual(null, 'Build #' + i + ': ' + imports[i].buildName); + expect(modal.state.errorMsg).toEqual(null); clickProceed(); - expect(modal.state.processed).toBeTruthy(); - clickImport(); - let allBuilds = Persist.getBuilds(); - let shipBuilds = allBuilds ? allBuilds[imports[i].shipId] : null; - let build = shipBuilds ? shipBuilds[imports[i].buildName] : null; - expect(build).toEqual(imports[i].buildCode, 'Build #' + i + ': ' + imports[i].buildName); + expect(MockRouter.go.mock.calls.length).toBe(1); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/' + fixture.shipId + '/' + fixture.buildCode + '?bn=' + encodeURIComponent(fixture.buildName)); } }); diff --git a/__tests__/test-persist.js b/__tests__/test-persist.js index 233d2739..0e9f98fa 100644 --- a/__tests__/test-persist.js +++ b/__tests__/test-persist.js @@ -4,32 +4,103 @@ import React from 'react'; import ReactDOM from 'react-dom'; import TU from 'react-testutils-additions'; -xdescribe('Persist', function() { +let origAddEventListener = window.addEventListener; +let storageListener; +let ls = {}; - const Persist = require('../src/app/stores/Persist').default; +// Implment mock localStorage +let localStorage = { + getItem: function(key) { + return ls[key]; + }, + setItem: function(key, value) { + ls[key] = value; + }, + removeItem: function(key) { + delete ls[key]; + }, + clear: function() { + ls = {}; + } +} + +window.addEventListener = function(eventName, listener) { + + if(eventName == 'storage') { + storageListener = listener; // Keep track of latest storage listener + } else { + origAddEventListener.apply(arguments); + } +} + +describe('Persist', function() { + + const Persist = require('../src/app/stores/Persist').Persist; describe('Builds', function() { + it("loads from localStorage correctly", function() { + }); + it("can save a build", function() { + + }); + + it("can delete a build", function() { + + }); + + it("works without localStorage", function() { + + }); }); describe('Comparisons', function() { + it("loads from localStorage correctly", function() { + }); + it("works without localStorage", function() { + + }); }); - describe('Settings', function() { + describe('Multi tab/window', function() { + it.only("syncs builds", function() { + window.localStorage = localStorage; - it("has defaults", function() { - expect(false).toBeTruthy('Implement'); + let p = new Persist(); + let newBuilds = {}; + + storageListener({ key: 'builds', newValue: JSON.stringify(newBuilds) }); + }); + }); + + describe('General and Settings', function() { + it.only("has defaults", function() { + let p = new Persist(); + expect(p.getLangCode()).toBe('en'); + expect(p.showTooltips()).toBe(true); + expect(p.getInsurance()).toBe('standard'); + expect(p.getShipDiscount()).toBe(1); + expect(p.getModuleDiscount()).toBe(1); + expect(p.getSizeRatio()).toBe(1); }); it("loads from localStorage correctly", function() { - expect(false).toBeTruthy('Implement'); + expect(false).toBeTruthy('TODO: Implement'); + }); + + it("uses defaults from a corrupted localStorage", function() { + expect(false).toBeTruthy('TODO: Implement'); + }); + + it("works without localStorage", function() { + }); it("generates the backup", function() { - expect(false).toBeTruthy('Implement'); + expect(false).toBeTruthy('TODO: Implement'); }); }); diff --git a/__tests__/test-serializer.js b/__tests__/test-serializer.js index 5c64b903..c03aff75 100644 --- a/__tests__/test-serializer.js +++ b/__tests__/test-serializer.js @@ -1,5 +1,5 @@ import Ship from '../src/app/shipyard/Ship'; -import { Ships } from 'coriolis-data'; +import { Ships } from 'coriolis-data/dist'; import * as Serializer from '../src/app/shipyard/Serializer'; import jsen from 'jsen'; @@ -11,7 +11,7 @@ describe("Serializer", function() { describe("To Detailed Build", function() { let testBuild = new Ship('anaconda', anaconda.properties, anaconda.slots).buildFrom(code); - let exportData = Serializer.toDetailedBuild('Test', testBuild); + let exportData = Serializer.toDetailedBuild('Test My Ship', testBuild); it("conforms to the v3 ship-loadout schema", function() { expect(validate(exportData)).toBe(true); diff --git a/__tests__/test-ship.js b/__tests__/test-ship.js index 69b5ff2a..de66a8c2 100644 --- a/__tests__/test-ship.js +++ b/__tests__/test-ship.js @@ -1,5 +1,5 @@ import Ship from '../src/app/shipyard/Ship'; -import { Ships } from 'coriolis-data'; +import { Ships } from 'coriolis-data/dist'; import * as ModuleUtils from '../src/app/shipyard/ModuleUtils'; describe("Ship Factory", function() { @@ -22,8 +22,8 @@ describe("Ship Factory", function() { expect(ship.unladenRange).toBeGreaterThan(0, s + ' unladenRange'); expect(ship.ladenRange).toBeGreaterThan(0, s + ' ladenRange'); expect(ship.fuelCapacity).toBeGreaterThan(0, s + ' fuelCapacity'); - expect(ship.unladenTotalRange).toBeGreaterThan(0, s + ' unladenTotalRange'); - expect(ship.ladenTotalRange).toBeGreaterThan(0, s + ' ladenTotalRange'); + expect(ship.unladenFastestRange).toBeGreaterThan(0, s + ' unladenFastestRange'); + expect(ship.ladenFastestRange).toBeGreaterThan(0, s + ' ladenFastestRange'); expect(ship.shieldStrength).toBeGreaterThan(0, s + ' shieldStrength'); expect(ship.armour).toBeGreaterThan(0, s + ' armour'); expect(ship.topSpeed).toBeGreaterThan(0, s + ' topSpeed'); diff --git a/nginx.conf b/nginx.conf index 75916e93..612029a2 100644 --- a/nginx.conf +++ b/nginx.conf @@ -37,7 +37,7 @@ http { gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; server { - listen 3300; + listen 3301; server_name localhost; root ./build/; index index.html; diff --git a/package.json b/package.json index 7fed1710..021031ce 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "coriolis_shipyard", - "version": "2.0.0-alpha", + "version": "2.0.1-Beta", "repository": { "type": "git", "url": "https://github.com/cmmcleod/coriolis" }, - "homepage": "http://coriolis.io", + "homepage": "https://coriolis.io", "bugs": "https://github.com/cmmcleod/coriolis/issues", "private": true, "engine": "node >= 4.0.0", @@ -17,9 +17,8 @@ "test": "jest", "prod-serve": "nginx -p $(pwd) -c nginx.conf", "prod-stop": "kill -QUIT $(cat nginx.pid)", - "build:prod": "npm run clean && NODE_ENV=production CDN='//cdn.coriolis.io' webpack -d -p --config webpack.config.prod.js", "build": "npm run clean && NODE_ENV=production webpack -d -p --config webpack.config.prod.js", - "rsync": "rsync -e 'ssh -i $CORIOLIS_PEM' -a --delete build/ $CORIOLIS_USER@$CORIOLIS_HOST:~/www", + "rsync": "rsync -ae \"ssh -i $CORIOLIS_PEM\" --delete build/ $CORIOLIS_USER@$CORIOLIS_HOST:~/wwws", "deploy": "npm run lint && npm test && npm run build:prod && npm run rsync" }, "jest": { @@ -73,7 +72,7 @@ "json-loader": "^0.5.3", "less": "^2.5.3", "less-loader": "^2.2.1", - "react-addons-test-utils": "^0.14.6", + "react-addons-test-utils": "^0.14.7", "react-testutils-additions": "^0.16.0", "rimraf": "^2.4.3", "style-loader": "^0.13.0", @@ -84,11 +83,12 @@ "dependencies": { "babel-polyfill": "^6.3.14", "classnames": "^2.2.0", + "coriolis-data": "cmmcleod/coriolis-data", "d3": "^3.5.9", "fbemitter": "^2.0.0", "lz-string": "^1.4.4", - "react": "^0.14.6", - "react-dom": "^0.14.6", + "react": "^0.14.7", + "react-dom": "^0.14.7", "superagent": "^1.4.0" } } diff --git a/src/app/Coriolis.jsx b/src/app/Coriolis.jsx index d6f41e09..e5d1a77c 100644 --- a/src/app/Coriolis.jsx +++ b/src/app/Coriolis.jsx @@ -6,12 +6,14 @@ import Persist from './stores/Persist'; import Header from './components/Header'; import Tooltip from './components/Tooltip'; +import ModalImport from './components/ModalImport'; import AboutPage from './pages/AboutPage'; import NotFoundPage from './pages/NotFoundPage'; import OutfittingPage from './pages/OutfittingPage'; import ComparisonPage from './pages/ComparisonPage'; import ShipyardPage from './pages/ShipyardPage'; +import ErrorDetails from './pages/ErrorDetails'; /** * Coriolis App @@ -28,7 +30,8 @@ export default class Coriolis extends React.Component { hideModal: React.PropTypes.func.isRequired, tooltip: React.PropTypes.func.isRequired, termtip: React.PropTypes.func.isRequired, - onWindowResize: React.PropTypes.func.isRequired + onWindowResize: React.PropTypes.func.isRequired, + onCommand: React.PropTypes.func.isRequired }; /** @@ -44,6 +47,7 @@ export default class Coriolis extends React.Component { this._tooltip = this._tooltip.bind(this); this._termtip = this._termtip.bind(this); this._onWindowResize = this._onWindowResize.bind(this); + this._onCommand = this._onCommand.bind(this); this._onLanguageChange = this._onLanguageChange.bind(this); this._onSizeRatioChange = this._onSizeRatioChange.bind(this); this._keyDown = this._keyDown.bind(this); @@ -70,12 +74,15 @@ export default class Coriolis extends React.Component { * @param {Object} route The current route */ _setPage(page, route) { - this.setState({ page, route, currentMenu: null }); + this.setState({ page, route, currentMenu: null, modal: null, error: null }); } /** - * Handle unexpected error - * TODO: Implement and fix to work with Webpack (dev + prod) + * Handle unexpected error. This is most likely an unhandled React Error which + * is also most likely unrecoverable. The best option is to catch as many details + * as possible so the user can report the error and provide a link to reload the page + * to reset the VM and clear any error state. + * * @param {string} msg Message * @param {string} scriptUrl URL * @param {number} line Line number @@ -83,8 +90,16 @@ export default class Coriolis extends React.Component { * @param {Object} errObj Error Object */ _onError(msg, scriptUrl, line, col, errObj) { - console.log('WINDOW ERROR', arguments); - // this._setPage(
Some errors occured!!
); + console && console.error && console.error(arguments); // eslint-disable-line no-console + this.setState({ + error: , + page: null, + currentMenu: null, + modal: null + }); + // TODO: Improve in the event of React Errors + // Potentially ReactDOM.render into dom here instead + // ReactDOM.render(this, document.getElementById('coriolis')); } /** @@ -108,11 +123,23 @@ export default class Coriolis extends React.Component { * @param {Event} e Keyboard Event */ _keyDown(e) { + // .keyCode will eventually be replaced with .key switch (e.keyCode) { - case 27: + case 27: // Escape Key this._hideModal(); this._closeMenu(); break; + case 73: // 'i' + if (e.ctrlKey || e.metaKey) { // CTRL/CMD + i + e.preventDefault(); + this._showModal(); + } + break; + case 101010: // 's' + if (e.ctrlKey || e.metaKey) { // CTRL/CMD + i + e.preventDefault(); + this.emitter.emit('command', 'save'); + } } } @@ -121,7 +148,7 @@ export default class Coriolis extends React.Component { * @param {React.Component} content Modal Content */ _showModal(content) { - let modal =
this._hideModal() }>{content}
; + let modal =
this._hideModal() }>{content}
; this.setState({ modal }); } @@ -169,16 +196,20 @@ export default class Coriolis extends React.Component { /** * Show the term tip - * @param {string} term Term - * @param {[type]} orientation Tooltip orientation (n,e,s,w) + * @param {string} term Term or Phrase + * @param {Object} opts Options - dontCap, orientation (n,e,s,w) * @param {SyntheticEvent} event Event */ - _termtip(term, orientation, event) { - if (typeof orientation != 'string') { - event = orientation; - orientation = null; + _termtip(term, opts, event) { + if (opts && opts.nativeEvent) { // Opts is a SyntheticEvent + event = opts; + opts = { cap: true }; } - this._tooltip(
{this.state.language.translate(term)}
, event.currentTarget.getBoundingClientRect(), { orientation }); + this._tooltip( +
{this.state.language.translate(term)}
, + event.currentTarget.getBoundingClientRect(), + opts + ); } /** @@ -190,6 +221,15 @@ export default class Coriolis extends React.Component { return this.emitter.addListener('windowResize', listener); } + /** + * Add a listener to global commands such as save, + * @param {Function} listener Listener callback + * @return {Object} Subscription token + */ + _onCommand(listener) { + return this.emitter.addListener('command', listener); + } + /** * Creates the context to be passed down to pages / components containing * language, sizeRatio and route references @@ -206,7 +246,8 @@ export default class Coriolis extends React.Component { hideModal: this._hideModal, tooltip: this._tooltip, termtip: this._termtip, - onWindowResize: this._onWindowResize + onWindowResize: this._onWindowResize, + onCommand: this._onCommand }; } @@ -238,9 +279,9 @@ export default class Coriolis extends React.Component { * @return {React.Component} The main app */ render() { - return
+ return
- { this.state.page ? : } + { this.state.error ? this.state.error : this.state.page ? : } { this.state.modal } { this.state.tooltip }
; diff --git a/src/app/Router.js b/src/app/Router.js index ac16e5a9..17a0dce5 100644 --- a/src/app/Router.js +++ b/src/app/Router.js @@ -151,7 +151,7 @@ function Context(path, state) { this.querystring.split('&').forEach((str) =>{ let query = str.split('='); - this.params[query[0]] = decodeURIComponent(query[1]); + this.params[query[0]] = decodeURIComponent(query[1]); }, this); } diff --git a/src/app/components/ActiveLink.jsx b/src/app/components/ActiveLink.jsx index ab472802..5b6b2d15 100644 --- a/src/app/components/ActiveLink.jsx +++ b/src/app/components/ActiveLink.jsx @@ -9,7 +9,7 @@ import cn from 'classnames'; * @return {boolean} If matches */ function isActive(href) { - return encodeURI(href) == (window.location.pathname + window.location.search); + return href == (window.location.pathname + window.location.search); } /** @@ -22,13 +22,12 @@ export default class ActiveLink extends Link { * @return {React.Component} The active link */ render() { - let action = this.handler.bind(this); let className = this.props.className; if (isActive(this.props.href)) { className = cn(className, 'active'); } - return {this.props.children}; + return {this.props.children}; } } \ No newline at end of file diff --git a/src/app/components/AvailableModulesMenu.jsx b/src/app/components/AvailableModulesMenu.jsx index 4f3cae16..c68cbc47 100644 --- a/src/app/components/AvailableModulesMenu.jsx +++ b/src/app/components/AvailableModulesMenu.jsx @@ -5,6 +5,8 @@ import { stopCtxPropagation } from '../utils/UtilityFunctions'; import cn from 'classnames'; import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; +const PRESS_THRESHOLD = 5000; // mouse/touch down threshold + /** * Available modules menu */ @@ -31,20 +33,19 @@ export default class AvailableModulesMenu extends TranslatedComponent { constructor(props, context) { super(props); this._hideDiff = this._hideDiff.bind(this); - this._diffMove = this._diffMove.bind(this); - this.state = { list: this._initList(props, context) }; + this.state = this._initState(props, context); } /** * Initiate the list of available moduels * @param {Object} props React Component properties * @param {Object} context React Component context - * @return {Array} Array of React Components + * @return {Object} list: Array of React Components, currentGroup Component if any */ - _initList(props, context) { + _initState(props, context) { let translate = context.language.translate; let { m, warning, shipMass, onSelect, modules } = props; - let list; + let list, currentGroup; let buildGroup = this._buildGroup.bind( this, translate, @@ -62,14 +63,19 @@ export default class AvailableModulesMenu extends TranslatedComponent { } else { list = []; // At present time slots with grouped options (Hardpoints and Internal) can be empty - list.push(
{translate('empty')}
); + list.push(
{translate('empty')}
); for (let g in modules) { - list.push(
{translate(g)}
); + if (m && g == m.grp) { + list.push(
this.groupElem = elem} key={g} className={'select-group cap'}>{translate(g)}
); + } else { + list.push(
{translate(g)}
); + } + list.push(buildGroup(g, modules[g])); } } - return list; + return { list, currentGroup }; } /** @@ -97,6 +103,22 @@ export default class AvailableModulesMenu extends TranslatedComponent { active, disabled }); + let eventHandlers; + + if (disabled || active) { + eventHandlers = {}; + } else { + let showDiff = this._showDiff.bind(this, mountedModule, m); + let select = onSelect.bind(null, m); + + eventHandlers = { + onMouseEnter: this._over.bind(this, showDiff), + onTouchStart: this._touchStart.bind(this, showDiff), + onTouchEnd: this._touchEnd.bind(this, select), + onMouseLeave: this._hideDiff, + onClick: select + }; + } switch(m.mount) { case 'F': mount = ; break; @@ -108,19 +130,10 @@ export default class AvailableModulesMenu extends TranslatedComponent { elems.push(
); } - let showDiff = disabled || active ? null : this._showDiff.bind(this, mountedModule, m); - elems.push( -
  • +
  • {mount} - {(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')} + {(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}
  • ); prevClass = m.class; @@ -135,22 +148,46 @@ export default class AvailableModulesMenu extends TranslatedComponent { * mounted module and the hovered modules * @param {Object} mm The module mounet currently * @param {Object} m The hovered module - * @param {SyntheticEvent} event Event + * @param {DOMRect} rect DOMRect for target element */ - _showDiff(mm, m, event) { - event.preventDefault(); + _showDiff(mm, m, rect) { if (this.props.diffDetails) { - this.context.tooltip(this.props.diffDetails(m, mm), event.currentTarget.getBoundingClientRect()); + this.touchTimeout = null; + this.context.tooltip(this.props.diffDetails(m, mm), rect); } } - _touchStart(event) { + /** + * Mouse over diff handler + * @param {Function} showDiff diff tooltip callback + * @param {SyntheticEvent} event Event + */ + _over(showDiff, event) { event.preventDefault(); - console.log(Object.assign({}, event)); + showDiff(event.currentTarget.getBoundingClientRect()); } - _diffMove(event) { - console.log(Object.assign({}, event)); + /** + * Toucch Start - Show diff after press, otherwise treat as tap + * @param {Function} showDiff diff tooltip callback + * @param {SyntheticEvent} event Event + */ + _touchStart(showDiff, event) { + let rect = event.currentTarget.getBoundingClientRect(); + this.touchTimeout = setTimeout(showDiff.bind(this, rect), PRESS_THRESHOLD); + } + + /** + * Touch End - Select module on tap + * @param {Function} select Select module callback + * @param {SyntheticEvent} event Event + */ + _touchEnd(select, event) { + if (this.touchTimeout !== null) { // If timeout has not fired (been nulled out) yet + select(); + } + event.preventDefault(); + this._hideDiff(); } /** @@ -158,18 +195,17 @@ export default class AvailableModulesMenu extends TranslatedComponent { * @param {SyntheticEvent} event Event */ _hideDiff(event) { - event.preventDefault(); + clearTimeout(this.touchTimeout); + this.touchTimeout = null; this.context.tooltip(); } /** - * Scroll to mounted (if it exists) component on mount + * Scroll to mounted (if it exists) module group on mount */ componentDidMount() { - let m = this.props.m; - - if (!(this.props.modules instanceof Array) && m && m.grp) { - findDOMNode(this).scrollTop = this.refs[m.grp].offsetTop; // Scroll to currently selected group + if (this.groupElem) { // Scroll to currently selected group + findDOMNode(this).scrollTop = this.groupElem.offsetTop; } } @@ -179,7 +215,7 @@ export default class AvailableModulesMenu extends TranslatedComponent { * @param {Object} nextContext Incoming/Next conext */ componentWillReceiveProps(nextProps, nextContext) { - this.setState({ list: this._initList(nextProps, nextContext) }); + this.setState(this._initState(nextProps, nextContext)); } /** @@ -192,9 +228,6 @@ export default class AvailableModulesMenu extends TranslatedComponent { className={cn('select', this.props.className)} onScroll={this._hideDiff} onClick={(e) => e.stopPropagation() } - onTouchStart={this._touchStart} - onTouchEnd={this._hideDiff} - onTouchCancel={this._hideDiff} onContextMenu={stopCtxPropagation} > {this.state.list} diff --git a/src/app/components/BarChart.jsx b/src/app/components/BarChart.jsx index 5789e375..a53c3d48 100644 --- a/src/app/components/BarChart.jsx +++ b/src/app/components/BarChart.jsx @@ -40,18 +40,21 @@ export default class BarChart extends TranslatedComponent { static defaultProps = { colors: ['#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c'], + labels: null, unit: '' }; static PropTypes = { - data: React.PropTypes.array.isRequired, - width: React.PropTypes.number.isRequired, - format: React.PropTypes.string.isRequired, - label: React.PropTypes.string.isRequired, - unit: React.PropTypes.string.isRequired, colors: React.PropTypes.array, + data: React.PropTypes.array.isRequired, + desc: React.PropTypes.bool, + format: React.PropTypes.string.isRequired, + labels: React.PropTypes.array, predicate: React.PropTypes.string, - desc: React.PropTypes.bool + properties: React.PropTypes.array, + title: React.PropTypes.string.isRequired, + unit: React.PropTypes.string.isRequired, + width: React.PropTypes.number.isRequired }; /** @@ -76,25 +79,30 @@ export default class BarChart extends TranslatedComponent { /** * Generate and Show tooltip - * @param {Object} build Ship build - * @param {string} property Property to display + * @param {Object} build Ship build + * @param {string} property Property to display + * @param {number} propertyIndex Property Label index */ - _showTip(build, property) { - let { unit, format } = this.props; + _showTip(build, property, propertyIndex) { + let { unit, format, labels } = this.props; let { scale, y0, y1 } = this.state; - let { formats } = this.context.language; + let { translate, formats } = this.context.language; let fontSize = parseFloat(window.getComputedStyle(document.getElementById('coriolis')).getPropertyValue('font-size') || 16); let val = build[property]; + let lblStr = labels ? translate(labels[propertyIndex]) + ': ' : ''; let valStr = formats[format](val) + ' ' + unit; - let width = (valStr.length / 1.7) * fontSize; - let midPoint = width / 2; let valMidPoint = scale(val) / 2; let y = y0(bName(build)) + y1(property) - fontSize - 5; + let width = ((lblStr.length + valStr.length) / 1.8) * fontSize; + let midPoint = width / 2; let tooltip = - {valStr} + + {lblStr} + {valStr} + ; @@ -173,12 +181,12 @@ export default class BarChart extends TranslatedComponent { return null; } - let { label, unit, width, data, properties } = this.props; + let { title, unit, width, data, properties } = this.props; let { innerWidth, outerHeight, innerHeight, y0, y1, scale, color, tooltip } = this.state; let bars = data.map((build, i) => - { properties.map((p) => + { properties.map((p, propIndex) => )} @@ -199,7 +207,7 @@ export default class BarChart extends TranslatedComponent { {tooltip} d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> - {label} + {title} { unit ? {` (${unit})`} : null } diff --git a/src/app/components/ComparisonTable.jsx b/src/app/components/ComparisonTable.jsx index cbe63663..ca99d307 100644 --- a/src/app/components/ComparisonTable.jsx +++ b/src/app/components/ComparisonTable.jsx @@ -38,8 +38,8 @@ export default class ComparisonTable extends TranslatedComponent { */ _buildHeaders(facets, onSort, translate) { let header = [ - {translate('ship')}, - {translate('build')} + {translate('ship')}, + {translate('build')} ]; let subHeader = []; @@ -47,13 +47,13 @@ export default class ComparisonTable extends TranslatedComponent { if (f.active) { let p = f.props; let pl = p.length; - header.push( + header.push( {translate(f.title)} ); if (pl > 1) { for (let i = 0; i < pl; i++) { - subHeader.push({translate(f.lbls[i])}); + subHeader.push({translate(f.lbls[i])}); } } } diff --git a/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx index e742ab10..314b5278 100644 --- a/src/app/components/CostSection.jsx +++ b/src/app/components/CostSection.jsx @@ -1,6 +1,6 @@ import React from 'react'; import cn from 'classnames'; -import { Ships } from 'coriolis-data'; +import { Ships } from 'coriolis-data/dist'; import Persist from '../stores/Persist'; import Ship from '../shipyard/Ship'; import { Insurance } from '../shipyard/Constants'; @@ -46,7 +46,6 @@ export default class CostSection extends TranslatedComponent { retrofitName, shipDiscount, moduleDiscount, - total: props.ship.totalCost, insurance: Insurance[Persist.getInsurance()], tab: Persist.getCostTab(), buildOptions: Persist.getBuildsNamesFor(props.ship.id), @@ -108,6 +107,7 @@ export default class CostSection extends TranslatedComponent { let moduleDiscount = Persist.getModuleDiscount(); this.props.ship.applyDiscounts(shipDiscount, moduleDiscount); this.state.retrofitShip.applyDiscounts(shipDiscount, moduleDiscount); + this._updateRetrofit(this.props.ship, this.state.retrofitShip); this.setState({ shipDiscount, moduleDiscount }); } @@ -137,32 +137,28 @@ export default class CostSection extends TranslatedComponent { } /** - * On build save - * @param {string} shipId Ship Id - * @param {string} name Build name - * @param {string} code Serialized ship 'code' + * On builds changed check to see if the retrofit ship needs + * to be updated */ - _onBuildSaved(shipId, name, code) { - if(this.state.retrofitName == name) { - this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build - this._updateRetrofit(this.props.ship, this.state.retrofitShip); - } else { - this.setState({ buildOptions: Persist.getBuildsNamesFor(this.props.shipId) }); - } - } + _onBuildsChanged() { + let update = false; + let ship = this.props.ship; + let { retrofitName, retrofitShip } = this.state; - /** - * On build deleted - * @param {string} shipId Ship Id - * @param {string} name Build name - * @param {string} code Serialized ship 'code' - */ - _onBuildDeleted(shipId, name, code) { - if(this.state.retrofitName == name) { - this.state.retrofitShip.buildWith(Ships[shipId].defaults); // Retrofit ship becomes stock build + if(!Persist.hasBuild(ship.id, retrofitName)) { + retrofitShip.buildWith(Ships[ship.id].defaults); // Retrofit ship becomes stock build this.setState({ retrofitName: null }); + update = true; + } else if (Persist.getBuild(ship.id, retrofitName) != retrofitShip.toString()) { + retrofitShip.buildFrom(Persist.getBuild(ship.id, retrofitName)); // Repopulate modules from saved build + update = true; } - this.setState({ buildOptions: Persist.getBuildsNamesFor(shipId) }); + + if (update) { // Update retrofit comparison + this._updateRetrofit(ship, retrofitShip); + } + // Update list of retrofit base build options + this.setState({ buildOptions: Persist.getBuildsNamesFor(ship.id) }); } /** @@ -171,7 +167,7 @@ export default class CostSection extends TranslatedComponent { */ _toggleCost(item) { this.props.ship.setCostIncluded(item, !item.incCost); - this.setState({ total: this.props.ship.totalCost }); + this.forceUpdate(); } /** @@ -286,7 +282,7 @@ export default class CostSection extends TranslatedComponent { */ _costsTab() { let { ship } = this.props; - let { total, shipDiscount, moduleDiscount, insurance } = this.state; + let { shipDiscount, moduleDiscount, insurance } = this.state; let { translate, formats, units } = this.context.language; let rows = []; @@ -295,9 +291,9 @@ export default class CostSection extends TranslatedComponent { if (item.m && item.m.cost) { let toggle = this._toggleCost.bind(this, item); rows.push( - {item.m.class + item.m.rating} - {slotName(translate, item)} - {formats.int(item.discountedCost)}{units.CR} + {item.m.class + item.m.rating} + {slotName(translate, item)} + {formats.int(item.discountedCost)}{units.CR} ); } } @@ -306,23 +302,23 @@ export default class CostSection extends TranslatedComponent { - - + {rows} - + - +
    + {translate('component')} {shipDiscount < 1 && {`[${translate('ship')} -${formats.pct1(1 - shipDiscount)}]`}} {moduleDiscount < 1 && {`[${translate('modules')} -${formats.pct1(1 - moduleDiscount)}]`}} {translate('credits')}{translate('credits')}
    {translate('total')}{formats.int(total)}{units.CR}{formats.int(ship.totalCost)}{units.CR}
    {translate('insurance')}{formats.int(total * insurance)}{units.CR}{formats.int(ship.totalCost * insurance)}{units.CR}
    @@ -346,12 +342,12 @@ export default class CostSection extends TranslatedComponent { if (retrofitCosts.length) { for (let i = 0, l = retrofitCosts.length; i < l; i++) { let item = retrofitCosts[i]; - rows.push( - {item.sellClassRating} - {translate(item.sellName)} - {item.buyClassRating} - {translate(item.buyName)} - 0 ? 'warning' : 'secondary-disabled' : 'disabled')}>{int(item.netCost)}{units.CR} + rows.push( + {item.sellClassRating} + {translate(item.sellName)} + {item.buyClassRating} + {translate(item.buyName)} + 0 ? 'warning' : 'secondary-disabled' : 'disabled')}>{int(item.netCost)}{units.CR} ); } } else { @@ -363,11 +359,11 @@ export default class CostSection extends TranslatedComponent { - - - + + @@ -473,10 +469,10 @@ export default class CostSection extends TranslatedComponent {
    {translate('sell')}{translate('buy')} + {translate('sell')}{translate('buy')} {translate('net cost')} - {moduleDiscount < 1 && {`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}} + {moduleDiscount < 1 && {`[${translate('modules')} -${formats.pct1(1 - moduleDiscount)}]`}}
    - - - - + + + + @@ -583,8 +579,7 @@ export default class CostSection extends TranslatedComponent { this.listeners = [ Persist.addListener('discounts', this._onDiscountChanged.bind(this)), Persist.addListener('insurance', this._onInsuranceChanged.bind(this)), - Persist.addListener('buildSaved', this._onBuildSaved.bind(this)), - Persist.addListener('buildDeleted', this._onBuildDeleted.bind(this)) + Persist.addListener('builds', this._onBuildsChanged.bind(this)), ]; this._updateAmmoCosts(this.props.ship); this._updateRetrofit(this.props.ship, this.state.retrofitShip); @@ -613,7 +608,6 @@ export default class CostSection extends TranslatedComponent { if (nextProps.ship != this.props.ship || nextProps.code != this.props.code) { this._updateAmmoCosts(nextProps.ship); this._updateRetrofit(nextProps.ship, retrofitShip); - this.setState({ total: nextProps.ship.totalCost }); this._sortCost(nextProps.ship); } } @@ -673,9 +667,9 @@ export default class CostSection extends TranslatedComponent {
    {translate('module')}{translate('qty')}{translate('unit cost')}{translate('total cost')}{translate('module')}{translate('qty')}{translate('unit cost')}{translate('total cost')}
    - - - + + +
    {translate('costs')}{translate('retrofit costs')}{translate('reload costs')}{translate('costs')}{translate('retrofit costs')}{translate('reload costs')}
    diff --git a/src/app/components/HardpointsSlotSection.jsx b/src/app/components/HardpointsSlotSection.jsx index 6fa84257..7591d099 100644 --- a/src/app/components/HardpointsSlotSection.jsx +++ b/src/app/components/HardpointsSlotSection.jsx @@ -91,39 +91,39 @@ export default class HardpointsSlotSection extends SlotSection { _getSectionMenu(translate) { let _fill = this._fill; - return
    e.stopPropagation()} onContextMenu={stopCtxPropagation}> + return
    e.stopPropagation()} onContextMenu={stopCtxPropagation}>
      -
    • {translate('empty all')}
    • +
    • {translate('empty all')}
    {translate('pl')}
      -
    • -
    • -
    • +
    • +
    • +
    {translate('ul')}
      -
    • -
    • -
    • +
    • +
    • +
    {translate('bl')}
      -
    • -
    • -
    • +
    • +
    • +
    {translate('mc')}
      -
    • -
    • -
    • +
    • +
    • +
    {translate('c')}
      -
    • -
    • -
    • +
    • +
    • +
    ; } diff --git a/src/app/components/Header.jsx b/src/app/components/Header.jsx index 4086620e..f08c7017 100644 --- a/src/app/components/Header.jsx +++ b/src/app/components/Header.jsx @@ -6,13 +6,14 @@ import Link from './Link'; import ActiveLink from './ActiveLink'; import cn from 'classnames'; import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons'; -import { Ships } from 'coriolis-data'; +import { Ships } from 'coriolis-data/dist'; import Persist from '../stores/Persist'; import { toDetailedExport } from '../shipyard/Serializer'; import ModalDeleteAll from './ModalDeleteAll'; import ModalExport from './ModalExport'; import ModalImport from './ModalImport'; import Slider from './Slider'; +import { outfitURL } from '../utils/UrlGenerators'; const SIZE_MIN = 0.65; const SIZE_RANGE = 0.55; @@ -181,11 +182,11 @@ export default class Header extends TranslatedComponent { let shipList = []; for (let s in Ships) { - shipList.push({Ships[s].properties.name}); + shipList.push({Ships[s].properties.name}); } return ( -
    e.stopPropagation() }> +
    e.stopPropagation() }> {shipList}
    ); @@ -203,7 +204,7 @@ export default class Header extends TranslatedComponent { let shipBuilds = []; let buildNameOrder = Object.keys(builds[shipId]).sort(); for (let buildName of buildNameOrder) { - let href = ['/outfit/', shipId, '/', builds[shipId][buildName], '?bn=', buildName].join(''); + let href = outfitURL(shipId, builds[shipId][buildName], buildName); shipBuilds.push(
  • {buildName}
  • ); } buildList.push(
      {Ships[shipId].properties.name}{shipBuilds}
    ); @@ -211,7 +212,7 @@ export default class Header extends TranslatedComponent { } return ( -
    e.stopPropagation() }> +
    e.stopPropagation() }>
    {buildList}
    ); @@ -237,10 +238,10 @@ export default class Header extends TranslatedComponent { } return ( -
    e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}> +
    e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}> {comparisons}
    - {translate('compare all')} + {translate('compare all')} {translate('create new')}
    ); @@ -255,14 +256,14 @@ export default class Header extends TranslatedComponent { let tips = Persist.showTooltips(); return ( -
    e.stopPropagation() }> +
    e.stopPropagation() }>
    {translate('language')}
    - + {translate('tooltips')}
    {(tips ? '✓' : '✗')}
    @@ -285,10 +286,10 @@ export default class Header extends TranslatedComponent {

    @@ -299,7 +300,7 @@ export default class Header extends TranslatedComponent { - +
    A
    {translate('reset')}{translate('reset')}
    @@ -313,13 +314,13 @@ export default class Header extends TranslatedComponent { * Add listeners on mount */ componentWillMount() { - Persist.addListener('language', () => this.forceUpdate()); - Persist.addListener('insurance', () => this.forceUpdate()); - Persist.addListener('discounts', () => this.forceUpdate()); - Persist.addListener('deletedAll', () => this.forceUpdate()); - Persist.addListener('buildSaved', () => this.forceUpdate()); - Persist.addListener('buildDeleted', () => this.forceUpdate()); - Persist.addListener('tooltips', () => this.forceUpdate()); + let update = () => this.forceUpdate(); + Persist.addListener('language', update); + Persist.addListener('insurance', update); + Persist.addListener('discounts', update); + Persist.addListener('deletedAll', update); + Persist.addListener('builds', update); + Persist.addListener('tooltips', update); } /** @@ -347,36 +348,36 @@ export default class Header extends TranslatedComponent { let hasBuilds = Persist.hasBuilds(); if (this.props.appCacheUpdate) { - return
    window.location.reload() }>{translate('PHRASE_UPDATE_RDY')}
    ; + return
    window.location.reload() }>{translate('PHRASE_UPDATE_RDY')}
    ; } return (
    - +
    -
    +
    {' ' + translate('ships')}
    {openedMenu == 's' ? this._getShipsMenu() : null}
    -
    +
    {' ' + translate('builds')}
    {openedMenu == 'b' ? this._getBuildsMenu() : null}
    -
    +
    {' ' + translate('compare')}
    {openedMenu == 'comp' ? this._getComparisonsMenu() : null}
    -
    +
    {translate('settings')}
    {openedMenu == 'settings' ? this._getSettingsMenu() : null} diff --git a/src/app/components/InternalSlotSection.jsx b/src/app/components/InternalSlotSection.jsx index 9dd5ae0e..ca9d197c 100644 --- a/src/app/components/InternalSlotSection.jsx +++ b/src/app/components/InternalSlotSection.jsx @@ -132,12 +132,12 @@ export default class InternalSlotSection extends SlotSection { * @return {React.Component} Section menu */ _getSectionMenu(translate) { - return
    e.stopPropagation()} onContextMenu={stopCtxPropagation}> + return
    e.stopPropagation()} onContextMenu={stopCtxPropagation}>
      -
    • {translate('empty all')}
    • -
    • {translate('cargo')}
    • -
    • {translate('scb')}
    • -
    • {translate('hr')}
    • +
    • {translate('empty all')}
    • +
    • {translate('cargo')}
    • +
    • {translate('scb')}
    • +
    • {translate('hr')}
    ; } diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx index bfafada0..15823ec7 100644 --- a/src/app/components/LineChart.jsx +++ b/src/app/components/LineChart.jsx @@ -3,7 +3,7 @@ import d3 from 'd3'; import TranslatedComponent from './TranslatedComponent'; const RENDER_POINTS = 20; // Only render 20 points on the graph -const MARGIN = { top: 15, right: 15, bottom: 35, left: 60 }; +const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 }; /** * Line Chart @@ -47,7 +47,7 @@ export default class LineChart extends TranslatedComponent { this._moveTip = this._moveTip.bind(this); let markerElems = []; - let detailElems = []; + let detailElems = []; let xScale = d3.scale.linear(); let xAxisScale = d3.scale.linear(); let yScale = d3.scale.linear(); @@ -60,7 +60,7 @@ export default class LineChart extends TranslatedComponent { for(let i = 0, l = series ? series.length : 1; i < l; i++) { let yAccessor = series ? function(d) { return yScale(d[1][this]); }.bind(series[i]) : (d) => yScale(d[1]); seriesLines.push(d3.svg.line().x((d) => xScale(d[0])).y(yAccessor)); - detailElems.push(); + detailElems.push(); markerElems.push(); } @@ -71,7 +71,7 @@ export default class LineChart extends TranslatedComponent { seriesLines, detailElems, markerElems, - tipHeight: 2 + (1.25 * (series ? series.length : 0.75)) + tipHeight: 2 + (1.2 * (series ? series.length : 0.8)) }; } @@ -94,7 +94,7 @@ export default class LineChart extends TranslatedComponent { xPos = xScale(x0); // Clamp xPos - tips.selectAll('text.label.y').text(function(d, i) { + tips.selectAll('text.text-tip.y').text(function(d, i) { let yVal = series ? y0[series[i]] : y0; yTotal += yVal; return (series ? translate(series[i]) : '') + ' ' + formats.f2(yVal); @@ -110,8 +110,8 @@ export default class LineChart extends TranslatedComponent { tipWidth += 8; tips.attr('transform', 'translate(' + xPos + ',' + tipY + ')'); - tips.selectAll('text.label').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start'); - tips.selectAll('text.label.x').text(formats.f2(x0)).append('tspan').attr('class', 'metric').text(' ' + xUnit); + tips.selectAll('text.text-tip').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start'); + tips.selectAll('text.text-tip.x').text(formats.f2(x0)).append('tspan').attr('class', 'metric').text(' ' + xUnit); tips.selectAll('rect').attr('width', tipWidth + 4).attr('x', flip ? -tipWidth - 12 : 8).attr('y', 0).style('text-anchor', flip ? 'end' : 'start'); this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0)); } diff --git a/src/app/components/Link.jsx b/src/app/components/Link.jsx index e4fef0e8..5aad9f73 100644 --- a/src/app/components/Link.jsx +++ b/src/app/components/Link.jsx @@ -7,6 +7,15 @@ import { shallowEqual } from '../utils/UtilityFunctions'; */ export default class Link extends React.Component { + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + this.handler = this.handler.bind(this); + } + /** * Determine if a component should be rerendered * @param {object} nextProps Next properties @@ -21,18 +30,15 @@ export default class Link extends React.Component { * @param {SyntheticEvent} event Event */ handler(event) { - if (event.getModifierState && - (event.getModifierState('Shift') || - event.getModifierState('Alt') || - event.getModifierState('Control') || - event.getModifierState('Meta') || - event.button > 1)) { + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey || event.button > 1) { return; } - event.nativeEvent && event.preventDefault && event.preventDefault(); + event.preventDefault(); - if (this.props.href) { - Router.go(encodeURI(this.props.href)); + if (this.props.onClick) { + this.props.onClick(event); + } else if (this.props.href) { + Router.go(this.props.href); } } @@ -41,8 +47,7 @@ export default class Link extends React.Component { * @return {React.Component} A href element */ render() { - let action = this.handler.bind(this); - return {this.props.children}; + return {this.props.children}; } } \ No newline at end of file diff --git a/src/app/components/ModalCompare.jsx b/src/app/components/ModalCompare.jsx index a7bbe8af..f4c1cfef 100644 --- a/src/app/components/ModalCompare.jsx +++ b/src/app/components/ModalCompare.jsx @@ -1,6 +1,6 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; -import { Ships } from 'coriolis-data'; +import { Ships } from 'coriolis-data/dist'; import Persist from '../stores/Persist'; /** @@ -96,39 +96,47 @@ export default class ModalCompare extends TranslatedComponent { let translate = this.context.language.translate; let availableBuilds = unusedBuilds.map((build, i) => - + {build.name} {build.buildName} ); let selectedBuilds = usedBuilds.map((build, i) => - + {build.name}< td className='tl'>{build.buildName} ); - return
    e.stopPropagation() }> + return
    e.stopPropagation() }>

    {translate('PHRASE_SELECT_BUILDS')}

    - - - - {availableBuilds} - -
    {translate('available')}
    +
    +

    {translate('available')}

    +
    + + + {availableBuilds} + +
    +
    +

    ⇆

    - - - - {selectedBuilds} - -
    {translate('added')}
    +
    +

    {translate('added')}

    +
    + + + {selectedBuilds} + +
    +
    +

    - - + +
    ; } } diff --git a/src/app/components/ModalDeleteAll.jsx b/src/app/components/ModalDeleteAll.jsx index 44aa65f8..fd0eb948 100644 --- a/src/app/components/ModalDeleteAll.jsx +++ b/src/app/components/ModalDeleteAll.jsx @@ -22,11 +22,11 @@ export default class ModalDeleteAll extends TranslatedComponent { render() { let translate = this.context.language.translate; - return
    e.stopPropagation()}> + return
    e.stopPropagation()}>

    {translate('delete all')}

    {translate('PHRASE_CONFIRMATION')}

    - - + +
    ; } } diff --git a/src/app/components/ModalExport.jsx b/src/app/components/ModalExport.jsx index 9b418d9f..cfe9c46d 100644 --- a/src/app/components/ModalExport.jsx +++ b/src/app/components/ModalExport.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { findDOMNode } from 'react-dom'; import TranslatedComponent from './TranslatedComponent'; /** @@ -40,6 +41,17 @@ export default class ModalExport extends TranslatedComponent { } } + /** + * Focus on textarea and select all + */ + componentDidMount() { + let e = findDOMNode(this.refs.exportField); + if (e) { + e.focus(); + e.select(); + } + } + /** * Render the modal * @return {React.Component} Modal Content @@ -52,13 +64,13 @@ export default class ModalExport extends TranslatedComponent { description =
    {translate(this.props.description)}
    ; } - return
    e.stopPropagation() }> + return
    e.stopPropagation() }>

    {translate(this.props.title || 'Export')}

    {description}
    -