From 8227a4e361a013e1d4c231cca94389fdb20acf5c Mon Sep 17 00:00:00 2001 From: Colin McLeod Date: Thu, 21 Jan 2016 22:06:05 -0800 Subject: [PATCH] Continued porting to react, approaching beta --- .babelrc | 21 +- .../anaconda-test-detailed-export-v3.json | 12 +- .../fixtures/ed-shipyard-import-invalid.json | 0 .../fixtures/ed-shipyard-import-valid.json | 0 .../fixtures/expected-builds.json | 0 .../fixtures/old-valid-export.json | 0 .../fixtures/valid-backup.json | 0 .../fixtures/valid-detailed-export.json | 0 .../test-controller-import.js | 2 +- __tests__/test-service-serializer.js | 54 +++ .../test-ship.js | 75 ++--- package.json | 56 +++- src/app/Coriolis.jsx | 107 +++++- src/app/Router.js | 158 ++++----- src/app/components/ActiveLink.jsx | 24 +- src/app/components/AvailableModulesMenu.jsx | 153 +++++++-- src/app/components/BarChart.jsx | 211 ++++++++++++ src/app/components/ComparisonTable.jsx | 42 ++- src/app/components/CostSection.jsx | 182 ++++++++-- src/app/components/HardpointSlot.jsx | 52 ++- src/app/components/HardpointsSlotSection.jsx | 41 ++- src/app/components/Header.jsx | 232 +++++++++---- src/app/components/InternalSlot.jsx | 56 ++-- src/app/components/InternalSlotSection.jsx | 45 ++- src/app/components/LineChart.jsx | 71 +++- src/app/components/Link.jsx | 34 +- src/app/components/ModalCompare.jsx | 72 ++-- src/app/components/ModalDeleteAll.jsx | 20 +- src/app/components/ModalExport.jsx | 29 +- src/app/components/ModalImport.jsx | 131 ++++++-- src/app/components/ModalPermalink.jsx | 28 +- src/app/components/PowerBands.jsx | 102 ++++-- src/app/components/PowerManagement.jsx | 98 ++++-- src/app/components/ShipSummaryTable.jsx | 129 +++---- src/app/components/Slider.jsx | 51 ++- src/app/components/Slot.jsx | 45 ++- src/app/components/SlotSection.jsx | 134 +++++++- src/app/components/StandardSlot.jsx | 50 +-- src/app/components/StandardSlotSection.jsx | 118 +++++-- src/app/components/SvgIcons.jsx | 317 +++++++++++++++++- src/app/components/Tooltip.jsx | 89 +++++ src/app/components/TranslatedComponent.jsx | 29 +- src/app/components/UtilitySlotSection.jsx | 43 ++- src/app/components/directive-bar-chart.js | 134 -------- src/app/i18n/Language.jsx | 12 +- src/app/i18n/de.js | 2 +- src/app/i18n/en.js | 179 +--------- src/app/i18n/es.js | 2 +- src/app/i18n/fr.js | 2 +- src/app/i18n/it.js | 2 +- src/app/i18n/ru.js | 2 +- src/app/pages/AboutPage.jsx | 11 + src/app/pages/ComparisonPage.jsx | 262 +++++++++++---- src/app/pages/ErrorPage.jsx | 38 ++- src/app/pages/NotFoundPage.jsx | 11 + src/app/pages/OutfittingPage.jsx | 117 +++++-- src/app/pages/Page.jsx | 32 +- src/app/pages/ShipyardPage.jsx | 52 ++- src/app/shipyard/Calculations.js | 22 +- src/app/shipyard/Constants.js | 39 ++- src/app/shipyard/ModuleSet.js | 65 +++- src/app/shipyard/ModuleUtils.js | 114 ++++--- src/app/shipyard/Serializer.js | 140 ++++---- src/app/shipyard/Ship.js | 305 ++++++++++++----- src/app/stores/Persist.js | 119 ++++++- src/app/utils/BBCode.js | 6 +- src/app/utils/InterfaceEvents.js | 72 ---- src/app/utils/ShortenUrl.js | 22 +- src/app/utils/SlotFunctions.js | 197 ++++++++++- .../{shallowEqual.js => UtilityFunctions.js} | 17 +- src/index.html | 10 +- src/less/app.less | 2 +- src/less/chart-tooltip.less | 63 ---- src/less/colors.less | 5 + src/less/comparison.less | 15 +- src/less/select.less | 2 +- src/less/slot.less | 60 +++- src/less/tooltip.less | 68 ++++ src/less/utilities.less | 8 + .../anaconda-test-detailed-export-v1.json | 220 ------------ test/fixtures/eddb-modules.json | 1 - test/karma.conf.js | 30 -- test/tests/test-controller-outfit.js | 50 --- test/tests/test-data.js | 132 -------- test/tests/test-service-serializer.js | 60 ---- webpack.config.dev.js | 3 +- 86 files changed, 3799 insertions(+), 2019 deletions(-) rename test/fixtures/anaconda-test-detailed-export-v2.json => __tests__/fixtures/anaconda-test-detailed-export-v3.json (95%) rename {test => __tests__}/fixtures/ed-shipyard-import-invalid.json (100%) rename {test => __tests__}/fixtures/ed-shipyard-import-valid.json (100%) rename {test => __tests__}/fixtures/expected-builds.json (100%) rename {test => __tests__}/fixtures/old-valid-export.json (100%) rename {test => __tests__}/fixtures/valid-backup.json (100%) rename {test => __tests__}/fixtures/valid-detailed-export.json (100%) rename {test/tests => __tests__}/test-controller-import.js (99%) create mode 100644 __tests__/test-service-serializer.js rename test/tests/test-factory-ship.js => __tests__/test-ship.js (67%) create mode 100644 src/app/components/BarChart.jsx create mode 100644 src/app/components/Tooltip.jsx delete mode 100755 src/app/components/directive-bar-chart.js delete mode 100644 src/app/utils/InterfaceEvents.js rename src/app/utils/{shallowEqual.js => UtilityFunctions.js} (62%) delete mode 100755 src/less/chart-tooltip.less create mode 100755 src/less/tooltip.less delete mode 100644 test/fixtures/anaconda-test-detailed-export-v1.json delete mode 100644 test/fixtures/eddb-modules.json delete mode 100644 test/karma.conf.js delete mode 100644 test/tests/test-controller-outfit.js delete mode 100644 test/tests/test-data.js delete mode 100644 test/tests/test-service-serializer.js diff --git a/.babelrc b/.babelrc index a3a2c1d4..31749320 100644 --- a/.babelrc +++ b/.babelrc @@ -1,20 +1,3 @@ { - "stage": 0, - "env": { - "development": { - "plugins": ["react-transform"], - "extra": { - "react-transform": { - "transforms": [{ - "transform": "react-transform-hmr", - "imports": ["react"], - "locals": ["module"] - }, { - "transform": "react-transform-catch-errors", - "imports": ["react", "redbox-react"] - }] - } - } - } - } -} + "presets": ["es2015", "react", "stage-0"] +} \ No newline at end of file diff --git a/test/fixtures/anaconda-test-detailed-export-v2.json b/__tests__/fixtures/anaconda-test-detailed-export-v3.json similarity index 95% rename from test/fixtures/anaconda-test-detailed-export-v2.json rename to __tests__/fixtures/anaconda-test-detailed-export-v3.json index 6aaff689..397443bd 100644 --- a/test/fixtures/anaconda-test-detailed-export-v2.json +++ b/__tests__/fixtures/anaconda-test-detailed-export-v3.json @@ -1,12 +1,12 @@ { - "$schema": "http://cdn.coriolis.io/schemas/ship-loadout/2.json#", + "$schema": "http://cdn.coriolis.io/schemas/ship-loadout/3.json#", "name": "Test", "ship": "Anaconda", "references": [ { "name": "Coriolis.io", - "url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18QDBNA%3D%3D%3D.AwhMJBGaei%2BJCyyiA%3D%3D%3D?bn=Test", - "code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18QDBNA===.AwhMJBGaei+JCyyiA===", + "url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test?bn=Test", + "code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA", "shipId": "anaconda" } ], @@ -264,12 +264,12 @@ "masslock": 23, "pipSpeed": 0.14, "shipCostMultiplier": 1, - "componentCostMultiplier": 1, + "moduleCostMultiplier": 1, "fuelCapacity": 32, "cargoCapacity": 128, "ladenMass": 1339.2, - "armour": 2078, - "armourAdded": 240, + "armour": 2228, + "armourAdded": 390, "armourMultiplier": 1.95, "shieldMultiplier": 1.4, "totalCost": 882362060, diff --git a/test/fixtures/ed-shipyard-import-invalid.json b/__tests__/fixtures/ed-shipyard-import-invalid.json similarity index 100% rename from test/fixtures/ed-shipyard-import-invalid.json rename to __tests__/fixtures/ed-shipyard-import-invalid.json diff --git a/test/fixtures/ed-shipyard-import-valid.json b/__tests__/fixtures/ed-shipyard-import-valid.json similarity index 100% rename from test/fixtures/ed-shipyard-import-valid.json rename to __tests__/fixtures/ed-shipyard-import-valid.json diff --git a/test/fixtures/expected-builds.json b/__tests__/fixtures/expected-builds.json similarity index 100% rename from test/fixtures/expected-builds.json rename to __tests__/fixtures/expected-builds.json diff --git a/test/fixtures/old-valid-export.json b/__tests__/fixtures/old-valid-export.json similarity index 100% rename from test/fixtures/old-valid-export.json rename to __tests__/fixtures/old-valid-export.json diff --git a/test/fixtures/valid-backup.json b/__tests__/fixtures/valid-backup.json similarity index 100% rename from test/fixtures/valid-backup.json rename to __tests__/fixtures/valid-backup.json diff --git a/test/fixtures/valid-detailed-export.json b/__tests__/fixtures/valid-detailed-export.json similarity index 100% rename from test/fixtures/valid-detailed-export.json rename to __tests__/fixtures/valid-detailed-export.json diff --git a/test/tests/test-controller-import.js b/__tests__/test-controller-import.js similarity index 99% rename from test/tests/test-controller-import.js rename to __tests__/test-controller-import.js index 8d9f199f..8ae919b9 100644 --- a/test/tests/test-controller-import.js +++ b/__tests__/test-controller-import.js @@ -1,4 +1,4 @@ -describe('Import Controller', function() { +xdescribe('Import Controller', function() { beforeEach(module('app')); var importController, $rootScope, $stateParams, scope; diff --git a/__tests__/test-service-serializer.js b/__tests__/test-service-serializer.js new file mode 100644 index 00000000..1ce3385a --- /dev/null +++ b/__tests__/test-service-serializer.js @@ -0,0 +1,54 @@ +import Ship from '../src/app/shipyard/Ship'; +import { Ships } from 'coriolis-data'; +import * as Serializer from '../src/app/shipyard/Serializer'; + +describe("Serializer Service", function() { + + const anacondaTestExport = require.requireActual('./fixtures/anaconda-test-detailed-export-v3'); + const code = anacondaTestExport.references[0].code; + const anaconda = Ships.anaconda; + + describe("To Detailed Build", function() { + + let testBuild, exportData; + + beforeEach(function() { + testBuild = new Ship('anaconda', anaconda.properties, anaconda.slots); + testBuild.buildFrom(code); + exportData = Serializer.toDetailedBuild('Test', testBuild); + }); + + xit("conforms to the v2 ship-loadout schema", function() { + // var validate = jsen(require('../schemas/ship-loadout/3')); + // var valid = validate(exportData); + expect(valid).toBeTruthy(); + }); + + it("contains the correct components and stats", function() { + expect(exportData.components).toEqual(anacondaTestExport.components); + expect(exportData.stats).toEqual(anacondaTestExport.stats); + expect(exportData.ship).toEqual(anacondaTestExport.ship); + expect(exportData.name).toEqual(anacondaTestExport.name); + }); + + }); + + describe("From Detailed Build", function() { + + it("builds the ship correctly", function() { + let testBuildA = new Ship('anaconda', anaconda.properties, anaconda.slots); + testBuildA.buildFrom(code); + let testBuildB = Serializer.fromDetailedBuild(anacondaTestExport); + + for(var p in testBuildB) { + if (p == 'availCS') { + continue; + } + expect(testBuildB[p]).toEqual(testBuildA[p], p + ' does not match'); + } + + }); + + }); + +}); diff --git a/test/tests/test-factory-ship.js b/__tests__/test-ship.js similarity index 67% rename from test/tests/test-factory-ship.js rename to __tests__/test-ship.js index ac31d633..69b5ff2a 100644 --- a/test/tests/test-factory-ship.js +++ b/__tests__/test-ship.js @@ -1,20 +1,15 @@ -describe("Ship Factory", function() { - - var Ship; - var Components; +import Ship from '../src/app/shipyard/Ship'; +import { Ships } from 'coriolis-data'; +import * as ModuleUtils from '../src/app/shipyard/ModuleUtils'; - beforeEach(module('shipyard')); - beforeEach(inject(['Ship', 'Components', function (_Ship_, _Components_) { - Ship = _Ship_; - Components = _Components_; - }])); +describe("Ship Factory", function() { it("can build all ships", function() { - for (var s in DB.ships) { - var shipData = DB.ships[s]; - var ship = new Ship(s, shipData.properties, shipData.slots); + for (let s in Ships) { + let shipData = Ships[s]; + let ship = new Ship(s, shipData.properties, shipData.slots); - for (p in shipData.properties) { + for (let p in shipData.properties) { expect(ship[p]).toEqual(shipData.properties[p], s + ' property [' + p + '] does not match when built'); } @@ -37,7 +32,7 @@ describe("Ship Factory", function() { it("resets and rebuilds properly", function() { var id = 'cobra_mk_iii'; - var cobra = DB.ships[id]; + var cobra = Ships[id]; var shipA = new Ship(id, cobra.properties, cobra.slots); var shipB = new Ship(id, cobra.properties, cobra.slots); var testShip = new Ship(id, cobra.properties, cobra.slots); @@ -81,7 +76,7 @@ describe("Ship Factory", function() { it("discounts hull and components properly", function() { var id = 'cobra_mk_iii'; - var cobra = DB.ships[id]; + var cobra = Ships[id]; var testShip = new Ship(id, cobra.properties, cobra.slots); testShip.buildWith(cobra.defaults); @@ -89,76 +84,76 @@ describe("Ship Factory", function() { var originalTotalCost = testShip.totalCost; var discount = 0.9; - expect(testShip.c.discountedCost).toEqual(originalHullCost, 'Hull cost does not match'); + expect(testShip.m.discountedCost).toEqual(originalHullCost, 'Hull cost does not match'); testShip.applyDiscounts(discount, discount); // Floating point errors cause miniscule decimal places which are handled in the app by rounding/formatting - expect(Math.floor(testShip.c.discountedCost)).toEqual(Math.floor(originalHullCost * discount), 'Discounted Hull cost does not match'); + expect(Math.floor(testShip.m.discountedCost)).toEqual(Math.floor(originalHullCost * discount), 'Discounted Hull cost does not match'); expect(Math.floor(testShip.totalCost)).toEqual(Math.floor(originalTotalCost * discount), 'Discounted Total cost does not match'); testShip.applyDiscounts(1, 1); // No discount, 100% of cost - expect(testShip.c.discountedCost).toEqual(originalHullCost, 'Hull cost does not match'); + expect(testShip.m.discountedCost).toEqual(originalHullCost, 'Hull cost does not match'); expect(testShip.totalCost).toEqual(originalTotalCost, 'Total cost does not match'); testShip.applyDiscounts(discount, 1); // Only discount hull - expect(Math.floor(testShip.c.discountedCost)).toEqual(Math.round(originalHullCost * discount), 'Discounted Hull cost does not match'); - expect(testShip.totalCost).toEqual(originalTotalCost - originalHullCost + testShip.c.discountedCost, 'Total cost does not match'); + expect(Math.floor(testShip.m.discountedCost)).toEqual(Math.round(originalHullCost * discount), 'Discounted Hull cost does not match'); + expect(testShip.totalCost).toEqual(originalTotalCost - originalHullCost + testShip.m.discountedCost, 'Total cost does not match'); }); it("enforces a single shield generator", function() { var id = 'anaconda'; - var anacondaData = DB.ships[id]; + var anacondaData = Ships[id]; var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots); anaconda.buildWith(anacondaData.defaults); - expect(anaconda.internal[2].c.grp).toEqual('sg', 'Anaconda default shield generator slot'); + expect(anaconda.internal[2].m.grp).toEqual('sg', 'Anaconda default shield generator slot'); - anaconda.use(anaconda.internal[1], '4j', Components.internal('4j')); // 6E Shield Generator + anaconda.use(anaconda.internal[1], ModuleUtils.internal('4j')); // 6E Shield Generator expect(anaconda.internal[2].c).toEqual(null, 'Anaconda default shield generator slot is empty'); - expect(anaconda.internal[2].id).toEqual(null, 'Anaconda default shield generator slot id is null'); - expect(anaconda.internal[1].id).toEqual('4j', 'Slot 1 should have SG 4j in it'); - expect(anaconda.internal[1].c.grp).toEqual('sg','Slot 1 should have SG 4j in it'); + expect(anaconda.internal[2].m).toEqual(null, 'Anaconda default shield generator slot id is null'); + expect(anaconda.internal[1].m.id).toEqual('4j', 'Slot 1 should have SG 4j in it'); + expect(anaconda.internal[1].m.grp).toEqual('sg','Slot 1 should have SG 4j in it'); }); it("enforces a single shield fuel scoop", function() { var id = 'anaconda'; - var anacondaData = DB.ships[id]; + var anacondaData = Ships[id]; var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots); anaconda.buildWith(anacondaData.defaults); - anaconda.use(anaconda.internal[4], '32', Components.internal('32')); // 4A Fuel Scoop - expect(anaconda.internal[4].c.grp).toEqual('fs', 'Anaconda fuel scoop slot'); + anaconda.use(anaconda.internal[4], ModuleUtils.internal('32')); // 4A Fuel Scoop + expect(anaconda.internal[4].m.grp).toEqual('fs', 'Anaconda fuel scoop slot'); - anaconda.use(anaconda.internal[3], '32', Components.internal('32')); + anaconda.use(anaconda.internal[3], ModuleUtils.internal('32')); expect(anaconda.internal[4].c).toEqual(null, 'Anaconda original fuel scoop slot is empty'); - expect(anaconda.internal[4].id).toEqual(null, 'Anaconda original fuel scoop slot id is null'); - expect(anaconda.internal[3].id).toEqual('32', 'Slot 1 should have FS 32 in it'); - expect(anaconda.internal[3].c.grp).toEqual('fs','Slot 1 should have FS 32 in it'); + expect(anaconda.internal[4].m).toEqual(null, 'Anaconda original fuel scoop slot id is null'); + expect(anaconda.internal[3].m.id).toEqual('32', 'Slot 1 should have FS 32 in it'); + expect(anaconda.internal[3].m.grp).toEqual('fs','Slot 1 should have FS 32 in it'); }); it("enforces a single refinery", function() { var id = 'anaconda'; - var anacondaData = DB.ships[id]; + var anacondaData = Ships[id]; var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots); anaconda.buildWith(anacondaData.defaults); - anaconda.use(anaconda.internal[4], '23', Components.internal('23')); // 4E Refinery - expect(anaconda.internal[4].c.grp).toEqual('rf', 'Anaconda refinery slot'); + anaconda.use(anaconda.internal[4], ModuleUtils.internal('23')); // 4E Refinery + expect(anaconda.internal[4].m.grp).toEqual('rf', 'Anaconda refinery slot'); - anaconda.use(anaconda.internal[3], '23', Components.internal('23')); + anaconda.use(anaconda.internal[3], ModuleUtils.internal('23')); expect(anaconda.internal[4].c).toEqual(null, 'Anaconda original refinery slot is empty'); - expect(anaconda.internal[4].id).toEqual(null, 'Anaconda original refinery slot id is null'); - expect(anaconda.internal[3].id).toEqual('23', 'Slot 1 should have RF 23 in it'); - expect(anaconda.internal[3].c.grp).toEqual('rf','Slot 1 should have RF 23 in it'); + expect(anaconda.internal[4].m).toEqual(null, 'Anaconda original refinery slot id is null'); + expect(anaconda.internal[3].m.id).toEqual('23', 'Slot 1 should have RF 23 in it'); + expect(anaconda.internal[3].m.grp).toEqual('rf','Slot 1 should have RF 23 in it'); }); }); diff --git a/package.json b/package.json index f0fc3d72..019aeacc 100644 --- a/package.json +++ b/package.json @@ -8,39 +8,67 @@ "homepage": "http://coriolis.io", "bugs": "https://github.com/cmmcleod/coriolis/issues", "private": true, - "engine": "node >= 0.12.2", + "engine": "node >= 4.0.0", "license": "MIT", "scripts": { "clean": "rimraf build", "start": "node devServer.js", "lint": "eslint --ext .js,.jsx src", + "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", - "deploy": "npm run lint && npm run build && npm run rsync" + "deploy": "npm run lint && npm test && npm run build:prod && npm run rsync" + }, + "jest": { + "scriptPreprocessor": "/node_modules/babel-jest", + "testFileExtensions": [ + "js" + ], + "moduleFileExtensions": [ + "js", + "json", + "jsx" + ], + "unmockedModulePathPatterns": [ + "/node_modules/react", + "/node_modules/react-dom", + "/node_modules/react-addons-test-utils", + "/node_modules/fbjs", + "/node_modules/fbemitter", + "/node_modules/classnames", + "/node_modules/d3", + "/node_modules/lz-string", + "/node_modules/coriolis-data", + "/src/app/shipyard", + "/src/app/i18n", + "/src/app/utils" + ] }, "devDependencies": { "appcache-webpack-plugin": "^1.2.1", - "babel-core": "^5.4.7", - "babel-eslint": "^4.1.6", - "babel-loader": "^5.1.2", - "babel-plugin-react-transform": "^1.1.1", - "css-loader": "^0.23.0", - "eslint": "^1.10.1", - "eslint-plugin-react": "^2.3.0", + "babel-core": "*", + "babel-eslint": "*", + "babel-jest": "*", + "babel-loader": "*", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", + "babel-preset-stage-0": "^6.3.13", + "css-loader": "^0.23.0", + "eslint": "2.0.0-beta.1", + "eslint-plugin-react": "^3.15.0", "expose-loader": "^0.7.1", "express": "^4.13.3", "extract-text-webpack-plugin": "^0.9.1", "file-loader": "^0.8.4", "html-webpack-plugin": "^1.7.0", + "jest-cli": "*", "json-loader": "^0.5.3", "less": "^2.5.3", "less-loader": "^2.2.1", - "react-transform-catch-errors": "^1.0.0", - "react-transform-hmr": "^1.0.0", - "redbox-react": "^1.0.1", + "react-addons-test-utils": "^0.14.6", "rimraf": "^2.4.3", "style-loader": "^0.13.0", "url-loader": "^0.5.6", @@ -52,8 +80,8 @@ "d3": "^3.5.9", "fbemitter": "^2.0.0", "lz-string": "^1.4.4", - "react": "^0.14.2", - "react-dom": "^0.14.2", + "react": "^0.14.6", + "react-dom": "^0.14.6", "superagent": "^1.4.0" } } diff --git a/src/app/Coriolis.jsx b/src/app/Coriolis.jsx index c0f5567a..2b0b5036 100644 --- a/src/app/Coriolis.jsx +++ b/src/app/Coriolis.jsx @@ -1,24 +1,39 @@ import React from 'react'; import Router from './Router'; +import { EventEmitter } from 'fbemitter'; import { getLanguage } from './i18n/Language'; import Persist from './stores/Persist'; -import InterfaceEvents from './utils/InterfaceEvents'; import Header from './components/Header'; +import Tooltip from './components/Tooltip'; + 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'; +/** + * Coriolis App + */ export default class Coriolis extends React.Component { static childContextTypes = { language: React.PropTypes.object.isRequired, sizeRatio: React.PropTypes.number.isRequired, - route: React.PropTypes.object.isRequired + route: React.PropTypes.object.isRequired, + openMenu: React.PropTypes.func.isRequired, + closeMenu: React.PropTypes.func.isRequired, + showModal: React.PropTypes.func.isRequired, + hideModal: React.PropTypes.func.isRequired, + tooltip: React.PropTypes.func.isRequired, + termtip: React.PropTypes.func.isRequired, + onWindowResize: React.PropTypes.func.isRequired }; + /** + * Creates an instance of the Coriolis App + */ constructor() { super(); this._setPage = this._setPage.bind(this); @@ -26,10 +41,14 @@ export default class Coriolis extends React.Component { this._closeMenu = this._closeMenu.bind(this); this._showModal = this._showModal.bind(this); this._hideModal = this._hideModal.bind(this); + this._tooltip = this._tooltip.bind(this); + this._termtip = this._termtip.bind(this); + this._onWindowResize = this._onWindowResize.bind(this); this._onLanguageChange = this._onLanguageChange.bind(this); - this._onSizeRatioChange = this._onSizeRatioChange.bind(this) + this._onSizeRatioChange = this._onSizeRatioChange.bind(this); this._keyDown = this._keyDown.bind(this); + this.emitter = new EventEmitter(); this.state = { page: null, language: getLanguage(Persist.getLangCode()), @@ -45,23 +64,49 @@ export default class Coriolis extends React.Component { Router('*', (r) => this._setPage(null, r)); } + /** + * Updates / Sets the page and route context + * @param {[type]} page The page to be shown + * @param {Object} route The current route + */ _setPage(page, route) { this.setState({ page, route, currentMenu: null }); } + /** + * Handle unexpected error + * TODO: Implement and fix to work with Webpack (dev + prod) + * @param {string} msg Message + * @param {string} scriptUrl URL + * @param {number} line Line number + * @param {number} col Column number + * @param {Object} errObj Error Object + */ _onError(msg, scriptUrl, line, col, errObj) { console.log('WINDOW ERROR', arguments); - //this._setPage(
Some errors occured!!
); + // this._setPage(
Some errors occured!!
); } + /** + * Propagate language and format changes + * @param {string} lang Language code + */ _onLanguageChange(lang) { this.setState({ language: getLanguage(Persist.getLangCode()) }); } + /** + * Propagate the sizeRatio change + * @param {number} sizeRatio Size ratio / scale + */ _onSizeRatioChange(sizeRatio) { this.setState({ sizeRatio }); } + /** + * Handle Key Down + * @param {Event} e Keyboard Event + */ _keyDown(e) { switch (e.keyCode) { case 27: @@ -108,6 +153,43 @@ export default class Coriolis extends React.Component { } } + /** + * Show/Hide the tooltip + * @param {React.Component} content Tooltip content + * @param {DOMRect} rect Target bounding rect + * @param {[type]} opts Options + */ + _tooltip(content, rect, opts) { + if (!content && this.state.tooltip) { + this.setState({ tooltip: null }); + } else if (content && Persist.showTooltips()) { + this.setState({ tooltip: {content} }); + } + } + + /** + * Show the term tip + * @param {string} term Term + * @param {[type]} orientation Tooltip orientation (n,e,s,w) + * @param {SyntheticEvent} event Event + */ + _termtip(term, orientation, event) { + if (typeof orientation != 'string') { + event = orientation; + orientation = null; + } + this._tooltip(
{this.state.language.translate(term)}
, event.currentTarget.getBoundingClientRect(), { orientation }); + } + + /** + * Add a listener to on window resize + * @param {Function} listener Listener callback + * @return {Object} Subscription token + */ + _onWindowResize(listener) { + return this.emitter.addListener('windowResize', listener); + } + /** * Creates the context to be passed down to pages / components containing * language, sizeRatio and route references @@ -117,7 +199,14 @@ export default class Coriolis extends React.Component { return { language: this.state.language, route: this.state.route, - sizeRatio: this.state.sizeRatio + sizeRatio: this.state.sizeRatio, + openMenu: this._openMenu, + closeMenu: this._closeMenu, + showModal: this._showModal, + hideModal: this._hideModal, + tooltip: this._tooltip, + termtip: this._termtip, + onWindowResize: this._onWindowResize }; } @@ -135,14 +224,11 @@ export default class Coriolis extends React.Component { } window.onerror = this._onError.bind(this); - window.addEventListener('resize', InterfaceEvents.windowResized); + window.addEventListener('resize', () => this.emitter.emit('windowResize')); + document.body.addEventListener('scroll', () => this._tooltip()); document.addEventListener('keydown', this._keyDown); Persist.addListener('language', this._onLanguageChange); Persist.addListener('sizeRatio', this._onSizeRatioChange); - InterfaceEvents.addListener('openMenu', this._openMenu); - InterfaceEvents.addListener('closeMenu', this._closeMenu); - InterfaceEvents.addListener('showModal', this._showModal); - InterfaceEvents.addListener('hideModal', this._hideModal); Router.start(); } @@ -157,6 +243,7 @@ export default class Coriolis extends React.Component {
{ this.state.page ? : } { this.state.modal } + { this.state.tooltip } ); } diff --git a/src/app/Router.js b/src/app/Router.js index edd8b389..ac16e5a9 100644 --- a/src/app/Router.js +++ b/src/app/Router.js @@ -1,5 +1,9 @@ import Persist from './stores/Persist'; +/** + * Determine if the app is running in mobile/tablet 'standalone' mode + * @return {Boolean} True if the app is in standalone mode + */ function isStandAlone() { try { return window.navigator.standalone || (window.external && window.external.msIsSiteMode && window.external.msIsSiteMode()); @@ -9,8 +13,7 @@ function isStandAlone() { } /** - * Register `path` with callback `fn()`, - * or route `path`, or `Router.start()`. + * Register path with callback fn(), or route path`, or Router.start(). * * Router('*', fn); * Router('/user/:id', load, user); @@ -18,15 +21,15 @@ function isStandAlone() { * Router('/user/' + user.id); * Router(); * - * @param {String} path - * @param {Function} fn... + * @param {String} path path + * @param {Function} fn Callbacks (fn, fn, ...) * @api public */ function Router(path, fn) { - var route = new Route(path); - for (var i = 1; i < arguments.length; ++i) { - Router.callbacks.push(route.middleware(arguments[i])); - } + let route = new Route(path); + for (let i = 1; i < arguments.length; ++i) { + Router.callbacks.push(route.middleware(arguments[i])); + } } /** @@ -35,20 +38,19 @@ function Router(path, fn) { Router.callbacks = []; -Router.start = function(){ +Router.start = function() { window.addEventListener('popstate', onpopstate, false); if (isStandAlone()) { - var state = Persist.getState(); + let state = Persist.getState(); // If a previous state has been stored, load that state if (state && state.name && state.params) { Router(this.props.initialPath || '/'); - //$state.go(state.name, state.params, { location: 'replace' }); } else { Router('/'); } } else { - var url = location.pathname + location.search; + let url = location.pathname + location.search; Router.replace(url, null, true, true); } }; @@ -56,14 +58,14 @@ Router.start = function(){ /** * Show `path` with optional `state` object. * - * @param {String} path - * @param {Object} state - * @return {Context} + * @param {String} path Path + * @param {Object} state Additional state + * @return {Context} New Context * @api public */ Router.go = function(path, state) { gaTrack(path); - var ctx = new Context(path, state); + let ctx = new Context(path, state); Router.dispatch(ctx); if (!ctx.unhandled) { history.pushState(ctx.state, ctx.title, ctx.canonicalPath); @@ -74,15 +76,15 @@ Router.go = function(path, state) { /** * Replace `path` with optional `state` object. * - * @param {String} path - * @param {Object} state - * @return {Context} + * @param {String} path path + * @param {Object} state State + * @param {Boolean} dispatch If true dispatch the route / trigger update + * @return {Context} New Context * @api public */ - Router.replace = function(path, state, dispatch) { gaTrack(path); - var ctx = new Context(path, state); + let ctx = new Context(path, state); if (dispatch) Router.dispatch(ctx); history.replaceState(ctx.state, ctx.title, ctx.canonicalPath); return ctx; @@ -91,15 +93,18 @@ Router.replace = function(path, state, dispatch) { /** * Dispatch the given `ctx`. * - * @param {Object} ctx + * @param {Context} ctx Context * @api private */ +Router.dispatch = function(ctx) { + let i = 0; -Router.dispatch = function(ctx){ - var i = 0; - + /** + * Handle the next route + * @return {Function} Unhandled + */ function next() { - var fn = Router.callbacks[i++]; + let fn = Router.callbacks[i++]; if (!fn) return unhandled(ctx); fn(ctx, next); } @@ -112,12 +117,12 @@ Router.dispatch = function(ctx){ * popstate then redirect. If you wish to handle * 404s on your own use `Router('*', callback)`. * - * @param {Context} ctx + * @param {Context} ctx Context + * @return {Context} context * @api private */ - function unhandled(ctx) { - var current = window.location.pathname + window.location.search; + let current = window.location.pathname + window.location.search; if (current != ctx.canonicalPath) { window.location = ctx.canonicalPath; } @@ -128,13 +133,12 @@ function unhandled(ctx) { * Initialize a new "request" `Context` * with the given `path` and optional initial `state`. * - * @param {String} path - * @param {Object} state + * @param {String} path Path + * @param {Object} state State * @api public */ - function Context(path, state) { - var i = path.indexOf('?'); + let i = path.indexOf('?'); this.canonicalPath = path; this.path = path || '/'; @@ -160,33 +164,28 @@ function Context(path, state) { * - `sensitive` enable case-sensitive routes * - `strict` enable strict matching for trailing slashes * - * @param {String} path - * @param {Object} options. + * @param {String} path Path + * @param {Object} options Options * @api private */ - function Route(path, options) { options = options || {}; this.path = path; this.method = 'GET'; - this.regexp = pathtoRegexp(path - , this.keys = [] - , options.sensitive - , options.strict); + this.regexp = pathtoRegexp(path, this.keys = [], options.sensitive, options.strict); } /** * Return route middleware with * the given callback `fn()`. * - * @param {Function} fn - * @return {Function} + * @param {Function} fn Route function + * @return {Function} Callback * @api public */ - -Route.prototype.middleware = function(fn){ - var self = this; - return function(ctx, next){ +Route.prototype.middleware = function(fn) { + let self = this; + return function(ctx, next) { if (self.match(ctx.path, ctx.params)) return fn(ctx, next); next(); }; @@ -196,24 +195,23 @@ Route.prototype.middleware = function(fn){ * Check if this route matches `path`, if so * populate `params`. * - * @param {String} path - * @param {Array} params - * @return {Boolean} + * @param {String} path Path + * @param {Array} params Path params + * @return {Boolean} True if path matches * @api private */ - -Route.prototype.match = function(path, params){ - var keys = this.keys - , qsIndex = path.indexOf('?') - , pathname = ~qsIndex ? path.slice(0, qsIndex) : path - , m = this.regexp.exec(decodeURIComponent(pathname)); +Route.prototype.match = function(path, params) { + let keys = this.keys, + qsIndex = path.indexOf('?'), + pathname = ~qsIndex ? path.slice(0, qsIndex) : path, + m = this.regexp.exec(decodeURIComponent(pathname)); if (!m) return false; - for (var i = 1, len = m.length; i < len; ++i) { - var key = keys[i - 1]; + for (let i = 1, len = m.length; i < len; ++i) { + let key = keys[i - 1]; - var val = 'string' == typeof m[i] ? decodeURIComponent(m[i]) : m[i]; + let val = 'string' == typeof m[i] ? decodeURIComponent(m[i]) : m[i]; if (key) { params[key.name] = undefined !== params[key.name] ? params[key.name] : val; @@ -223,22 +221,9 @@ Route.prototype.match = function(path, params){ return true; }; - -/** - * Check if the app is running in stand alone mode. - * @return {Boolean} true if running in Standalone mode - */ -function isStandAlone() { - try { - return window.navigator.standalone || (window.external && window.external.msIsSiteMode && window.external.msIsSiteMode()); - } catch (ex) { - return false; - } -} - /** * Track a page view in Google Analytics - * @param {string} path + * @param {string} path Path to track */ function gaTrack(path) { if (window.ga) { @@ -255,29 +240,28 @@ function gaTrack(path) { * key names. For example "/user/:id" will * then contain ["id"]. * - * @param {String|RegExp|Array} path - * @param {Array} keys - * @param {Boolean} sensitive - * @param {Boolean} strict - * @return {RegExp} + * @param {String|RegExp|Array} path Path template(s) + * @param {Array} keys keys + * @param {Boolean} sensitive Case sensitive + * @param {Boolean} strict Strict matching + * @return {RegExp} Regular expression * @api private */ - function pathtoRegexp(path, keys, sensitive, strict) { if (path instanceof RegExp) return path; if (path instanceof Array) path = '(' + path.join('|') + ')'; path = path .concat(strict ? '' : '/?') .replace(/\/\(/g, '(?:/') - .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){ + .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) { keys.push({ name: key, optional: !! optional }); slash = slash || ''; - return '' - + (optional ? '' : slash) - + '(?:' - + (optional ? slash : '') - + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' - + (optional || ''); + return '' + + (optional ? '' : slash) + + '(?:' + + (optional ? slash : '') + + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + + (optional || ''); }) .replace(/([\/.])/g, '\\$1') .replace(/\*/g, '(.*)'); @@ -286,11 +270,11 @@ function pathtoRegexp(path, keys, sensitive, strict) { /** * Handle "populate" events. + * @param {Event} e Event object */ - function onpopstate(e) { if (e.state) { - var path = e.state.path; + let path = e.state.path; Router.replace(path, e.state, true); } } diff --git a/src/app/components/ActiveLink.jsx b/src/app/components/ActiveLink.jsx index dfb5228c..26cf40cd 100644 --- a/src/app/components/ActiveLink.jsx +++ b/src/app/components/ActiveLink.jsx @@ -2,19 +2,31 @@ import React from 'react'; import Link from './Link'; import cn from 'classnames'; -export default class ActiveLink extends Link { - isActive = () => { - return encodeURI(this.props.href) == (window.location.pathname + window.location.search); - } +/** + * Returns true if the current window location equals the link + * @return {boolean} If matches + */ +function isActive(href) { + return encodeURI(href) == (window.location.pathname + window.location.search); +} + +/** + * Active Link - Highlighted when URL matches window location + */ +export default class ActiveLink extends Link { + /** + * Renders the component + * @return {React.Component} The active link + */ render() { let className = this.props.className; - if (this.isActive()) { + 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 ea30de3e..b66776fa 100644 --- a/src/app/components/AvailableModulesMenu.jsx +++ b/src/app/components/AvailableModulesMenu.jsx @@ -4,11 +4,15 @@ import TranslatedComponent from './TranslatedComponent'; import cn from 'classnames'; import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; +/** + * Available modules menu + */ export default class AvailableModulesMenu extends TranslatedComponent { static propTypes = { - modules: React.PropTypes.oneOfType([ React.PropTypes.object, React.PropTypes.array ]).isRequired, + modules: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.array]).isRequired, onSelect: React.PropTypes.func.isRequired, + diffDetails: React.PropTypes.func, m: React.PropTypes.object, shipMass: React.PropTypes.number, warning: React.PropTypes.func @@ -18,23 +22,83 @@ export default class AvailableModulesMenu extends TranslatedComponent { shipMass: 0 }; - buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules) { + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + this._hideDiff = this._hideDiff.bind(this); + this.state = { list: this._initList(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 + */ + _initList(props, context) { + let translate = context.language.translate; + let { m, warning, shipMass, onSelect, modules } = props; + let list; + let buildGroup = this._buildGroup.bind( + this, + translate, + m, + warning, + shipMass - (m && m.mass ? m.mass : 0), + (m) => { + this._hideDiff(); + onSelect(m); + } + ); + + if (modules instanceof Array) { + list = buildGroup(modules[0].grp, modules); + } else { + list = []; + // At present time slots with grouped options (Hardpoints and Internal) can be empty + list.push(
{translate('empty')}
); + for (let g in modules) { + list.push(
{translate(g)}
); + list.push(buildGroup(g, modules[g])); + } + } + + return list; + } + + /** + * Generate React Components for Module Group + * @param {Function} translate Translate function + * @param {Objecy} mountedModule Mounted Module + * @param {Funciton} warningFunc Warning function + * @param {number} mass Mass + * @param {function} onSelect Select/Mount callback + * @param {string} grp Group name + * @param {Array} modules Available modules + * @return {React.Component} Available Module Group contents + */ + _buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules) { let prevClass = null, prevRating = null; let elems = []; for (let i = 0; i < modules.length; i++) { let m = modules[i]; let mount = null; + let disabled = m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass; let classes = cn(m.name ? 'lc' : 'c', { active: mountedModule && mountedModule.id === m.id, - warning: warningFunc && warningFunc(m), - disabled: m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass + warning: !disabled && warningFunc && warningFunc(m), + disabled }); switch(m.mount) { - case 'F': mount = ; break; - case 'G': mount = ; break; - case 'T': mount = ; break; + case 'F': mount = ; break; + case 'G': mount = ; break; + case 'T': mount = ; break; } if (i > 0 && modules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mount) && m.grp != 'pa') { @@ -42,7 +106,13 @@ export default class AvailableModulesMenu extends TranslatedComponent { } elems.push( -
  • +
  • {mount} {(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}
  • @@ -54,43 +124,54 @@ export default class AvailableModulesMenu extends TranslatedComponent { return
      {elems}
    ; } + /** + * Generate tooltip content for the difference between the + * mounted module and the hovered modules + * @param {Object} mm The module mounet currently + * @param {Object} m The hovered module + * @param {SyntheticEvent} event Event + */ + _showDiff(mm, m, event) { + if (this.props.diffDetails) { + this.context.tooltip(this.props.diffDetails(m, mm), event.currentTarget.getBoundingClientRect()); + } + } + + /** + * Hide diff tooltip + */ + _hideDiff() { + this.context.tooltip(); + } + + /** + * Scroll to mounted (if it exists) component on mount + */ componentDidMount() { - let m = this.props.m + 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 } } - render() { - let translate = this.context.language.translate; - let m = this.props.m; - let modules = this.props.modules; - let list; - let buildGroup = this.buildGroup.bind( - null, - translate, - m, - this.props.warning, - this.props.shipMass - (m && m.mass ? m.mass : 0), - this.props.onSelect - ); - - if (modules instanceof Array) { - list = buildGroup(modules[0].grp, modules); - } else { - list = []; - // At present time slots with grouped options (Hardpoints and Internal) can be empty - list.push(
    {translate('empty')}
    ); - for (let g in modules) { - list.push(
    {translate(g)}
    ); - list.push(buildGroup(g, modules[g])); - } - } + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ + componentWillReceiveProps(nextProps, nextContext) { + this.setState({ list: this._initList(nextProps, nextContext) }); + } + /** + * Render the list + * @return {React.Component} List + */ + render() { return ( -
    e.stopPropagation() }> - {list} +
    e.stopPropagation() }> + {this.state.list}
    ); } diff --git a/src/app/components/BarChart.jsx b/src/app/components/BarChart.jsx new file mode 100644 index 00000000..69e7d912 --- /dev/null +++ b/src/app/components/BarChart.jsx @@ -0,0 +1,211 @@ +import React from 'react'; +import { findDOMNode } from 'react-dom'; +import d3 from 'd3'; +import TranslatedComponent from './TranslatedComponent'; + +const MARGIN = { top: 15, right: 20, bottom: 40, left: 150 }; +const BAR_HEIGHT = 30; + +/** + * Get ship and build name + * @param {Object} build Ship build + * @return {string} name and build name + */ +function bName(build) { + return build.buildName + '\n' + build.name; +} + +/** + * Replace a SVG text element's content with + * tspans that wrap on newline + * @param {string} d Data point + */ +function insertLinebreaks(d) { + let el = d3.select(this); + let lines = d.split('\n'); + el.text('').attr('y', -6); + for (let i = 0; i < lines.length; i++) { + let tspan = el.append('tspan').text(lines[i].length > 18 ? lines[i].substring(0, 15) + '...' : lines[i]); + if (i > 0) { + tspan.attr('x', -9).attr('dy', '1em'); + } else { + tspan.attr('class', 'primary'); + } + } +} + +/** + * Bar Chart + */ +export default class BarChart extends TranslatedComponent { + + static defaultProps = { + colors: ['#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c'], + 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, + predicate: React.PropTypes.string, + desc: React.PropTypes.bool + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this._updateDimensions = this._updateDimensions.bind(this); + this._hideTip = this._hideTip.bind(this); + + let scale = d3.scale.linear(); + let y0 = d3.scale.ordinal(); + let y1 = d3.scale.ordinal(); + + this.xAxis = d3.svg.axis().scale(scale).ticks(5).outerTickSize(0).orient('bottom').tickFormat(context.language.formats.s2); + this.yAxis = d3.svg.axis().scale(y0).outerTickSize(0).orient('left'); + this.state = { scale, y0, y1, color: d3.scale.ordinal().range(props.colors) }; + } + + /** + * Generate and Show tooltip + * @param {Object} build Ship build + * @param {string} property Property to display + */ + _showTip(build, property) { + let { unit, format } = this.props; + let { scale, y0, y1 } = this.state; + let { formats } = this.context.language; + let fontSize = parseFloat(window.getComputedStyle(document.getElementById('coriolis')).getPropertyValue('font-size') || 16); + let val = build[property]; + 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 tooltip = + + + {valStr} + + + ; + this.setState({ tooltip }); + } + + /** + * Hide tooltip + */ + _hideTip() { + this.setState({ tooltip: null }); + } + + /** + * Update dimensions based on properties and scale + * @param {Object} props React Component properties + * @param {number} scale size ratio / scale + */ + _updateDimensions(props, scale) { + let { width, data, properties } = props; + let innerWidth = width - MARGIN.left - MARGIN.right; + let barHeight = Math.round(BAR_HEIGHT * scale); + let dataSize = data.length; + let innerHeight = barHeight * dataSize; + let outerHeight = innerHeight + MARGIN.top + MARGIN.bottom; + let max = data.reduce((max, build) => (properties.reduce(((m, p) => (m > build[p] ? m : build[p])), max)), 0); + + this.state.scale.range([0, innerWidth]).domain([0, max]); + this.state.y0.domain(data.map(bName)).rangeRoundBands([0, innerHeight], 0.3); + this.state.y1.domain(properties).rangeRoundBands([0, this.state.y0.rangeBand()]); + + this.setState({ + barHeight, + dataSize, + innerWidth, + outerHeight, + innerHeight + }); + } + + /** + * Update dimensions based on props and context. + */ + componentWillMount() { + this._updateDimensions(this.props, this.context.sizeRatio); + } + + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ + componentWillReceiveProps(nextProps, nextContext) { + let { data, width, predicate, desc } = nextProps; + let props = this.props; + + if (width != props.width || this.context.sizeRatio != nextContext.sizeRatio || data != props.data) { + this._updateDimensions(nextProps, nextContext.sizeRatio); + } + + if (this.context.language != nextContext.language) { + this.xAxis.tickFormat(nextContext.language.formats.s2); + } + + if (predicate != props.predicate || desc != props.desc) { + this.state.y0.domain(data.map(bName)); + } + } + + /** + * Render the chart + * @return {React.Component} Chart SVG + */ + render() { + if (!this.props.width) { + return null; + } + + let { label, 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) => + + )} + + ); + + return + + {bars} + {tooltip} + d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> + + {label} + { unit ? {` (${unit})`} : null } + + + { let e = d3.select(elem); e.call(this.yAxis); e.selectAll('text').each(insertLinebreaks); }} /> + + ; + } +} diff --git a/src/app/components/ComparisonTable.jsx b/src/app/components/ComparisonTable.jsx index 5a7b9eb1..ca99d307 100644 --- a/src/app/components/ComparisonTable.jsx +++ b/src/app/components/ComparisonTable.jsx @@ -5,6 +5,9 @@ import cn from 'classnames'; import { SizeMap } from '../shipyard/Constants'; +/** + * Comparison Table + */ export default class ComparisonTable extends TranslatedComponent { static propTypes = { @@ -12,16 +15,27 @@ export default class ComparisonTable extends TranslatedComponent { builds: React.PropTypes.array.isRequired, onSort: React.PropTypes.func.isRequired, predicate: React.PropTypes.string.isRequired, // Used only to test again prop changes for shouldRender - desc: React.PropTypes.bool.isRequired, // Used only to test again prop changes for shouldRender - } - + desc: React.PropTypes.oneOfType([React.PropTypes.bool.isRequired, React.PropTypes.number.isRequired]), // Used only to test again prop changes for shouldRender + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ constructor(props, context) { super(props, context); this._buildHeaders = this._buildHeaders.bind(this); - this.state = this._buildHeaders(props.facets, props.onSort, context.language.translate); } + /** + * Build table headers + * @param {Array} facets Facets list + * @param {Function} onSort Sort callback + * @param {Function} translate Translate function + * @return {Object} Header Components + */ _buildHeaders(facets, onSort, translate) { let header = [ {translate('ship')}, @@ -39,7 +53,7 @@ export default class ComparisonTable extends TranslatedComponent { if (pl > 1) { for (let i = 0; i < pl; i++) { - subHeader.push({translate(f.lbls[i])}); + subHeader.push({translate(f.lbls[i])}); } } } @@ -48,6 +62,14 @@ export default class ComparisonTable extends TranslatedComponent { return { header, subHeader }; } + /** + * Generate a table row for the build + * @param {Object} build Ship build + * @param {Array} facets Facets list + * @param {Object} formats Localized formats map + * @param {Object} units Localized untis map + * @return {React.Component} Table row + */ _buildRow(build, facets, formats, units) { let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`; let cells = [ @@ -66,6 +88,11 @@ export default class ComparisonTable extends TranslatedComponent { return {cells}; } + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ componentWillReceiveProps(nextProps, nextContext) { // If facets or language has changed re-render header if (nextProps.facets != this.props.facets || nextContext.language != this.context.language) { @@ -73,6 +100,10 @@ export default class ComparisonTable extends TranslatedComponent { } } + /** + * Render the table + * @return {React.Component} Comparison table + */ render() { let { builds, facets } = this.props; let { header, subHeader } = this.state; @@ -97,6 +128,5 @@ export default class ComparisonTable extends TranslatedComponent {
    ); - } } diff --git a/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx index 7dffe493..c3a01901 100644 --- a/src/app/components/CostSection.jsx +++ b/src/app/components/CostSection.jsx @@ -4,9 +4,12 @@ import { Ships } from 'coriolis-data'; import Persist from '../stores/Persist'; import Ship from '../shipyard/Ship'; import { Insurance } from '../shipyard/Constants'; -import { slotName, nameComparator } from '../utils/SlotFunctions'; +import { slotName, slotComparator } from '../utils/SlotFunctions'; import TranslatedComponent from './TranslatedComponent'; +/** + * Cost Section + */ export default class CostSection extends TranslatedComponent { static PropTypes = { @@ -15,6 +18,10 @@ export default class CostSection extends TranslatedComponent { buildName: React.PropTypes.string }; + /** + * Constructor + * @param {Object} props React Component properties + */ constructor(props) { super(props); this._costsTab = this._costsTab.bind(this); @@ -52,10 +59,17 @@ export default class CostSection extends TranslatedComponent { }; } + /** + * Create a ship instance to base/reference retrofit changes from + * @param {string} shipId Ship Id + * @param {string} name Build name + * @param {Ship} retrofitShip Existing retrofit ship + * @return {Ship} Retrofit ship + */ _buildRetrofitShip(shipId, name, retrofitShip) { let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults - if (!retrofitShip) { + if (!retrofitShip) { // Don't create a new instance unless needed retrofitShip = new Ship(shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison } @@ -67,15 +81,28 @@ export default class CostSection extends TranslatedComponent { return retrofitShip; } + /** + * Get the default retrofit build name if it exists + * @param {string} shipId Ship Id + * @param {string} name Build name + * @return {string} Build name or null + */ _defaultRetrofitName(shipId, name) { return Persist.hasBuild(shipId, name) ? name : null; } + /** + * Show selected tab + * @param {string} tab Tab name + */ _showTab(tab) { Persist.setCostTab(tab); this.setState({ tab }); } + /** + * Update prices on discount change + */ _onDiscountChanged() { let shipDiscount = Persist.getShipDiscount(); let moduleDiscount = Persist.getModuleDiscount(); @@ -84,13 +111,17 @@ export default class CostSection extends TranslatedComponent { this.setState({ shipDiscount, moduleDiscount }); } + /** + * Update insurance on change + * @param {string} insuranceName Insurance level name + */ _onInsuranceChanged(insuranceName) { this.setState({ insurance: Insurance[insuranceName] }); } /** * Repopulate modules on retrofit ship from existing build - * @param {string} retrofitName Build name to base the retrofit ship on + * @param {SyntheticEvent} event Build name to base the retrofit ship on */ _onBaseRetrofitChange(event) { let retrofitName = event.target.value; @@ -105,6 +136,12 @@ export default class CostSection extends TranslatedComponent { this.setState({ retrofitName }); } + /** + * On build save + * @param {string} shipId Ship Id + * @param {string} name Build name + * @param {string} code Serialized ship 'code' + */ _onBuildSaved(shipId, name, code) { if(this.state.retrofitName == name) { this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build @@ -114,6 +151,12 @@ export default class CostSection extends TranslatedComponent { } } + /** + * 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 @@ -122,11 +165,19 @@ export default class CostSection extends TranslatedComponent { this.setState({ buildOptions: Persist.getBuildsNamesFor(shipId) }); } + /** + * Toggle item cost inclusion in overall total + * @param {Object} item Cost item + */ _toggleCost(item) { this.props.ship.setCostIncluded(item, !item.incCost); this.setState({ total: this.props.ship.totalCost }); } + /** + * Toggle item cost inclusion in retrofit total + * @param {Object} item Cost item + */ _toggleRetrofitCost(item) { let retrofitTotal = this.state.retrofitTotal; item.retroItem.incCost = !item.retroItem.incCost; @@ -134,6 +185,10 @@ export default class CostSection extends TranslatedComponent { this.setState({ retrofitTotal }); } + /** + * Set cost list sort predicate + * @param {string} predicate sort predicate + */ _sortCostBy(predicate) { let { costPredicate, costDesc } = this.state; @@ -144,20 +199,27 @@ export default class CostSection extends TranslatedComponent { this.setState({ costPredicate: predicate, costDesc }); } + /** + * Sort cost list + * @param {Ship} ship Ship instance + * @param {string} predicate Sort predicate + * @param {Boolean} desc Sort descending + */ _sortCost(ship, predicate, desc) { let costList = ship.costList; + let translate = this.context.language.translate; if (predicate == 'm') { - costList.sort(nameComparator(this.context.language.translate)); + costList.sort(slotComparator(translate, null, desc)); } else { - costList.sort((a, b) => (a.m && a.m.cost ? a.m.cost : 0) - (b.m && b.m.cost ? b.m.cost : 0)); - } - - if (!desc) { - costList.reverse(); + costList.sort(slotComparator(translate, (a, b) => (a.m.cost || 0) - (b.m.cost || 0), desc)); } } + /** + * Set ammo list sort predicate + * @param {string} predicate sort predicate + */ _sortAmmoBy(predicate) { let { ammoPredicate, ammoDesc } = this.state; @@ -168,19 +230,26 @@ export default class CostSection extends TranslatedComponent { this.setState({ ammoPredicate: predicate, ammoDesc }); } + /** + * Sort ammo cost list + * @param {Array} ammoCosts Ammo cost list + * @param {string} predicate Sort predicate + * @param {Boolean} desc Sort descending + */ _sortAmmo(ammoCosts, predicate, desc) { + let translate = this.context.language.translate; if (predicate == 'm') { - ammoCosts.sort(nameComparator(this.context.language.translate)); + ammoCosts.sort(slotComparator(translate, null, desc)); } else { - ammoCosts.sort((a, b) => a[predicate] - b[predicate]); - } - - if (!desc) { - ammoCosts.reverse(); + ammoCosts.sort(slotComparator(translate, (a, b) => a[predicate] - b[predicate], desc)); } } + /** + * Set retrofit list sort predicate + * @param {string} predicate sort predicate + */ _sortRetrofitBy(predicate) { let { retroPredicate, retroDesc } = this.state; @@ -191,6 +260,12 @@ export default class CostSection extends TranslatedComponent { this.setState({ retroPredicate: predicate, retroDesc }); } + /** + * Sort retrofit cost list + * @param {Array} retrofitCosts Retrofit cost list + * @param {string} predicate Sort predicate + * @param {Boolean} desc Sort descending + */ _sortRetrofit(retrofitCosts, predicate, desc) { let translate = this.context.language.translate; @@ -205,6 +280,10 @@ export default class CostSection extends TranslatedComponent { } } + /** + * Render the cost tab + * @return {React.Component} Tab contents + */ _costsTab() { let { ship } = this.props; let { total, shipDiscount, moduleDiscount, insurance } = this.state; @@ -250,6 +329,10 @@ export default class CostSection extends TranslatedComponent { ; } + /** + * Render the retofit tab + * @return {React.Component} Tab contents + */ _retrofitTab() { let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state; let { translate, formats, units } = this.context.language; @@ -268,11 +351,11 @@ export default class CostSection extends TranslatedComponent { {translate(item.sellName)} {item.buyClassRating} {translate(item.buyName)} - 0 ? 'warning' : 'secondary-disabled' : 'disabled' )}>{int(item.netCost)}{units.CR} + 0 ? 'warning' : 'secondary-disabled' : 'disabled')}>{int(item.netCost)}{units.CR} ); } } else { - rows = {translate('PHRASE_NO_RETROCH')} + rows = {translate('PHRASE_NO_RETROCH')}; } return
    @@ -284,7 +367,7 @@ export default class CostSection extends TranslatedComponent { {translate('buy')} {translate('net cost')} - {moduleDiscount < 1 && {`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}} + {moduleDiscount < 1 && {`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}} @@ -301,7 +384,7 @@ export default class CostSection extends TranslatedComponent { @@ -311,9 +394,15 @@ export default class CostSection extends TranslatedComponent {
    ; } + + /** + * Update retrofit costs + * @param {Ship} ship Ship instance + * @param {Ship} retrofitShip Retrofit Ship instance + */ _updateRetrofit(ship, retrofitShip) { let retrofitCosts = []; - var retrofitTotal = 0, i, l, item; + let retrofitTotal = 0, i, l, item; if (ship.bulkheads.index != retrofitShip.bulkheads.index) { item = { @@ -330,9 +419,9 @@ export default class CostSection extends TranslatedComponent { } } - for (var g in { standard: 1, internal: 1, hardpoints: 1 }) { - var retroSlotGroup = retrofitShip[g]; - var slotGroup = ship[g]; + for (let g in { standard: 1, internal: 1, hardpoints: 1 }) { + let retroSlotGroup = retrofitShip[g]; + let slotGroup = ship[g]; for (i = 0, l = slotGroup.length; i < l; i++) { if (slotGroup[i].m != retroSlotGroup[i].m) { item = { netCost: 0, retroItem: retroSlotGroup[i] }; @@ -358,6 +447,10 @@ export default class CostSection extends TranslatedComponent { this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc); } + /** + * Render the ammo tab + * @return {React.Component} Tab contents + */ _ammoTab() { let { ammoTotal, ammoCosts } = this.state; let { translate, formats, units } = this.context.language; @@ -400,6 +493,7 @@ export default class CostSection extends TranslatedComponent { /** * Recalculate all ammo costs + * @param {Ship} ship Ship instance */ _updateAmmoCosts(ship) { let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, srvs = 0, scoop = false; @@ -408,10 +502,10 @@ export default class CostSection extends TranslatedComponent { let slotGroup = ship[g]; for (let i = 0, l = slotGroup.length; i < l; i++) { if (slotGroup[i].m) { - //special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip + // Special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip q = 0; switch (slotGroup[i].m.grp) { - case 'fs': //skip fuel calculation if scoop present + case 'fs': // Skip fuel calculation if scoop present scoop = true; break; case 'scb': @@ -429,7 +523,7 @@ export default class CostSection extends TranslatedComponent { default: q = slotGroup[i].m.clip + slotGroup[i].m.ammo; } - //calculate ammo costs only if a cost is specified + // Calculate ammo costs only if a cost is specified if (slotGroup[i].m.ammocost > 0) { item = { m: slotGroup[i].m, @@ -444,7 +538,7 @@ export default class CostSection extends TranslatedComponent { } } - //limpets if controllers exist and cargo space available + // Limpets if controllers exist and cargo space available if (limpets > 0) { item = { m: { name: 'limpets', class: '', rating: '' }, @@ -466,7 +560,7 @@ export default class CostSection extends TranslatedComponent { ammoCosts.push(item); ammoTotal += item.total; } - //calculate refuel costs if no scoop present + // Calculate refuel costs if no scoop present if (!scoop) { item = { m: { name: 'fuel', class: '', rating: '' }, @@ -482,7 +576,10 @@ export default class CostSection extends TranslatedComponent { this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc); } - componentWillMount(){ + /** + * Add listeners on mount and update costs + */ + componentWillMount() { this.listeners = [ Persist.addListener('discounts', this._onDiscountChanged.bind(this)), Persist.addListener('insurance', this._onInsuranceChanged.bind(this)), @@ -494,13 +591,18 @@ export default class CostSection extends TranslatedComponent { this._sortCost(this.props.ship); } + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next context + */ componentWillReceiveProps(nextProps, nextContext) { let retrofitShip = this.state.retrofitShip; if (nextProps.ship != this.props.ship) { // Ship has changed let nextId = nextProps.ship.id; let retrofitName = this._defaultRetrofitName(nextId, nextProps.buildName); - retrofitShip = this._buildRetrofitShip(nextId, retrofitName, nextId == this.props.ship.id ? retrofitShip : null ); + retrofitShip = this._buildRetrofitShip(nextId, retrofitName, nextId == this.props.ship.id ? retrofitShip : null); this.setState({ retrofitShip, retrofitName, @@ -515,6 +617,11 @@ export default class CostSection extends TranslatedComponent { } } + /** + * Sort lists before render + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextState Incoming/Next state + */ componentWillUpdate(nextProps, nextState) { let state = this.state; @@ -536,10 +643,17 @@ export default class CostSection extends TranslatedComponent { } } - componentWillUnmount(){ + /** + * Remove listeners + */ + componentWillUnmount() { this.listeners.forEach(l => l.remove()); } + /** + * Render the Cost section + * @return {React.Component} Contents + */ render() { let tab = this.state.tab; let translate = this.context.language.translate; @@ -558,9 +672,9 @@ export default class CostSection extends TranslatedComponent { - - - + + +
    {translate('costs')}{translate('retrofit costs')}{translate('reload costs')}{translate('costs')}{translate('retrofit costs')}{translate('reload costs')}
    diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx index 13abf4b8..c6818253 100644 --- a/src/app/components/HardpointSlot.jsx +++ b/src/app/components/HardpointSlot.jsx @@ -1,36 +1,58 @@ import React from 'react'; import Slot from './Slot'; +/** + * Hardpoint / Utility Slot + */ export default class HardpointSlot extends Slot { + /** + * Get the CSS class name for the slot. + * @return {string} CSS Class name + */ _getClassNames() { return this.props.maxClass > 0 ? 'hardpoint' : null; } - _getMaxClassLabel(translate){ + /** + * Get the label for the slot + * @param {Function} translate Translate function + * @return {string} Label + */ + _getMaxClassLabel(translate) { return translate(['U','S','M','L','H'][this.props.maxClass]); } + /** + * Generate the slot contents + * @param {Object} m Mounted Module + * @param {Function} translate Translate function + * @param {Object} formats Localized Formats map + * @param {Object} u Localized Units Map + * @return {React.Component} Slot contents + */ _getSlotDetails(m, translate, formats, u) { if (m) { let classRating = `${m.class}${m.rating}${m.mount ? '/' + m.mount : ''}${m.missile ? m.missile : ''}`; - return ( -
    + let { drag, drop } = this.props; + + return
    +
    {classRating + ' ' + translate(m.name || m.grp)}
    {m.mass}{u.T}
    -
    - { m.damage ?
    {translate('damage')}: {m.damage} { m.ssdam ? ({formats.int(m.ssdam)} {u.MJ}) : null }
    : null } - { m.dps ?
    {translate('DPS')}: {m.dps} { m.mjdps ? ({formats.int(m.mjdps)} {u.MJ}) : null }
    : null } - { m.thermload ?
    {translate('T_LOAD')}: {m.thermload}
    : null } - { m.type ?
    {translate('type')}: {m.type}
    : null } - { m.rof ?
    {translate('ROF')}: {m.rof}{u.ps}
    : null } - { m.armourpen ?
    {translate('pen')}: {m.armourpen}
    : null } - { m.shieldmul ?
    +{formats.rPct(m.shieldmul)}
    : null } - { m.range ?
    {m.range} km
    : null } - { m.ammo >= 0 ?
    {translate('ammo')}: {formats.int(m.clip)}+{formats.int(m.ammo)}
    : null } -
    - ); +
    + { m.damage ?
    {translate('damage')}: {m.damage} { m.ssdam ? ({formats.int(m.ssdam)} {u.MJ}) : null }
    : null } + { m.dps ?
    {translate('DPS')}: {m.dps} { m.mjdps ? ({formats.int(m.mjdps)} {u.MJ}) : null }
    : null } + { m.thermload ?
    {translate('T_LOAD')}: {m.thermload}
    : null } + { m.type ?
    {translate('type')}: {m.type}
    : null } + { m.rof ?
    {translate('ROF')}: {m.rof}{u.ps}
    : null } + { m.armourpen ?
    {translate('pen')}: {m.armourpen}
    : null } + { m.shieldmul ?
    +{formats.rPct(m.shieldmul)}
    : null } + { m.range ?
    {m.range} km
    : null } + { m.ammo >= 0 ?
    {translate('ammo')}: {formats.int(m.clip)}+{formats.int(m.ammo)}
    : null } +
    +
    ; } else { return
    {translate('empty')}
    ; } diff --git a/src/app/components/HardpointsSlotSection.jsx b/src/app/components/HardpointsSlotSection.jsx index ce7bd3ef..86a8a898 100644 --- a/src/app/components/HardpointsSlotSection.jsx +++ b/src/app/components/HardpointsSlotSection.jsx @@ -4,35 +4,60 @@ import HardpointSlot from './HardpointSlot'; import cn from 'classnames'; import { MountFixed, MountGimballed, MountTurret } from '../components/SvgIcons'; +/** + * Hardpoint slot section + */ export default class HardpointsSlotSection extends SlotSection { + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ constructor(props, context) { super(props, context, 'hardpoints', 'hardpoints'); this._empty = this._empty.bind(this); } + /** + * Empty all slots + */ _empty() { this.props.ship.emptyWeapons(); this.props.onChange(); this._close(); } + /** + * Fill slots with specified module + * @param {string} group Group name + * @param {string} mount Mount Type - F, G, T + * @param {SyntheticEvent} event Event + */ _fill(group, mount, event) { this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt')); this.props.onChange(); this._close(); } + /** + * Empty all on section header right click + */ _contextMenu() { this._empty(); } + /** + * Generate the slot React Components + * @return {Array} Array of Slots + */ _getSlots() { + let { ship, currentMenu } = this.props; + let { originSlot, targetSlot } = this.state; let slots = []; - let hardpoints = this.props.ship.hardpoints; - let availableModules = this.props.ship.getAvailableModules(); - let currentMenu = this.props.currentMenu; + let hardpoints = ship.hardpoints; + let availableModules = ship.getAvailableModules(); for (let i = 0, l = hardpoints.length; i < l; i++) { let h = hardpoints[i]; @@ -44,6 +69,11 @@ export default class HardpointsSlotSection extends SlotSection { onOpen={this._openMenu.bind(this, h)} onSelect={this._selectModule.bind(this, h)} selected={currentMenu == h} + drag={this._drag.bind(this, h)} + dragOver={this._dragOverSlot.bind(this, h)} + drop={this._drop} + dropClass={this._dropClass(h, originSlot, targetSlot)} + ship={ship} m={h.m} />); } @@ -52,6 +82,11 @@ export default class HardpointsSlotSection extends SlotSection { return slots; } + /** + * Generate the section drop-down menu + * @param {Function} translate Translate function + * @return {React.Component} Section menu + */ _getSectionMenu(translate) { let _fill = this._fill; diff --git a/src/app/components/Header.jsx b/src/app/components/Header.jsx index dd714b20..40e0e4a5 100644 --- a/src/app/components/Header.jsx +++ b/src/app/components/Header.jsx @@ -7,7 +7,6 @@ import ActiveLink from './ActiveLink'; import cn from 'classnames'; import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons'; import { Ships } from 'coriolis-data'; -import InterfaceEvents from '../utils/InterfaceEvents'; import Persist from '../stores/Persist'; import { toDetailedExport } from '../shipyard/Serializer'; import ModalDeleteAll from './ModalDeleteAll'; @@ -18,8 +17,16 @@ import Slider from './Slider'; const SIZE_MIN = 0.65; const SIZE_RANGE = 0.55; +/** + * Coriolis App Header section / menus + */ export default class Header extends TranslatedComponent { + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ constructor(props, context) { super(props); this.shipOrder = Object.keys(Ships).sort(); @@ -44,64 +51,114 @@ export default class Header extends TranslatedComponent { for (let name in Discounts) { this.discountOptions.push(); } - } + /** + * Update insurance level + * @param {SyntheticEvent} e Event + */ _setInsurance(e) { Persist.setInsurance(e.target.value); } + /** + * Update the Module discount + * @param {SyntheticEvent} e Event + */ _setModuleDiscount(e) { Persist.setModuleDiscount(e.target.value * 1); } + /** + * Update the Ship discount + * @param {SyntheticEvent} e Event + */ _setShipDiscount(e) { Persist.setShipDiscount(e.target.value * 1); } - _setLanguage(e){ + /** + * Update the current language + * @param {SyntheticEvent} e Event + */ + _setLanguage(e) { Persist.setLangCode(e.target.value); } + /** + * Toggle tooltips setting + */ + _toggleTooltips() { + Persist.showTooltips(!Persist.showTooltips()); + } + + /** + * Show delete all modal + * @param {SyntheticEvent} e Event + */ _showDeleteAll(e) { e.preventDefault(); - InterfaceEvents.showModal(); + this.context.showModal(); }; + /** + * Show export modal with backup data + * @param {SyntheticEvent} e Event + */ _showBackup(e) { let translate = this.context.language.translate; e.preventDefault(); - InterfaceEvents.showModal(); }; - _showDetailedExport(e){ + /** + * Show export modal with detailed export + * @param {SyntheticEvent} e Event + */ + _showDetailedExport(e) { let translate = this.context.language.translate; e.preventDefault(); - InterfaceEvents.showModal(); } + /** + * Show import modal + * @param {SyntheticEvent} e Event + */ _showImport(e) { e.preventDefault(); - InterfaceEvents.showModal(); + this.context.showModal(); } - _setTextSize(size) { - Persist.setSizeRatio((size * SIZE_RANGE) + SIZE_MIN); + /** + * Update the app scale / size ratio + * @param {number} scale scale Size Ratio + */ + _setTextSize(scale) { + Persist.setSizeRatio((scale * SIZE_RANGE) + SIZE_MIN); } + /** + * Reset the app scale / size ratio + */ _resetTextSize() { Persist.setSizeRatio(1); } + /** + * Open a menu + * @param {SyntheticEvent} event Event + * @param {string} menu Menu name + */ _openMenu(event, menu) { event.stopPropagation(); @@ -109,23 +166,31 @@ export default class Header extends TranslatedComponent { menu = null; } - InterfaceEvents.openMenu(menu); + this.context.openMenu(menu); } + /** + * Generate the ships menu + * @return {React.Component} Menu + */ _getShipsMenu() { 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}
    ); } + /** + * Generate the builds menu + * @return {React.Component} Menu + */ _getBuildsMenu() { let builds = Persist.getBuilds(); let buildList = []; @@ -135,19 +200,23 @@ export default class Header extends TranslatedComponent { let buildNameOrder = Object.keys(builds[shipId]).sort(); for (let buildName of buildNameOrder) { let href = ['/outfit/', shipId, '/', builds[shipId][buildName], '?bn=', buildName].join(''); - shipBuilds.push(
  • {buildName}
  • ); + shipBuilds.push(
  • {buildName}
  • ); } buildList.push(
      {Ships[shipId].properties.name}{shipBuilds}
    ); } } return ( -
    e.stopPropagation() }> -
    {buildList}
    +
    e.stopPropagation() }> +
    {buildList}
    ); } + /** + * Generate the comparison menu + * @return {React.Component} Menu + */ _getComparisonsMenu() { let comparisons; let translate = this.context.language.translate; @@ -157,69 +226,68 @@ export default class Header extends TranslatedComponent { let comps = Object.keys(Persist.getComparisons()).sort(); for (let name of comps) { - comparisons.push({name}); + comparisons.push({name}); } } else { - comparisons = {translate('none created')}; + comparisons = {translate('none created')}; } return ( -
    e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}> +
    e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}> {comparisons}
    - {translate('compare all')} - {translate('create new')} + {translate('compare all')} + {translate('create new')}
    ); } + /** + * Generate the settings menu + * @return {React.Component} Menu + */ _getSettingsMenu() { let translate = this.context.language.translate; + let tips = Persist.showTooltips(); return ( -
    e.stopPropagation() }> -
      - {translate('language')} -
    • - -
    • -

    -
      - {translate('insurance')} -
    • - -
    • -

    -
      - {translate('ship')} {translate('discount')} -
    • - -
    • -

    -
      - {translate('module')} {translate('discount')} -
    • - -
    • -
    +
    e.stopPropagation() }> +
    + {translate('language')} + +
    + + {translate('tooltips')} +
    {(tips ? '✔' : '✖')}
    +
    +
    + {translate('insurance')} + +
    + {translate('ship')} {translate('discount')} + +
    + {translate('module')} {translate('discount')} + +


    - +
    @@ -227,22 +295,34 @@ export default class Header extends TranslatedComponent { - +
    AA
    {translate('reset')}{translate('reset')}

    - {translate('about')} + {translate('about')}
    ); } - componentWillMount(){ + /** + * 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()); } + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ componentWillReceiveProps(nextProps, nextContext) { if(this.context.language != nextContext.language) { let translate = nextContext.language.translate; @@ -253,6 +333,10 @@ export default class Header extends TranslatedComponent { } } + /** + * Render the header + * @return {React.Component} Header + */ render() { let translate = this.context.language.translate; let openedMenu = this.props.currentMenu; @@ -264,32 +348,32 @@ export default class Header extends TranslatedComponent { return (
    - + -
    -
    this._openMenu(e,'s') } > - {' ' + translate('ships')} +
    +
    this._openMenu(e,'s') } > + {' ' + translate('ships')}
    {openedMenu == 's' ? this._getShipsMenu() : null}
    -
    -
    this._openMenu(e,'b') : null }> - {' ' + translate('builds')} +
    +
    this._openMenu(e,'b') : null }> + {' ' + translate('builds')}
    {openedMenu == 'b' ? this._getBuildsMenu() : null}
    -
    -
    this._openMenu(e,'comp') : null }> - {' ' + translate('compare')} +
    +
    this._openMenu(e,'comp') : null }> + {' ' + translate('compare')}
    {openedMenu == 'comp' ? this._getComparisonsMenu() : null}
    -
    -
    this._openMenu(e,'settings') }> - {translate('settings')} +
    +
    this._openMenu(e,'settings') }> + {translate('settings')}
    {openedMenu == 'settings' ? this._getSettingsMenu() : null}
    diff --git a/src/app/components/InternalSlot.jsx b/src/app/components/InternalSlot.jsx index 8e5c5e09..fcfb322d 100644 --- a/src/app/components/InternalSlot.jsx +++ b/src/app/components/InternalSlot.jsx @@ -2,36 +2,48 @@ import React from 'react'; import Slot from './Slot'; import { Infinite } from './SvgIcons'; +/** + * Internal Slot + */ export default class InternalSlot extends Slot { + /** + * Generate the slot contents + * @param {Object} m Mounted Module + * @param {Function} translate Translate function + * @param {Object} formats Localized Formats map + * @param {Object} u Localized Units Map + * @return {React.Component} Slot contents + */ _getSlotDetails(m, translate, formats, u) { if (m) { let classRating = m.class + m.rating; + let { drag, drop } = this.props; - return ( -
    + return
    +
    {classRating + ' ' + translate(m.name || m.grp)}
    -
    {m.mass || m.capacity || 0}{u.T}
    -
    - { m.optmass ?
    {translate('optimal mass') + ': '}{m.optmass}{u.T}
    : null } - { m.maxmass ?
    {translate('max mass') + ': '}{m.maxmass}{u.T}
    : null } - { m.bins ?
    {m.bins + ' '}{translate('bins')}
    : null } - { m.bays ?
    {translate('bays') + ': ' + m.bays}
    : null } - { m.rate ?
    {translate('rate')}: {m.rate}{u.kgs}   {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}
    : null } - { m.ammo ?
    {translate('ammo')}: {formats.gen(m.ammo)}
    : null } - { m.cells ?
    {translate('cells')}: {m.cells}
    : null } - { m.recharge ?
    {translate('recharge')}: {m.recharge} MJ   {translate('total')}: {m.cells * m.recharge}{u.MJ}
    : null } - { m.repair ?
    {translate('repair')}: {m.repair}
    : null } - { m.range ?
    {translate('range')} {m.range}{u.km}
    : null } - { m.time ?
    {translate('time')}: {formats.time(m.time)}
    : null } - { m.maximum ?
    {translate('max')}: {(m.maximum)}
    : null } - { m.rangeLS ?
    {m.rangeLS}{u.Ls}
    : null } - { m.rangeLS === null ?
    {u.Ls}
    : null } - { m.rangeRating ?
    {translate('range')}: {m.rangeRating}
    : null } - { m.armouradd ?
    +{m.armouradd} {translate('armour')}
    : null } -
    +
    {m.mass || m.cargo || m.fuel || 0}{u.T}
    - ); +
    + { m.optmass ?
    {translate('optimal mass') + ': '}{m.optmass}{u.T}
    : null } + { m.maxmass ?
    {translate('max mass') + ': '}{m.maxmass}{u.T}
    : null } + { m.bins ?
    {m.bins + ' '}{translate('bins')}
    : null } + { m.bays ?
    {translate('bays') + ': ' + m.bays}
    : null } + { m.rate ?
    {translate('rate')}: {m.rate}{u.kgs}   {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}
    : null } + { m.ammo ?
    {translate('ammo')}: {formats.gen(m.ammo)}
    : null } + { m.cells ?
    {translate('cells')}: {m.cells}
    : null } + { m.recharge ?
    {translate('recharge')}: {m.recharge} MJ   {translate('total')}: {m.cells * m.recharge}{u.MJ}
    : null } + { m.repair ?
    {translate('repair')}: {m.repair}
    : null } + { m.range ?
    {translate('range')} {m.range}{u.km}
    : null } + { m.time ?
    {translate('time')}: {formats.time(m.time)}
    : null } + { m.maximum ?
    {translate('max')}: {(m.maximum)}
    : null } + { m.rangeLS ?
    {m.rangeLS}{u.Ls}
    : null } + { m.rangeLS === null ?
    {u.Ls}
    : null } + { m.rangeRating ?
    {translate('range')}: {m.rangeRating}
    : null } + { m.armouradd ?
    +{m.armouradd} {translate('armour')}
    : null } +
    +
    ; } else { return
    {translate('empty')}
    ; } diff --git a/src/app/components/InternalSlotSection.jsx b/src/app/components/InternalSlotSection.jsx index 29305568..69e8863c 100644 --- a/src/app/components/InternalSlotSection.jsx +++ b/src/app/components/InternalSlotSection.jsx @@ -4,9 +4,16 @@ import SlotSection from './SlotSection'; import InternalSlot from './InternalSlot'; import * as ModuleUtils from '../shipyard/ModuleUtils'; - +/** + * Internal slot section + */ export default class InternalSlotSection extends SlotSection { + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ constructor(props, context) { super(props, context, 'internal', 'internal compartments'); @@ -16,12 +23,19 @@ export default class InternalSlotSection extends SlotSection { this._fillWithArmor = this._fillWithArmor.bind(this); } + /** + * Empty all slots + */ _empty() { this.props.ship.emptyInternal(); this.props.onChange(); this._close(); } + /** + * Fill all slots with cargo racks + * @param {SyntheticEvent} event Event + */ _fillWithCargo(event) { let clobber = event.getModifierState('Alt'); let ship = this.props.ship; @@ -34,6 +48,10 @@ export default class InternalSlotSection extends SlotSection { this._close(); } + /** + * Fill all slots with Shield Cell Banks + * @param {SyntheticEvent} event Event + */ _fillWithCells(event) { let clobber = event.getModifierState('Alt'); let ship = this.props.ship; @@ -49,6 +67,10 @@ export default class InternalSlotSection extends SlotSection { this._close(); } + /** + * Fill all slots with Hull Reinforcement Packages + * @param {SyntheticEvent} event Event + */ _fillWithArmor(event) { let clobber = event.getModifierState('Alt'); let ship = this.props.ship; @@ -61,18 +83,27 @@ export default class InternalSlotSection extends SlotSection { this._close(); } + /** + * Empty all on section header right click + */ _contextMenu() { this._empty(); } + /** + * Generate the slot React Components + * @return {Array} Array of Slots + */ _getSlots() { let slots = []; let { currentMenu, ship } = this.props; - let {internal, fuelCapacity, ladenMass } = ship; + let { originSlot, targetSlot } = this.state; + let { internal, fuelCapacity, ladenMass } = ship; let availableModules = ship.getAvailableModules(); for (let i = 0, l = internal.length; i < l; i++) { let s = internal[i]; + slots.push(); } return slots; } + /** + * Generate the section drop-down menu + * @param {Function} translate Translate function + * @return {React.Component} Section menu + */ _getSectionMenu(translate) { return
    e.stopPropagation()}>
      diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx index 27e117da..ada105d9 100644 --- a/src/app/components/LineChart.jsx +++ b/src/app/components/LineChart.jsx @@ -4,15 +4,18 @@ 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: 15, bottom: 35, left: 60 }; +/** + * Line Chart + */ export default class LineChart extends TranslatedComponent { static defaultProps = { xMin: 0, yMin: 0, colors: ['#ff8c0d'] - } + }; static PropTypes = { width: React.PropTypes.number.isRequired, @@ -29,6 +32,11 @@ export default class LineChart extends TranslatedComponent { colors: React.PropTypes.array, }; + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ constructor(props, context) { super(props); @@ -42,11 +50,12 @@ export default class LineChart extends TranslatedComponent { let markerElems = []; let detailElems = []; let xScale = d3.scale.linear(); + let xAxisScale = d3.scale.linear(); let yScale = d3.scale.linear(); let series = props.series; let seriesLines = []; - this.xAxis = d3.svg.axis().scale(xScale).outerTickSize(0).orient('bottom'); + this.xAxis = d3.svg.axis().scale(xAxisScale).outerTickSize(0).orient('bottom'); this.yAxis = d3.svg.axis().scale(yScale).ticks(6).outerTickSize(0).orient('left'); for(let i = 0, l = series ? series.length : 1; i < l; i++) { @@ -58,6 +67,7 @@ export default class LineChart extends TranslatedComponent { this.state = { xScale, + xAxisScale, yScale, seriesLines, detailElems, @@ -66,15 +76,19 @@ export default class LineChart extends TranslatedComponent { }; } + /** + * Update tooltip content + * @param {number} xPos x coordinate + */ _tooltip(xPos) { let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; - let { xScale, yScale } = this.state; + let { xScale, yScale, innerWidth } = this.state; let { formats, translate } = this.context.language; let x0 = xScale.invert(xPos), y0 = func(x0), tips = this.tipContainer, yTotal = 0, - flip = (x0 / xScale.domain()[1] > 0.65), + flip = (xPos / innerWidth > 0.65), tipWidth = 0, tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; @@ -100,32 +114,53 @@ export default class LineChart extends TranslatedComponent { this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0)); } - _updateDimensions(props, sizeRatio) { + /** + * Update dimensions based on properties and scale + * @param {Object} props React Component properties + * @param {number} scale size ratio / scale + */ + _updateDimensions(props, scale) { let { width, xMax, xMin, yMin, yMax } = props; let innerWidth = width - MARGIN.left - MARGIN.right; - let outerHeight = Math.round(width * 0.5 * sizeRatio); + let outerHeight = Math.round(width * 0.5 * scale); let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); + this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true); this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax]); this.setState({ innerWidth, outerHeight, innerHeight }); } + /** + * Show tooltip + * @param {SyntheticEvent} e Event + */ _showTip(e) { this._moveTip(e); this.tipContainer.style('display', null); this.markersContainer.style('display', null); } + /** + * Move and update tooltip + * @param {SyntheticEvent} e Event + */ _moveTip(e) { this._tooltip(Math.round(e.clientX - e.target.getBoundingClientRect().left)); } + /** + * Hide tooltip + */ _hideTip() { this.tipContainer.style('display', 'none'); this.markersContainer.style('display', 'none'); } + /** + * Update series data generated from props + * @param {Object} props React Component properties + */ _updateSeriesData(props) { let { func, xMin, xMax, series } = props; let delta = (xMax - xMin) / RENDER_POINTS; @@ -134,23 +169,31 @@ export default class LineChart extends TranslatedComponent { if (delta) { seriesData = new Array(RENDER_POINTS); for (let i = 0, x = xMin; i < RENDER_POINTS; i++) { - seriesData[i] = [ x, func(x) ]; + seriesData[i] = [x, func(x)]; x += delta; } - seriesData[RENDER_POINTS - 1] = [ xMax, func(xMax) ]; + seriesData[RENDER_POINTS - 1] = [xMax, func(xMax)]; } else { let yVal = func(xMin); - seriesData = [ [0, yVal], [1, yVal]]; + seriesData = [[0, yVal], [1, yVal]]; } this.setState({ seriesData }); } - componentWillMount(){ + /** + * Update dimensions and series data based on props and context. + */ + componentWillMount() { this._updateDimensions(this.props, this.context.sizeRatio); this._updateSeriesData(this.props); } + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ componentWillReceiveProps(nextProps, nextContext) { let { func, xMin, xMax, yMin, yMax, width } = nextProps; let props = this.props; @@ -166,6 +209,10 @@ export default class LineChart extends TranslatedComponent { } } + /** + * Render the chart + * @return {React.Component} Chart SVG + */ render() { if (!this.props.width) { return null; @@ -192,7 +239,7 @@ export default class LineChart extends TranslatedComponent { this.tipContainer = d3.select(g)} className='tooltip' style={{ display: 'none' }}> - + {detailElems} this.markersContainer = d3.select(g)} style={{ display: 'none' }}> diff --git a/src/app/components/Link.jsx b/src/app/components/Link.jsx index fdf73cb9..7567d087 100644 --- a/src/app/components/Link.jsx +++ b/src/app/components/Link.jsx @@ -1,20 +1,32 @@ import React from 'react'; import Router from '../Router'; -import shallowEqual from '../utils/shallowEqual'; +import { shallowEqual } from '../utils/UtilityFunctions'; +/** + * Link wrapper component + */ export default class Link extends React.Component { + /** + * Determine if a component should be rerendered + * @param {object} nextProps Next properties + * @return {boolean} true if update is needed + */ shouldComponentUpdate(nextProps) { return !shallowEqual(this.props, nextProps); } - handler = (event) => { - if (event.getModifierState - && ( event.getModifierState('Shift') - || event.getModifierState('Alt') - || event.getModifierState('Control') - || event.getModifierState('Meta') - || event.button > 1)) { + /** + * Link click handler + * @param {SyntheticEvent} event Event + */ + handler(event) { + if (event.getModifierState && + (event.getModifierState('Shift') || + event.getModifierState('Alt') || + event.getModifierState('Control') || + event.getModifierState('Meta') || + event.button > 1)) { return; } event.nativeEvent && event.preventDefault && event.preventDefault(); @@ -24,8 +36,12 @@ export default class Link extends React.Component { } } + /** + * Renders the link + * @return {React.Component} A href element + */ render() { - 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 1c7eea73..00a70aef 100644 --- a/src/app/components/ModalCompare.jsx +++ b/src/app/components/ModalCompare.jsx @@ -1,17 +1,24 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; -import InterfaceEvents from '../utils/InterfaceEvents'; import { Ships } from 'coriolis-data'; import Persist from '../stores/Persist'; +/** + * Build ship and name comparator + * @param {Object} a [description] + * @param {Object} b [description] + * @return {number} 1, 0, -1 + */ function buildComparator(a, b) { if (a.name == b.name) { - return a.buildName > b.buildName; + return a.buildName.localeCompare(b.buildName); } - return a.name > b.name; + return a.name.localeCompare(b.name); } - +/** + * Compare builds modal + */ export default class ModalCompare extends TranslatedComponent { static propTypes = { @@ -21,52 +28,71 @@ export default class ModalCompare extends TranslatedComponent { static defaultProps = { builds: [] - } + }; + /** + * Constructor + * @param {Object} props React Component properties + */ constructor(props) { super(props); let builds = props.builds; let allBuilds = Persist.getBuilds(); let unusedBuilds = []; + let usedBuilds = []; for (let id in allBuilds) { for (let buildName in allBuilds[id]) { - if (!builds.find((e) => e.buildName == buildName && e.id == id)) { - unusedBuilds.push({ id, buildName, name: Ships[id].properties.name }) - } + let b = { id, buildName, name: Ships[id].properties.name }; + builds.find((e) => e.buildName == buildName && e.id == id) ? usedBuilds.push(b) : unusedBuilds.push(b); } } - builds.sort(buildComparator); + usedBuilds.sort(buildComparator); unusedBuilds.sort(buildComparator); - this.state = { builds, unusedBuilds }; + this.state = { usedBuilds, unusedBuilds, used: usedBuilds.length }; } + /** + * Add a build to the compare list + * @param {number} buildIndex Idnex of build in list + */ _addBuild(buildIndex) { - let { builds, unusedBuilds } = this.state; - builds.push(unusedBuilds[buildIndex]); - unusedBuilds = unusedBuilds.splice(buildIndex, 1); - builds.sort(buildComparator); + let { usedBuilds, unusedBuilds } = this.state; + usedBuilds.push(unusedBuilds[buildIndex]); + unusedBuilds.splice(buildIndex, 1); + usedBuilds.sort(buildComparator); - this.setState({ builds, unusedBuilds }); + this.setState({ used: usedBuilds.length }); } + /** + * Remove a build from the compare list + * @param {number} buildIndex Idnex of build in list + */ _removeBuild(buildIndex) { - let { builds, unusedBuilds } = this.state; - unusedBuilds.push(builds[buildIndex]); - builds = builds.splice(buildIndex, 1); + let { usedBuilds, unusedBuilds } = this.state; + unusedBuilds.push(usedBuilds[buildIndex]); + usedBuilds.splice(buildIndex, 1); unusedBuilds.sort(buildComparator); - this.setState({ builds, unusedBuilds }); + this.setState({ used: usedBuilds.length }); } + /** + * OK Action - Use selected builds + */ _selectBuilds() { - this.props.onSelect(this.state.builds); + this.props.onSelect(this.state.usedBuilds); } + /** + * Render the modal + * @return {React.Component} Modal Content + */ render() { - let { builds, unusedBuilds } = this.state; + let { usedBuilds, unusedBuilds } = this.state; let translate = this.context.language.translate; let availableBuilds = unusedBuilds.map((build, i) => @@ -76,7 +102,7 @@ export default class ModalCompare extends TranslatedComponent { ); - let selectedBuilds = builds.map((build, i) => + let selectedBuilds = usedBuilds.map((build, i) => {build.name}< td className='tl'>{build.buildName} @@ -102,7 +128,7 @@ export default class ModalCompare extends TranslatedComponent {

    - +
    ; } } diff --git a/src/app/components/ModalDeleteAll.jsx b/src/app/components/ModalDeleteAll.jsx index 1bfda0d6..fd0eb948 100644 --- a/src/app/components/ModalDeleteAll.jsx +++ b/src/app/components/ModalDeleteAll.jsx @@ -1,22 +1,32 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; -import InterfaceEvents from '../utils/InterfaceEvents'; +import Persist from '../stores/Persist'; +/** + * Delete All saved data modal + */ export default class ModalDeleteAll extends TranslatedComponent { + /** + * Delete everything and hide the modal + */ _deleteAll() { Persist.deleteAll(); - InterfaceEvents.hideModal(); + this.context.hideModal(); } + /** + * Renders the component + * @return {React.Component} Modal contents + */ render() { let translate = this.context.language.translate; return
    e.stopPropagation()}>

    {translate('delete all')}

    -

    {translate('PHRASE_CONFIRMATION')}

    - - +

    {translate('PHRASE_CONFIRMATION')}

    + +
    ; } } diff --git a/src/app/components/ModalExport.jsx b/src/app/components/ModalExport.jsx index a7a1759e..70e95ee2 100644 --- a/src/app/components/ModalExport.jsx +++ b/src/app/components/ModalExport.jsx @@ -1,20 +1,26 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; -import InterfaceEvents from '../utils/InterfaceEvents'; +/** + * Export Modal + */ export default class ModalExport extends TranslatedComponent { static propTypes = { title: React.PropTypes.string, - promise: React.PropTypes.func, + generator: React.PropTypes.func, data: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object, React.PropTypes.array]) }; + /** + * Constructor + * @param {Object} props React Component properties + */ constructor(props) { super(props); let exportJson; - if (props.promise) { + if (props.generator) { exportJson = 'Generating...'; } else if(typeof props.data == 'string') { exportJson = props.data; @@ -25,16 +31,25 @@ export default class ModalExport extends TranslatedComponent { this.state = { exportJson }; } - componentWillMount(){ - // When promise is done update exportJson accordingly + /** + * If generator is provided, execute on mount + */ + componentWillMount() { + if (this.props.generator) { + this.props.generator((str) => this.setState({ exportJson: str })); + } } + /** + * Render the modal + * @return {React.Component} Modal Content + */ render() { let translate = this.context.language.translate; let description; if (this.props.description) { - description =
    {translate(this.props.description)}
    + description =
    {translate(this.props.description)}
    ; } return
    e.stopPropagation() }> @@ -43,7 +58,7 @@ export default class ModalExport extends TranslatedComponent {