diff --git a/src/app/Coriolis.jsx b/src/app/Coriolis.jsx index 5f298f60..c0f5567a 100644 --- a/src/app/Coriolis.jsx +++ b/src/app/Coriolis.jsx @@ -8,13 +8,15 @@ import Header from './components/Header'; 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'; export default class Coriolis extends React.Component { static childContextTypes = { language: React.PropTypes.object.isRequired, - route: React.PropTypes.object + sizeRatio: React.PropTypes.number.isRequired, + route: React.PropTypes.object.isRequired }; constructor() { @@ -24,19 +26,21 @@ 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._onLanguageChange = this._onLanguageChange.bind(this) + this._onLanguageChange = this._onLanguageChange.bind(this); + this._onSizeRatioChange = this._onSizeRatioChange.bind(this) this._keyDown = this._keyDown.bind(this); this.state = { page: null, language: getLanguage(Persist.getLangCode()), - route: null + route: null, + sizeRatio: Persist.getSizeRatio() }; Router('', (r) => this._setPage(ShipyardPage, r)); Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r)); - // Router('/compare/:name', compare); - // Router('/comparison/:code', comparison); + Router('/compare/:name?', (r) => this._setPage(ComparisonPage, r)); + Router('/comparison/:code', (r) => this._setPage(ComparisonPage, r)); Router('/about', (r) => this._setPage(AboutPage, r)); Router('*', (r) => this._setPage(null, r)); } @@ -54,6 +58,10 @@ export default class Coriolis extends React.Component { this.setState({ language: getLanguage(Persist.getLangCode()) }); } + _onSizeRatioChange(sizeRatio) { + this.setState({ sizeRatio }); + } + _keyDown(e) { switch (e.keyCode) { case 27: @@ -63,36 +71,59 @@ export default class Coriolis extends React.Component { } } + /** + * Opens the modal display with the specified content + * @param {React.Component} content Modal Content + */ _showModal(content) { let modal =
this._hideModal() }>{content}
; this.setState({ modal }); } + /** + * Hides any open modal + */ _hideModal() { if (this.state.modal) { this.setState({ modal: null }); } } + /** + * Sets the open menu state + * @param {string|object} currentMenu The reference to the current menu + */ _openMenu(currentMenu) { if (this.state.currentMenu != currentMenu) { this.setState({ currentMenu }); } } + /** + * Closes the open menu + */ _closeMenu() { if (this.state.currentMenu) { this.setState({ currentMenu: null }); } } + /** + * Creates the context to be passed down to pages / components containing + * language, sizeRatio and route references + * @return {object} Context to be passed down + */ getChildContext() { return { language: this.state.language, - route: this.state.route + route: this.state.route, + sizeRatio: this.state.sizeRatio }; } + /** + * Adds necessary listeners and starts Routing + */ componentWillMount() { // Listen for appcache updated event, present refresh to update view if (window.applicationCache) { @@ -107,7 +138,7 @@ export default class Coriolis extends React.Component { window.addEventListener('resize', InterfaceEvents.windowResized); document.addEventListener('keydown', this._keyDown); Persist.addListener('language', this._onLanguageChange); - 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); @@ -116,6 +147,10 @@ export default class Coriolis extends React.Component { Router.start(); } + /** + * Renders the main app + * @return {React.Component} The main app + */ render() { return (
diff --git a/src/app/components/ComparisonTable.jsx b/src/app/components/ComparisonTable.jsx new file mode 100644 index 00000000..5a7b9eb1 --- /dev/null +++ b/src/app/components/ComparisonTable.jsx @@ -0,0 +1,102 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import Link from './Link'; +import cn from 'classnames'; +import { SizeMap } from '../shipyard/Constants'; + + +export default class ComparisonTable extends TranslatedComponent { + + static propTypes = { + facets: React.PropTypes.array.isRequired, + 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 + } + + constructor(props, context) { + super(props, context); + this._buildHeaders = this._buildHeaders.bind(this); + + this.state = this._buildHeaders(props.facets, props.onSort, context.language.translate); + } + + _buildHeaders(facets, onSort, translate) { + let header = [ + {translate('ship')}, + {translate('build')} + ]; + let subHeader = []; + + for (let f of facets) { + if (f.active) { + let p = f.props; + let pl = p.length; + header.push( + {translate(f.title)} + ); + + if (pl > 1) { + for (let i = 0; i < pl; i++) { + subHeader.push({translate(f.lbls[i])}); + } + } + } + } + + return { header, subHeader }; + } + + _buildRow(build, facets, formats, units) { + let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`; + let cells = [ + {build.name}, + {build.buildName} + ]; + + for (let f of facets) { + if (f.active) { + for (let p of f.props) { + cells.push({formats[f.fmt](build[p])}{f.unit ? units[f.unit] : null}); + } + } + } + + return {cells}; + } + + componentWillReceiveProps(nextProps, nextContext) { + // If facets or language has changed re-render header + if (nextProps.facets != this.props.facets || nextContext.language != this.context.language) { + this.setState(this._buildHeaders(nextProps.facets, nextProps.onSort, nextContext.language.translate)); + } + } + + render() { + let { builds, facets } = this.props; + let { header, subHeader } = this.state; + let { formats, units } = this.context.language; + + let buildsRows = new Array(builds.length); + + for (let i = 0, l = buildsRows.length; i < l; i++) { + buildsRows[i] = this._buildRow(builds[i], facets, formats, units); + } + + return ( +
+ + + {header} + {subHeader} + + + {buildsRows} + +
+
+ ); + + } +} diff --git a/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx index ae226b30..7dffe493 100644 --- a/src/app/components/CostSection.jsx +++ b/src/app/components/CostSection.jsx @@ -11,28 +11,25 @@ export default class CostSection extends TranslatedComponent { static PropTypes = { ship: React.PropTypes.object.isRequired, - shipId: React.PropTypes.string.isRequired, code: React.PropTypes.string.isRequired, buildName: React.PropTypes.string }; constructor(props) { super(props); - this._costsTab = this._costsTab.bind(this); + this._sortCost = this._sortCost.bind(this); + this._sortAmmo = this._sortAmmo.bind(this); + this._sortRetrofit = this._sortRetrofit.bind(this); + this._buildRetrofitShip = this._buildRetrofitShip.bind(this); + this._onBaseRetrofitChange = this._onBaseRetrofitChange.bind(this); + this._defaultRetrofitName = this._defaultRetrofitName.bind(this); - let data = Ships[props.shipId]; // Retrieve the basic ship properties, slots and defaults - let retrofitName = props.buildName; + let data = Ships[props.ship.id]; // Retrieve the basic ship properties, slots and defaults + let retrofitName = this._defaultRetrofitName(props.ship.id, props.buildName); + let retrofitShip = this._buildRetrofitShip(props.ship.id, retrofitName); let shipDiscount = Persist.getShipDiscount(); let moduleDiscount = Persist.getModuleDiscount(); - let existingBuild = Persist.getBuild(props.shipId, retrofitName); - let retrofitShip = new Ship(props.shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison - - if (existingBuild) { - retrofitShip.buildFrom(existingBuild); // Populate modules from existing build - } else { - retrofitShip.buildWith(data.defaults); // Populate with default components - } this.props.ship.applyDiscounts(shipDiscount, moduleDiscount); retrofitShip.applyDiscounts(shipDiscount, moduleDiscount); @@ -45,16 +42,35 @@ export default class CostSection extends TranslatedComponent { total: props.ship.totalCost, insurance: Insurance[Persist.getInsurance()], tab: Persist.getCostTab(), - buildOptions: Persist.getBuildsNamesFor(props.shipId), - ammoPredicate: 'module', + buildOptions: Persist.getBuildsNamesFor(props.ship.id), + ammoPredicate: 'cr', ammoDesc: true, costPredicate: 'cr', costDesc: true, - retroPredicate: 'module', + retroPredicate: 'cr', retroDesc: true }; } + _buildRetrofitShip(shipId, name, retrofitShip) { + let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults + + if (!retrofitShip) { + retrofitShip = new Ship(shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison + } + + if (Persist.hasBuild(shipId, name)) { + retrofitShip.buildFrom(Persist.getBuild(shipId, name)); // Populate modules from existing build + } else { + retrofitShip.buildWith(data.defaults); // Populate with default components + } + return retrofitShip; + } + + _defaultRetrofitName(shipId, name) { + return Persist.hasBuild(shipId, name) ? name : null; + } + _showTab(tab) { Persist.setCostTab(tab); this.setState({ tab }); @@ -72,72 +88,121 @@ export default class CostSection extends TranslatedComponent { this.setState({ insurance: Insurance[insuranceName] }); } - _onBaseRetrofitChange(retrofitName) { - let existingBuild = Persist.getBuild(this.props.shipId, retrofitName); - this.state.retrofitShip.buildFrom(existingBuild); // Repopulate modules from existing build + /** + * Repopulate modules on retrofit ship from existing build + * @param {string} retrofitName Build name to base the retrofit ship on + */ + _onBaseRetrofitChange(event) { + let retrofitName = event.target.value; + let ship = this.props.ship; + + if (retrofitName) { + this.state.retrofitShip.buildFrom(Persist.getBuild(ship.id, retrofitName)); + } else { + this.state.retrofitShip.buildWith(Ships[ship.id].defaults); // Retrofit ship becomes stock build + } + this._updateRetrofit(ship, this.state.retrofitShip); this.setState({ retrofitName }); } _onBuildSaved(shipId, name, code) { if(this.state.retrofitName == name) { this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build + this._updateRetrofit(this.props.ship, this.state.retrofitShip); } else { - this.setState({buildOptions: Persist.getBuildsNamesFor(this.props.shipId) }); + this.setState({ buildOptions: Persist.getBuildsNamesFor(this.props.shipId) }); } } + _onBuildDeleted(shipId, name, code) { + if(this.state.retrofitName == name) { + this.state.retrofitShip.buildWith(Ships[shipId].defaults); // Retrofit ship becomes stock build + this.setState({ retrofitName: null }); + } + this.setState({ buildOptions: Persist.getBuildsNamesFor(shipId) }); + } + _toggleCost(item) { this.props.ship.setCostIncluded(item, !item.incCost); this.setState({ total: this.props.ship.totalCost }); } - _sortCost(predicate) { - let costList = this.props.ship.costList; + _toggleRetrofitCost(item) { + let retrofitTotal = this.state.retrofitTotal; + item.retroItem.incCost = !item.retroItem.incCost; + retrofitTotal += item.netCost * (item.retroItem.incCost ? 1 : -1); + this.setState({ retrofitTotal }); + } + + _sortCostBy(predicate) { let { costPredicate, costDesc } = this.state; - if (predicate) { - if (costPredicate == predicate) { - costDesc = !costDesc; - } - } else { - predicate = costPredicate; - } - - if (predicate == 'm') { - let translate = this.context.language.translate; - costList.sort(nameComparator(translate)); - } else { - costList.sort((a, b) => (a.m && a.m.cost ? a.m.cost : 0) - (b.m && b.m.cost ? b.m.cost : 0)); - } - - if (!costDesc) { - costList.reverse(); + if (costPredicate == predicate) { + costDesc = !costDesc; } this.setState({ costPredicate: predicate, costDesc }); } - _sortAmmo(predicate) { - let { ammoPredicate, ammoDesc, ammoCosts } = this.state; + _sortCost(ship, predicate, desc) { + let costList = ship.costList; + + if (predicate == 'm') { + costList.sort(nameComparator(this.context.language.translate)); + } 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(); + } + } + + _sortAmmoBy(predicate) { + let { ammoPredicate, ammoDesc } = this.state; if (ammoPredicate == predicate) { ammoDesc = !ammoDesc; } - switch (predicate) { - case 'm': - let translate = this.context.language.translate; - ammoCosts.sort(nameComparator(translate)); - break; - default: - ammoCosts.sort((a, b) => a[predicate] - b[predicate]); + this.setState({ ammoPredicate: predicate, ammoDesc }); + } + + _sortAmmo(ammoCosts, predicate, desc) { + + if (predicate == 'm') { + ammoCosts.sort(nameComparator(this.context.language.translate)); + } else { + ammoCosts.sort((a, b) => a[predicate] - b[predicate]); } - if (!ammoDesc) { + if (!desc) { ammoCosts.reverse(); } + } - this.setState({ ammoPredicate: predicate, ammoDesc }); + _sortRetrofitBy(predicate) { + let { retroPredicate, retroDesc } = this.state; + + if (retroPredicate == predicate) { + retroDesc = !retroDesc; + } + + this.setState({ retroPredicate: predicate, retroDesc }); + } + + _sortRetrofit(retrofitCosts, predicate, desc) { + let translate = this.context.language.translate; + + if (predicate == 'cr') { + retrofitCosts.sort((a, b) => a.netCost - b.netCost); + } else { + retrofitCosts.sort((a , b) => (a[predicate] ? translate(a[predicate]).toLowerCase() : '').localeCompare(b[predicate] ? translate(b[predicate]).toLowerCase() : '')); + } + + if (!desc) { + retrofitCosts.reverse(); + } } _costsTab() { @@ -162,12 +227,12 @@ export default class CostSection extends TranslatedComponent { - - + @@ -185,11 +250,72 @@ export default class CostSection extends TranslatedComponent { ; } - updateRetrofitCosts() { - var costs = $scope.retrofitList = []; - var total = 0, i, l, item; + _retrofitTab() { + let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state; + let { translate, formats, units } = this.context.language; + let int = formats.int; + let rows = [], options = []; - if (ship.bulkheads.id != retrofitShip.bulkheads.id) { + for (let opt of this.state.buildOptions) { + options.push(); + } + + if (retrofitCosts.length) { + for (let i = 0, l = retrofitCosts.length; i < l; i++) { + let item = retrofitCosts[i]; + rows.push( + + + + + + ); + } + } else { + rows = + } + + return
+
+
+ {translate('component')} - {shipDiscount < 1 && {`[${translate('ship')} -${formats.rPct(1 - shipDiscount)}]`}} - {moduleDiscount < 1 && {`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}} + {shipDiscount < 1 && {`[${translate('ship')} -${formats.pct1(1 - shipDiscount)}]`}} + {moduleDiscount < 1 && {`[${translate('modules')} -${formats.pct1(1 - moduleDiscount)}]`}} {translate('credits')}{translate('credits')}
{item.sellClassRating}{translate(item.sellName)}{item.buyClassRating}{translate(item.buyName)} 0 ? 'warning' : 'secondary-disabled' : 'disabled' )}>{int(item.netCost)}{units.CR}
{translate('PHRASE_NO_RETROCH')}
+ + + + + + + + + {rows} + + + + + + + + + + +
{translate('sell')}{translate('buy')} + {translate('net cost')} + {moduleDiscount < 1 && {`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}} +
{translate('cost')} 0 ? 'warning' : 'secondary-disabled')} style={{ borderBottom:'none' }}> + {int(retrofitTotal)}{units.CR} +
{translate('retrofit from')} + +
+
+ ; + } + + _updateRetrofit(ship, retrofitShip) { + let retrofitCosts = []; + var retrofitTotal = 0, i, l, item; + + if (ship.bulkheads.index != retrofitShip.bulkheads.index) { item = { buyClassRating: ship.bulkheads.m.class + ship.bulkheads.m.rating, buyName: ship.bulkheads.m.name, @@ -198,9 +324,9 @@ export default class CostSection extends TranslatedComponent { netCost: ship.bulkheads.discountedCost - retrofitShip.bulkheads.discountedCost, retroItem: retrofitShip.bulkheads }; - costs.push(item); + retrofitCosts.push(item); if (retrofitShip.bulkheads.incCost) { - total += item.netCost; + retrofitTotal += item.netCost; } } @@ -208,71 +334,28 @@ export default class CostSection extends TranslatedComponent { var retroSlotGroup = retrofitShip[g]; var slotGroup = ship[g]; for (i = 0, l = slotGroup.length; i < l; i++) { - if (slotGroup[i].id != retroSlotGroup[i].id) { + if (slotGroup[i].m != retroSlotGroup[i].m) { item = { netCost: 0, retroItem: retroSlotGroup[i] }; - if (slotGroup[i].id) { + if (slotGroup[i].m) { item.buyName = slotGroup[i].m.name || slotGroup[i].m.grp; item.buyClassRating = slotGroup[i].m.class + slotGroup[i].m.rating; item.netCost = slotGroup[i].discountedCost; } - if (retroSlotGroup[i].id) { + if (retroSlotGroup[i].m) { item.sellName = retroSlotGroup[i].m.name || retroSlotGroup[i].m.grp; item.sellClassRating = retroSlotGroup[i].m.class + retroSlotGroup[i].m.rating; item.netCost -= retroSlotGroup[i].discountedCost; } - costs.push(item); + retrofitCosts.push(item); if (retroSlotGroup[i].incCost) { - total += item.netCost; + retrofitTotal += item.netCost; } } } } - $scope.retrofitTotal = total; - } - _retrofitTab() { - // return
- //
- // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - //
{translate('sell')}{translate('buy')} - // {{'net cost' | translate}} [-{{fRPct(1 - discounts.components)}}] - //
{translate('PHRASE_NO_RETROCH')}
{{item.sellClassRating}}{{item.sellName | translate}}{{item.buyClassRating}}{{item.buyName | translate}} 0 ? 'warning' : 'secondary-disabled' : 'disabled' )}>{{ fCrd(item.netCost)}} CR
- //
- // - // - // - // - // - // - // - // - // - // - //
{translate('cost')} 0 ? 'warning' : 'secondary-disabled'}>{{fCrd(retrofitTotal)}} CR
{translate('retrofit from')} - // - //
- //
; + this.setState({ retrofitCosts, retrofitTotal }); + this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc); } _ammoTab() { @@ -291,16 +374,16 @@ export default class CostSection extends TranslatedComponent { {int(item.total)}{units.CR} ); } - console.log(rows); + return
- +
- - - - + + + + @@ -318,14 +401,13 @@ export default class CostSection extends TranslatedComponent { /** * Recalculate all ammo costs */ - _updateAmmoCosts() { - let ship = this.props.ship; - let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, scoop = false; + _updateAmmoCosts(ship) { + let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, srvs = 0, scoop = false; for (let g in { standard: 1, internal: 1, hardpoints: 1 }) { let slotGroup = ship[g]; for (let i = 0, l = slotGroup.length; i < l; i++) { - if (slotGroup[i].id) { + if (slotGroup[i].m) { //special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip q = 0; switch (slotGroup[i].m.grp) { @@ -338,6 +420,9 @@ export default class CostSection extends TranslatedComponent { case 'am': q = slotGroup[i].m.ammo; break; + case 'pv': + srvs += slotGroup[i].m.vehicles; + break; case 'fx': case 'hb': case 'cc': case 'pc': limpets = ship.cargoCapacity; break; @@ -370,6 +455,17 @@ export default class CostSection extends TranslatedComponent { ammoCosts.push(item); ammoTotal += item.total; } + + if (srvs > 0) { + item = { + m: { name: 'SRVs', class: '', rating: '' }, + max: srvs, + cost: 6005, + total: srvs * 6005 + }; + ammoCosts.push(item); + ammoTotal += item.total; + } //calculate refuel costs if no scoop present if (!scoop) { item = { @@ -381,26 +477,66 @@ export default class CostSection extends TranslatedComponent { ammoCosts.push(item); ammoTotal += item.total; } + this.setState({ ammoTotal, ammoCosts }); + this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc); } componentWillMount(){ this.listeners = [ Persist.addListener('discounts', this._onDiscountChanged.bind(this)), Persist.addListener('insurance', this._onInsuranceChanged.bind(this)), + Persist.addListener('buildSaved', this._onBuildSaved.bind(this)), + Persist.addListener('buildDeleted', this._onBuildDeleted.bind(this)) ]; this._updateAmmoCosts(this.props.ship); - this._sortCost.call(this, this.state.costPredicate); + this._updateRetrofit(this.props.ship, this.state.retrofitShip); + this._sortCost(this.props.ship); } componentWillReceiveProps(nextProps, nextContext) { - this._updateAmmoCosts(nextProps.ship); + let retrofitShip = this.state.retrofitShip; - this._sortCost(); + 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 ); + this.setState({ + retrofitShip, + retrofitName, + buildOptions: Persist.getBuildsNamesFor(nextId) + }); + } + + if (nextProps.ship != this.props.ship || nextProps.code != this.props.code) { + this._updateAmmoCosts(nextProps.ship); + this._updateRetrofit(nextProps.ship, retrofitShip); + this._sortCost(nextProps.ship); + } + } + + componentWillUpdate(nextProps, nextState) { + let state = this.state; + + switch (nextState.tab) { + case 'ammo': + if (state.ammoPredicate != nextState.ammoPredicate || state.ammoDesc != nextState.ammoDesc) { + this._sortAmmo(nextState.ammoCosts, nextState.ammoPredicate, nextState.ammoDesc); + } + break; + case 'retrofit': + if (state.retroPredicate != nextState.retroPredicate || state.retroDesc != nextState.retroDesc) { + this._sortRetrofit(nextState.retrofitCosts, nextState.retroPredicate, nextState.retroDesc); + } + break; + default: + if (state.costPredicate != nextState.costPredicate || state.costDesc != nextState.costDesc) { + this._sortCost(nextProps.ship, nextState.costPredicate, nextState.costDesc); + } + } } componentWillUnmount(){ - // remove window listener this.listeners.forEach(l => l.remove()); } diff --git a/src/app/components/Header.jsx b/src/app/components/Header.jsx index da8511b9..dd714b20 100644 --- a/src/app/components/Header.jsx +++ b/src/app/components/Header.jsx @@ -1,5 +1,7 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; +import { Languages } from '../i18n/Language'; +import { Insurance, Discounts } from '../shipyard/Constants'; import Link from './Link'; import ActiveLink from './ActiveLink'; import cn from 'classnames'; @@ -7,28 +9,58 @@ 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'; +import ModalExport from './ModalExport'; +import ModalImport from './ModalImport'; +import Slider from './Slider'; + +const SIZE_MIN = 0.65; +const SIZE_RANGE = 0.55; export default class Header extends TranslatedComponent { - constructor(props) { + constructor(props, context) { super(props); this.shipOrder = Object.keys(Ships).sort(); + + this._setLanguage = this._setLanguage.bind(this); + this._setInsurance = this._setInsurance.bind(this); + + this.languageOptions = []; + this.insuranceOptions = []; + this.discountOptions = []; + + let translate = context.language.translate; + + for (let langCode in Languages) { + this.languageOptions.push(); + } + + for (let name in Insurance) { + this.insuranceOptions.push(); + } + + for (let name in Discounts) { + this.discountOptions.push(); + } + } _setInsurance(e) { - e.stopPropagation(); - Persist.setInsurance('beta'); // TODO: get insurance name + Persist.setInsurance(e.target.value); } _setModuleDiscount(e) { - e.stopPropagation(); - Persist.setModuleDiscount(0); // TODO: get module discount + Persist.setModuleDiscount(e.target.value * 1); } _setShipDiscount(e) { - e.stopPropagation(); - Persist.setShipDiscount(0); // TODO: get ship discount + Persist.setShipDiscount(e.target.value * 1); + } + + _setLanguage(e){ + Persist.setLangCode(e.target.value); } _showDeleteAll(e) { @@ -37,29 +69,33 @@ export default class Header extends TranslatedComponent { }; _showBackup(e) { + let translate = this.context.language.translate; e.preventDefault(); - /*$state.go('modal.export', { - title: 'backup', - data: Persist.getAll(), - description: 'PHRASE_BACKUP_DESC' - });*/ - // TODO: implement modal + InterfaceEvents.showModal(); }; _showDetailedExport(e){ + let translate = this.context.language.translate; e.preventDefault(); - e.stopPropagation(); - /*$state.go('modal.export', { - title: 'detailed export', - data: Serializer.toDetailedExport(Persist.getBuilds()), - description: 'PHRASE_EXPORT_DESC' - });*/ - // TODO: implement modal + InterfaceEvents.showModal(); + } + + _showImport(e) { + e.preventDefault(); + InterfaceEvents.showModal(); } _setTextSize(size) { - Persist.setSizeRatio(size); // TODO: implement properly + Persist.setSizeRatio((size * SIZE_RANGE) + SIZE_MIN); } _resetTextSize() { @@ -117,9 +153,8 @@ export default class Header extends TranslatedComponent { let translate = this.context.language.translate; if (Persist.hasComparisons()) { - let comparisons = []; + comparisons = []; let comps = Object.keys(Persist.getComparisons()).sort(); - console.log(comps); for (let name of comps) { comparisons.push({name}); @@ -145,38 +180,54 @@ export default class Header extends TranslatedComponent {
e.stopPropagation() }>
    {translate('language')} -
  • +
  • + +

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

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

    - {translate('component')} {translate('discount')} -
  • + {translate('module')} {translate('discount')} +
  • + +


{translate('module')}{translate('qty')}{translate('unit cost')}{translate('total cost')}{translate('module')}{translate('qty')}{translate('unit cost')}{translate('total cost')}
- - - + + + - +
AAAA
{translate('reset')}
@@ -186,6 +237,22 @@ export default class Header extends TranslatedComponent { ); } + componentWillMount(){ + Persist.addListener('language', () => this.forceUpdate()); + Persist.addListener('insurance', () => this.forceUpdate()); + Persist.addListener('discounts', () => this.forceUpdate()); + } + + componentWillReceiveProps(nextProps, nextContext) { + if(this.context.language != nextContext.language) { + let translate = nextContext.language.translate; + this.insuranceOptions = []; + for (let name in Insurance) { + this.insuranceOptions.push(); + } + } + } + render() { let translate = this.context.language.translate; let openedMenu = this.props.currentMenu; diff --git a/src/app/components/InternalSlot.jsx b/src/app/components/InternalSlot.jsx index cff5d53e..8e5c5e09 100644 --- a/src/app/components/InternalSlot.jsx +++ b/src/app/components/InternalSlot.jsx @@ -16,6 +16,7 @@ export default class InternalSlot extends Slot { { 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 } diff --git a/src/app/components/InternalSlotSection.jsx b/src/app/components/InternalSlotSection.jsx index 079edd8b..29305568 100644 --- a/src/app/components/InternalSlotSection.jsx +++ b/src/app/components/InternalSlotSection.jsx @@ -83,7 +83,6 @@ export default class InternalSlotSection extends SlotSection { enabled={s.enabled} m={s.m} fuel={fuelCapacity} - shipMass={ladenMass} />); } diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx index 21512762..27e117da 100644 --- a/src/app/components/LineChart.jsx +++ b/src/app/components/LineChart.jsx @@ -1,7 +1,10 @@ import React from 'react'; +import { findDOMNode } from 'react-dom'; +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 } export default class LineChart extends TranslatedComponent { @@ -12,40 +15,201 @@ export default class LineChart extends TranslatedComponent { } static PropTypes = { - xMax: React.PropTypes.number.isRequired, - yMax: React.PropTypes.number.isRequired, + width: React.PropTypes.number.isRequired, func: React.PropTypes.func.isRequired, + xLabel: React.PropTypes.string.isRequired, + xMin: React.PropTypes.number, + xMax: React.PropTypes.number.isRequired, + xUnit: React.PropTypes.string.isRequired, + yLabel: React.PropTypes.string.isRequired, + yMin: React.PropTypes.number, + yMax: React.PropTypes.number.isRequired, + yUnit: React.PropTypes.string.isRequired, series: React.PropTypes.array, colors: React.PropTypes.array, - xMin: React.PropTypes.number, - yMin: React.PropTypes.number, - xUnit: React.PropTypes.string, - yUnit: React.PropTypes.string, - xLabel: React.PropTypes.string, - xLabel: React.PropTypes.string, }; - constructor(props) { + constructor(props, context) { super(props); - // init + this._updateDimensions = this._updateDimensions.bind(this); + this._updateSeriesData = this._updateSeriesData.bind(this); + this._tooltip = this._tooltip.bind(this); + this._showTip = this._showTip.bind(this); + this._hideTip = this._hideTip.bind(this); + this._moveTip = this._moveTip.bind(this); + let markerElems = []; + let detailElems = []; + let xScale = 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.yAxis = d3.svg.axis().scale(yScale).ticks(6).outerTickSize(0).orient('left'); + + for(let i = 0, l = series ? series.length : 1; i < l; i++) { + let yAccessor = series ? function(d) { return yScale(d[1][this]); }.bind(series[i]) : (d) => yScale(d[1]); + seriesLines.push(d3.svg.line().x((d) => xScale(d[0])).y(yAccessor)); + detailElems.push(); + markerElems.push(); + } + + this.state = { + xScale, + yScale, + seriesLines, + detailElems, + markerElems, + tipHeight: 2 + (1.25 * (series ? series.length : 0.75)) + }; + } + + _tooltip(xPos) { + let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; + let { xScale, yScale } = 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), + tipWidth = 0, + tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; + + tips.selectAll('text.label.y').text(function(d, i) { + let yVal = series ? y0[series[i]] : y0; + yTotal += yVal; + return (series ? translate(series[i]) : '') + ' ' + formats.f2(yVal); + }).append('tspan').attr('class', 'metric').text(' ' + yUnit); + + tips.selectAll('text').each(function() { + if (this.getBBox().width > tipWidth) { + tipWidth = Math.ceil(this.getBBox().width); + } + }); + + let tipY = Math.floor(yScale(yTotal / (series ? series.length : 1)) - (tipHeightPx / 2)); + + tipWidth += 8; + tips.attr('transform', 'translate(' + xPos + ',' + tipY + ')'); + tips.selectAll('text.label').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start'); + tips.selectAll('text.label.x').text(formats.f2(x0)).append('tspan').attr('class', 'metric').text(' ' + xUnit); + tips.selectAll('rect').attr('width', tipWidth + 4).attr('x', flip ? -tipWidth - 12 : 8).style('text-anchor', flip ? 'end' : 'start'); + this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0)); + } + + _updateDimensions(props, sizeRatio) { + let { width, xMax, xMin, yMin, yMax } = props; + let innerWidth = width - MARGIN.left - MARGIN.right; + let outerHeight = Math.round(width * 0.5 * sizeRatio); + let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; + + this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); + this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax]); + this.setState({ innerWidth, outerHeight, innerHeight }); + } + + _showTip(e) { + this._moveTip(e); + this.tipContainer.style('display', null); + this.markersContainer.style('display', null); + } + + _moveTip(e) { + this._tooltip(Math.round(e.clientX - e.target.getBoundingClientRect().left)); + } + + _hideTip() { + this.tipContainer.style('display', 'none'); + this.markersContainer.style('display', 'none'); + } + + _updateSeriesData(props) { + let { func, xMin, xMax, series } = props; + let delta = (xMax - xMin) / RENDER_POINTS; + let seriesData = new Array(RENDER_POINTS); + + if (delta) { + seriesData = new Array(RENDER_POINTS); + for (let i = 0, x = xMin; i < RENDER_POINTS; i++) { + seriesData[i] = [ x, func(x) ]; + x += delta; + } + seriesData[RENDER_POINTS - 1] = [ xMax, func(xMax) ]; + } else { + let yVal = func(xMin); + seriesData = [ [0, yVal], [1, yVal]]; + } + + this.setState({ seriesData }); } componentWillMount(){ - // Listen to window resize - } - - componentWillUnmount(){ - // remove window listener - // remove mouse move listener / touch listner? + this._updateDimensions(this.props, this.context.sizeRatio); + this._updateSeriesData(this.props); } componentWillReceiveProps(nextProps, nextContext) { - // on language change update formatting + let { func, xMin, xMax, yMin, yMax, width } = nextProps; + let props = this.props; + + let domainChanged = xMax != props.xMax || xMin != props.xMin || yMax != props.yMax || yMin != props.yMin || func != props.func; + + if (width != props.width || domainChanged || this.context.sizeRatio != nextContext.sizeRatio) { + this._updateDimensions(nextProps, nextContext.sizeRatio); + } + + if (domainChanged) { + this._updateSeriesData(nextProps); + } } render() { - return
; + if (!this.props.width) { + return null; + } + + let { xLabel, yLabel, xUnit, yUnit, colors } = this.props; + let { innerWidth, outerHeight, innerHeight, tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; + let line = this.line; + let lines = seriesLines.map((line, i) => ); + + return + + {lines} + d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> + + {xLabel} + {` (${xUnit})`} + + + d3.select(elem).call(this.yAxis)}> + + {yLabel} + {` (${yUnit})`} + + + this.tipContainer = d3.select(g)} className='tooltip' style={{ display: 'none' }}> + + {detailElems} + + this.markersContainer = d3.select(g)} style={{ display: 'none' }}> + {markerElems} + + + + ; } } diff --git a/src/app/components/ModalCompare.jsx b/src/app/components/ModalCompare.jsx new file mode 100644 index 00000000..1c7eea73 --- /dev/null +++ b/src/app/components/ModalCompare.jsx @@ -0,0 +1,108 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import InterfaceEvents from '../utils/InterfaceEvents'; +import { Ships } from 'coriolis-data'; +import Persist from '../stores/Persist'; + +function buildComparator(a, b) { + if (a.name == b.name) { + return a.buildName > b.buildName; + } + return a.name > b.name; +} + + +export default class ModalCompare extends TranslatedComponent { + + static propTypes = { + onSelect: React.PropTypes.func.isRequired, + builds: React.PropTypes.array + }; + + static defaultProps = { + builds: [] + } + + constructor(props) { + super(props); + let builds = props.builds; + let allBuilds = Persist.getBuilds(); + let unusedBuilds = []; + + 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 }) + } + } + } + + builds.sort(buildComparator); + unusedBuilds.sort(buildComparator); + + this.state = { builds, unusedBuilds }; + } + + _addBuild(buildIndex) { + let { builds, unusedBuilds } = this.state; + builds.push(unusedBuilds[buildIndex]); + unusedBuilds = unusedBuilds.splice(buildIndex, 1); + builds.sort(buildComparator); + + this.setState({ builds, unusedBuilds }); + } + + _removeBuild(buildIndex) { + let { builds, unusedBuilds } = this.state; + unusedBuilds.push(builds[buildIndex]); + builds = builds.splice(buildIndex, 1); + unusedBuilds.sort(buildComparator); + + this.setState({ builds, unusedBuilds }); + } + + _selectBuilds() { + this.props.onSelect(this.state.builds); + } + + render() { + let { builds, unusedBuilds } = this.state; + let translate = this.context.language.translate; + + let availableBuilds = unusedBuilds.map((build, i) => + + {build.name} + {build.buildName} + + ); + + let selectedBuilds = builds.map((build, i) => + + {build.name}< + td className='tl'>{build.buildName} + + ); + + return
e.stopPropagation() }> +

{translate('PHRASE_SELECT_BUILDS')}

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

+ + + + {selectedBuilds} + +
{translate('added')}
+
+
+ + +
; + } +} diff --git a/src/app/components/ModalExport.jsx b/src/app/components/ModalExport.jsx index f84c3d10..a7a1759e 100644 --- a/src/app/components/ModalExport.jsx +++ b/src/app/components/ModalExport.jsx @@ -7,7 +7,7 @@ export default class ModalExport extends TranslatedComponent { static propTypes = { title: React.PropTypes.string, promise: React.PropTypes.func, - data: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object]) + data: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object, React.PropTypes.array]) }; constructor(props) { diff --git a/src/app/components/ModalImport.jsx b/src/app/components/ModalImport.jsx index 4892a711..e1b01088 100644 --- a/src/app/components/ModalImport.jsx +++ b/src/app/components/ModalImport.jsx @@ -3,7 +3,7 @@ import cn from 'classnames'; import TranslatedComponent from './TranslatedComponent'; import InterfaceEvents from '../utils/InterfaceEvents'; import Persist from '../stores/Persist'; -import Ships from '../shipyard/Ships'; +import { Ships } from 'coriolis-data'; import Ship from '../shipyard/Ship'; import * as ModuleUtils from '../shipyard/ModuleUtils'; import { Download } from './SvgIcons'; @@ -36,7 +36,8 @@ function validateBuild(shipId, code, name) { throw shipData.properties.name + ' build "' + name + '" is not valid!'; } try { - Serializer.toShip(new Ship(shipId, shipData.properties, shipData.slots), code); + let ship = new Ship(shipId, shipData.properties, shipData.slots); + ship.buildFrom(code); } catch (e) { throw shipData.properties.name + ' build "' + name + '" is not valid!'; } @@ -58,17 +59,11 @@ function detailedJsonToBuild(detailedBuild) { throw detailedBuild.ship + ' Build "' + detailedBuild.name + '": Invalid data'; } - return { shipId: ship.id, name: detailedBuild.name, code: Serializer.fromShip(ship) }; + return { shipId: ship.id, name: detailedBuild.name, code: ship.toString() }; } export default class ModalImport extends TranslatedComponent { - static propTypes = { - title: React.propTypes.string, - promise: React.propTypes.func, - data: React.propTypes.oneOfType[React.propTypes.string, React.propTypes.object] - }; - constructor(props) { super(props); @@ -84,7 +79,11 @@ export default class ModalImport extends TranslatedComponent { }; this._process = this._process.bind(this); + this._import = this._import.bind(this); this._importBackup = this._importBackup.bind(this); + this._importDetailedArray = this._importDetailedArray.bind(this); + this._importTextBuild = this._importTextBuild.bind(this); + this._validateImport = this._validateImport.bind(this); } _importBackup(importData) { @@ -121,7 +120,7 @@ export default class ModalImport extends TranslatedComponent { _importDetailedArray(importArr) { let builds = {}; for (let i = 0, l = importArr.length; i < l; i++) { - let build = this._detailedJsonToBuild(importArr[i]); + let build = detailedJsonToBuild(importArr[i]); if (!builds[build.shipId]) { builds[build.shipId] = {}; } @@ -226,16 +225,17 @@ export default class ModalImport extends TranslatedComponent { this.setState({ builds }); } - _validateImport() { + _validateImport(e) { let importData = null; - let importString = $scope.importString.trim(); + let importString = e.target.value; this.setState({ builds: null, comparisons: null, discounts: null, errorMsg: null, importValid: false, - insurance: null + insurance: null, + importString, }); if (!importString) { @@ -246,7 +246,7 @@ export default class ModalImport extends TranslatedComponent { if (textBuildRegex.test(importString)) { // E:D Shipyard build text importTextBuild(importString); } else { // JSON Build data - importData = angular.fromJson($scope.importString); + importData = JSON.parse(importString); if (!importData || typeof importData != 'object') { throw 'Must be an object or array!'; @@ -272,7 +272,7 @@ export default class ModalImport extends TranslatedComponent { let builds = null, comparisons = null; if (this.state.builds) { - builds = $scope.builds; + builds = this.state.builds; for (let shipId in builds) { for (let buildName in builds[shipId]) { let code = builds[shipId][buildName]; @@ -286,7 +286,7 @@ export default class ModalImport extends TranslatedComponent { } if (this.state.comparisons) { - let comparisons = $scope.comparisons; + let comparisons = this.state.comparisons; for (let name in comparisons) { comparisons[name].useName = name; } @@ -341,19 +341,20 @@ export default class ModalImport extends TranslatedComponent { render() { let translate = this.context.language.translate; + let state = this.state; let importStage; - if (this.state.processed) { + if (!state.processed) { importStage = (
-