React port nearing completetion. Adding tests

This commit is contained in:
Colin McLeod
2016-01-27 00:52:57 -08:00
parent 8227a4e361
commit b42a812a45
23 changed files with 362 additions and 298 deletions

View File

@@ -238,13 +238,11 @@ export default class Coriolis extends React.Component {
* @return {React.Component} The main app
*/
render() {
return (
<div onClick={this._closeMenu}>
<Header appCacheUpdate={this.state.appCacheUpdate} currentMenu={this.state.currentMenu} />
{ this.state.page ? <this.state.page currentMenu={this.state.currentMenu} /> : <NotFoundPage/> }
{ this.state.modal }
{ this.state.tooltip }
</div>
);
return <div onClick={this._closeMenu}>
<Header appCacheUpdate={this.state.appCacheUpdate} currentMenu={this.state.currentMenu} />
{ this.state.page ? <this.state.page currentMenu={this.state.currentMenu} /> : <NotFoundPage/> }
{ this.state.modal }
{ this.state.tooltip }
</div>;
}
}

View File

@@ -5,6 +5,7 @@ import cn from 'classnames';
/**
* Returns true if the current window location equals the link
* @param {string} href URL/Href
* @return {boolean} If matches
*/
function isActive(href) {

View File

@@ -613,6 +613,7 @@ export default class CostSection extends TranslatedComponent {
if (nextProps.ship != this.props.ship || nextProps.code != this.props.code) {
this._updateAmmoCosts(nextProps.ship);
this._updateRetrofit(nextProps.ship, retrofitShip);
this.setState({ total: nextProps.ship.totalCost });
this._sortCost(nextProps.ship);
}
}

View File

@@ -260,7 +260,7 @@ export default class Header extends TranslatedComponent {
<br/>
<span className='cap ptr' onClick={this._toggleTooltips} >
{translate('tooltips')}
<div className={cn({ disabled: !tips, 'primary-disabled': tips })} style={{ marginLeft: '0.5em', display: 'inline-block' }}>{(tips ? '' : '')}</div>
<div className={cn({ disabled: !tips, 'primary-disabled': tips })} style={{ marginLeft: '0.5em', display: 'inline-block' }}>{(tips ? '' : '')}</div>
</span>
<br/>
{translate('insurance')}

View File

@@ -88,10 +88,13 @@ export default class LineChart extends TranslatedComponent {
y0 = func(x0),
tips = this.tipContainer,
yTotal = 0,
flip = (xPos / innerWidth > 0.65),
flip = (xPos / innerWidth > 0.60),
tipWidth = 0,
tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height;
xPos = xScale(x0); // Clamp xPos
tips.selectAll('text.label.y').text(function(d, i) {
let yVal = series ? y0[series[i]] : y0;
yTotal += yVal;
@@ -110,7 +113,7 @@ export default class LineChart extends TranslatedComponent {
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');
tips.selectAll('rect').attr('width', tipWidth + 4).attr('x', flip ? -tipWidth - 12 : 8).attr('y', 0).style('text-anchor', flip ? 'end' : 'start');
this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0));
}
@@ -136,9 +139,10 @@ export default class LineChart extends TranslatedComponent {
* @param {SyntheticEvent} e Event
*/
_showTip(e) {
this._moveTip(e);
e.preventDefault();
this.tipContainer.style('display', null);
this.markersContainer.style('display', null);
this._moveTip(e);
}
/**
@@ -146,13 +150,16 @@ export default class LineChart extends TranslatedComponent {
* @param {SyntheticEvent} e Event
*/
_moveTip(e) {
this._tooltip(Math.round(e.clientX - e.target.getBoundingClientRect().left));
let clientX = e.touches ? e.touches[0].clientX : e.clientX;
this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left));
}
/**
* Hide tooltip
* @param {SyntheticEvent} e Event
*/
_hideTip() {
_hideTip(e) {
e.preventDefault();
this.tipContainer.style('display', 'none');
this.markersContainer.style('display', 'none');
}
@@ -238,8 +245,8 @@ export default class LineChart extends TranslatedComponent {
<tspan className='metric'>{` (${yUnit})`}</tspan>
</text>
</g>
<g ref={(g) => this.tipContainer = d3.select(g)} className='tooltip' style={{ display: 'none' }}>
<rect className='tip' style={{ height: tipHeight + 'em' }}></rect>
<g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}>
<rect className='tooltip' height={tipHeight + 'em'}></rect>
{detailElems}
</g>
<g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}>

View File

@@ -4,7 +4,7 @@ import TranslatedComponent from './TranslatedComponent';
import Persist from '../stores/Persist';
import { Ships } from 'coriolis-data';
import Ship from '../shipyard/Ship';
import { ModuleNameToGroup } from '../shipyard/Constants';
import { ModuleNameToGroup, Insurance } from '../shipyard/Constants';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import { fromDetailedBuild } from '../shipyard/Serializer';
import { Download } from './SvgIcons';
@@ -146,8 +146,14 @@ export default class ModalImport extends TranslatedComponent {
if (importData.discounts instanceof Array && importData.discounts.length == 2) {
this.setState({ discounts: importData.discounts });
}
if (typeof importData.insurance == 'string' && importData.insurance.length > 3) {
this.setState({ insurance: importData.insurance });
if (typeof importData.insurance == 'string') {
let insurance = importData.insurance.toLowerCase();
if (Insurance[insurance] !== undefined) {
this.setState({ insurance });
} else {
throw 'Invalid insurance type: ' + insurance;
}
}
}
@@ -220,7 +226,7 @@ export default class ModalImport extends TranslatedComponent {
if (!slot) { throw 'No hardpoint slot available for: "' + line + '"'; }
group = ModuleNameToGroup[name.trim()];
group = ModuleNameToGroup[name.toLowerCase()];
let hp = ModuleUtils.findHardpoint(group, cl, rating, group ? null : name, mount, missile);
@@ -238,7 +244,7 @@ export default class ModalImport extends TranslatedComponent {
if (ship.standard[standardIndex].maxClass < cl) { throw name + ' exceeds max class for the ' + ship.name; }
ship.use(ship.standard[standardIndex], cl + rating, ModuleUtils.standard(standardIndex, cl + rating), true);
ship.use(ship.standard[standardIndex], ModuleUtils.standard(standardIndex, cl + rating), true);
} else {
throw 'Unknown component: "' + line + '"';
}
@@ -249,13 +255,13 @@ export default class ModalImport extends TranslatedComponent {
if (!slot) { throw 'No internal slot available for: "' + line + '"'; }
group = ModuleNameToGroup[name.trim()];
group = ModuleNameToGroup[name.toLowerCase()];
let intComp = ModuleUtils.findInternal(group, cl, rating, group ? null : name);
if (!intComp) { throw 'Unknown component: "' + line + '"'; }
ship.use(slot, intComp.id, intComp);
ship.use(slot, intComp);
}
}
@@ -272,7 +278,7 @@ export default class ModalImport extends TranslatedComponent {
*/
_validateImport(event) {
let importData = null;
let importString = event.target.value;
let importString = event.target.value.trim();
this.setState({
builds: null,
comparisons: null,
@@ -334,7 +340,7 @@ export default class ModalImport extends TranslatedComponent {
}
if (this.state.comparisons) {
let comparisons = this.state.comparisons;
comparisons = this.state.comparisons;
for (let name in comparisons) {
comparisons[name].useName = name;
}
@@ -371,8 +377,9 @@ export default class ModalImport extends TranslatedComponent {
}
}
if (this.state.discounts) {
Persist.setDiscount(this.state.discounts);
if (this.state.discounts && this.state.discounts.length == 2) {
Persist.setShipDiscount(this.state.discounts[0]);
Persist.setModuleDiscount(this.state.discounts[1]);
}
if (this.state.insurance) {
@@ -384,11 +391,11 @@ export default class ModalImport extends TranslatedComponent {
/**
* Capture build name changes
* @param {Object} build Build import object
* @param {Object} item Build/Comparison import object
* @param {SyntheticEvent} e Event
*/
_changeBuildName(build, e) {
build.useName = e.target.value;
_changeName(item, e) {
item.useName = e.target.value;
this.forceUpdate();
}
@@ -415,7 +422,7 @@ export default class ModalImport extends TranslatedComponent {
importStage = (
<div>
<textarea className='cb json' onChange={this._validateImport} defaultValue={this.state.importString} placeholder={translate('PHRASE_IMPORT')} />
<button className='l cap' onClick={this._process} disabled={!state.importValid} >{translate('proceed')}</button>
<button id='proceed' className='l cap' onClick={this._process} disabled={!state.importValid} >{translate('proceed')}</button>
<div className='l warning' style={{ marginLeft:'3em' }}>{state.errorMsg}</div>
</div>
);
@@ -426,14 +433,14 @@ export default class ModalImport extends TranslatedComponent {
for (let name in state.comparisons) {
let comparison = state.comparisons[name];
let hasComparison = Persist.hasComparison(name);
let hasComparison = Persist.hasComparison(comparison.useName);
comparisonRows.push(
<tr key={name} className='cb'>
<td>
<input type='text' value={comparison.useName}/>
<input type='text' onChange={this._changeName.bind(this, comparison)} value={comparison.useName}/>
</td>
<td style={{ textAlign:'center' }} className={ cn('cap', { warning: hasComparison, disabled: comparison.useName == '' }) }>
{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}>
{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}
</td>
</tr>
);
@@ -467,7 +474,7 @@ export default class ModalImport extends TranslatedComponent {
buildRows.push(
<tr key={shipId + buildName} className='cb'>
<td>{Ships[shipId].properties.name}</td>
<td><input type='text' onChange={this._changeBuildName.bind(this, b)} value={b.useName}/></td>
<td><input type='text' onChange={this._changeName.bind(this, b)} value={b.useName}/></td>
<td style={{ textAlign: 'center' }} className={cn('cap', { warning: hasBuild, disabled: b.useName == '' })}>
{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}
</td>
@@ -491,7 +498,7 @@ export default class ModalImport extends TranslatedComponent {
</tbody>
</table>
{comparisonTable}
<button className='cl l' onClick={this._import}><Download/> {translate('import')}</button>
<button id='import' className='cl l' onClick={this._import}><Download/> {translate('import')}</button>
{edit}
</div>
);

View File

@@ -130,9 +130,9 @@ export default class PowerManagement extends TranslatedComponent {
<td className='ptr le shorten cap' onClick={toggleEnabled}>{slotName(translate, slot)}</td>
<td className='ptr' onClick={toggleEnabled}><u>{translate(slot.type)}</u></td>
<td>
<span className='flip ptr' onClick={this._priority.bind(this, slot, -1)}>&#9658;</span>
<span className='flip ptr btn' onClick={this._priority.bind(this, slot, -1)}>&#9658;</span>
{' ' + (slot.priority + 1) + ' '}
<span className='ptr' onClick={this._priority.bind(this, slot, 1)}>&#9658;</span>
<span className='ptr btn' onClick={this._priority.bind(this, slot, 1)}>&#9658;</span>
</td>
<td className='ri ptr' style={{ width: '3.25em' }} onClick={toggleEnabled}>{pwr(m.power)}</td>
<td className='ri ptr' style={{ width: '3em' }} onClick={toggleEnabled}><u>{pct(m.power / ship.powerAvailable)}</u></td>

View File

@@ -32,29 +32,35 @@ export default class Slider extends React.Component {
/**
* On Mouse down handler
* @param {SyntheticEvent} e Event
* @param {SyntheticEvent} event Event
*/
down(e) {
let rect = e.currentTarget.getBoundingClientRect();
this.move = this._updatePercent.bind(this, rect.left, rect.width);
this.move(e);
document.addEventListener('mousemove', this.move);
document.addEventListener('mouseup', this.up);
down(event) {
if (this.move) {
this.up(event);
} else {
let rect = event.currentTarget.getBoundingClientRect();
this.move = this._updatePercent.bind(this, rect.left, rect.width);
this.move(event);
document.addEventListener('mousemove', this.move, true);
document.addEventListener('mouseup', this.up, true);
}
}
/**
* On Mouse up handler
* @param {Event} event DOM Event
*/
up() {
document.removeEventListener('mousemove', this.move);
document.removeEventListener('mouseup', this.up);
up(event) {
document.removeEventListener('mousemove', this.move, true);
document.removeEventListener('mouseup', this.up, true);
this.move = null;
}
/**
* Update the slider percentage
* @param {number} left Slider left position
* @param {number} width Slider width
* @param {Event} event Event
* @param {Event} event DOM Event
*/
_updatePercent(left, width, event) {
this.props.onChange(Math.min(Math.max((event.clientX - left) / width, 0), 1));
@@ -88,7 +94,7 @@ export default class Slider extends React.Component {
<rect className='primary' style={{ opacity: 0.3 }} y='0.25em' rx='0.3em' ry='0.3em' width='100%' height='0.7em' />
<rect className='primary-disabled'y='0.45em' rx='0.15em' ry='0.15em' width={pctStr} height='0.3em' />
<circle className='primary' r='0.6em' cy='0.6em' cx={pctStr} />
<rect width='100%' height='100%' fillOpacity='0' onMouseDown={this.down}/>
<rect width='100%' height='100%' fillOpacity='0' onMouseDown={this.down} onClick={this.click} />
{axisGroup}
</svg>;
}

View File

@@ -328,10 +328,10 @@ export default class OutfittingPage extends Page {
</div>
<div className='group half'>
<table style={{ width: '100%', lineHeight: '1em' }}>
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody >
<tr>
<td style={{ verticalAlign: 'top', padding:0 }}><Fuel className='xl primary-disabled' /></td>
<td style={{ verticalAlign: 'top', padding: 0, width: '2.5em' }}><Fuel className='xl primary-disabled' /></td>
<td><Slider axis={true} onChange={this._fuelChange} axisUnit={translate('T')} percent={fuelLevel} max={fuelCapacity} /></td>
<td className='primary' style={{ width: '10em', verticalAlign: 'top', fontSize: '0.9em' }}>{formats.f2(fuelLevel * fuelCapacity)}{units.T} {formats.pct1(fuelLevel)}</td>
</tr>

View File

@@ -415,12 +415,10 @@ export default class Ship {
this.updatePower()
.updateJumpStats()
.updateShieldStrength()
.updateTopSpeed()
.updatePowerPrioritesString()
.updatePowerEnabledString();
.updateTopSpeed();
}
return this;
return this.updatePowerPrioritesString().updatePowerEnabledString();
}
/**
@@ -570,12 +568,12 @@ export default class Ship {
if (newPriority >= 0 && newPriority < this.priorityBands.length) {
let oldPriority = slot.priority;
slot.priority = newPriority;
this.updatePowerPrioritesString();
if (slot.enabled) { // Only update power if the slot is enabled
let usage = powerUsageType(slot, slot.m);
this.priorityBands[oldPriority][usage] -= slot.m.power;
this.priorityBands[newPriority][usage] += slot.m.power;
this.updatePowerPrioritesString();
this.updatePower();
}
return true;

View File

@@ -14,8 +14,8 @@ let LS;
// Safe check to determine if localStorage is enabled
try {
localStorage.setItem('s', 1);
localStorage.removeItem('s');
localStorage.setItem('test_string', 1);
localStorage.removeItem('test_string');
LS = localStorage;
} catch(e) {
LS = null;
@@ -38,7 +38,7 @@ function _put(key, value) {
* @return {string} The stored string
*/
function _getString(key) {
return LS.getItem(key);
return LS ? LS.getItem(key) : null;
}
/**
@@ -80,7 +80,7 @@ class Persist extends EventEmitter {
this.comparisons = comparisonJson ? comparisonJson : {};
this.buildCount = Object.keys(this.builds).length;
this.langCode = _getString(LS_KEY_LANG) || 'en';
this.insurance = _getString(LS_KEY_INSURANCE);
this.insurance = _getString(LS_KEY_INSURANCE) || 'standard';
this.discounts = _get(LS_KEY_DISCOUNTS) || [1, 1];
this.costTab = _getString(LS_KEY_COST_TAB);
this.state = _get(LS_KEY_STATE);
@@ -129,16 +129,13 @@ class Persist extends EventEmitter {
* @param {string} code The serialized code
*/
saveBuild(shipId, name, code) {
if (LS) {
if (!this.builds[shipId]) {
this.builds[shipId] = {};
}
let newBuild = !this.builds[shipId][name];
this.builds[shipId][name] = code;
_put(LS_KEY_BUILDS, this.builds);
this.emit('buildSaved', shipId, name, code);
if (!this.builds[shipId]) {
this.builds[shipId] = {};
}
};
this.builds[shipId][name] = code;
_put(LS_KEY_BUILDS, this.builds);
this.emit('buildSaved', shipId, name, code);
}
/**
* Get the serialized code/string for a build. Returns null if a
@@ -153,7 +150,7 @@ class Persist extends EventEmitter {
return this.builds[shipId][name];
}
return null;
};
}
/**
* Get all builds (object) or builds for a specific ship (array)
@@ -225,7 +222,7 @@ class Persist extends EventEmitter {
_put(LS_KEY_COMPARISONS, this.comparisons);
this.emit('buildDeleted', shipId, name);
}
};
}
/**
* Persist a comparison in localstorage.
@@ -244,7 +241,7 @@ class Persist extends EventEmitter {
};
_put(LS_KEY_COMPARISONS, this.comparisons);
this.emit('comparisons', this.comparisons);
};
}
/**
* [getComparison description]
@@ -256,7 +253,7 @@ class Persist extends EventEmitter {
return this.comparisons[name];
}
return null;
};
}
/**
* Get all saved comparisons
@@ -293,7 +290,7 @@ class Persist extends EventEmitter {
_put(LS_KEY_COMPARISONS, this.comparisons);
this.emit('comparisons', this.comparisons);
}
};
}
/**
* Delete all builds and comparisons from localStorage
@@ -304,7 +301,7 @@ class Persist extends EventEmitter {
_delete(LS_KEY_BUILDS);
_delete(LS_KEY_COMPARISONS);
this.emit('deletedAll');
};
}
/**
* Get all saved data and settings
@@ -317,25 +314,25 @@ class Persist extends EventEmitter {
data[LS_KEY_INSURANCE] = this.getInsurance();
data[LS_KEY_DISCOUNTS] = this.discounts;
return data;
};
}
/**
* Get the saved insurance type
* @return {string} The name of the saved insurance type of null
*/
getInsurance() {
return this.insurance;
};
return this.insurance.toLowerCase();
}
/**
* Persist selected insurance type
* @param {string} insurance Insurance type name
*/
setInsurance(insurance) {
this.insurance = insurance;
this.insurance = insurance.toLowerCase();
_put(LS_KEY_INSURANCE, insurance);
this.emit('insurance', insurance);
};
}
/**
* Persist selected ship discount
@@ -345,7 +342,7 @@ class Persist extends EventEmitter {
this.discounts[0] = shipDiscount;
_put(LS_KEY_DISCOUNTS, this.discounts);
this.emit('discounts', this.discounts);
};
}
/**
* Get the saved ship discount
@@ -363,7 +360,7 @@ class Persist extends EventEmitter {
this.discounts[1] = moduleDiscount;
_put(LS_KEY_DISCOUNTS, this.discounts);
this.emit('discounts', this.discounts);
};
}
/**
* Get the saved ship discount
@@ -371,7 +368,7 @@ class Persist extends EventEmitter {
*/
getModuleDiscount() {
return this.discounts[1];
};
}
/**
* Persist selected cost tab
@@ -380,7 +377,7 @@ class Persist extends EventEmitter {
setCostTab(tabName) {
this.costTab = tabName;
_put(LS_KEY_COST_TAB, tabName);
};
}
/**
* Get the saved discount
@@ -388,7 +385,7 @@ class Persist extends EventEmitter {
*/
getCostTab() {
return this.costTab;
};
}
/**
* Retrieve the last router state from local storage
@@ -396,7 +393,7 @@ class Persist extends EventEmitter {
*/
getState() {
return this.state;
};
}
/**
* Save the current router state to localstorage
@@ -405,7 +402,7 @@ class Persist extends EventEmitter {
setState(state) {
this.state = state;
_put(LS_KEY_STATE, state);
};
}
/**
* Retrieve the last router state from local storage
@@ -413,7 +410,7 @@ class Persist extends EventEmitter {
*/
getSizeRatio() {
return this.sizeRatio;
};
}
/**
* Save the current size ratio to localstorage
@@ -425,7 +422,7 @@ class Persist extends EventEmitter {
_put(LS_KEY_SIZE_RATIO, sizeRatio);
this.emit('sizeRatio', sizeRatio);
}
};
}
/**
* Check if localStorage is enabled/active

View File

@@ -2,6 +2,7 @@
<html {%= o.htmlWebpackPlugin.options.appCache ? 'manifest=/' + o.htmlWebpackPlugin.options.appCache : '' %} >
<head>
<title>Coriolis</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="{%= o.htmlWebpackPlugin.files.css[0] %}">
<!-- Standard headers -->
<meta name="description" content="A ship builder, outfitting and comparison tool for Elite Dangerous">

View File

@@ -58,7 +58,7 @@ svg {
fill: @fg;
}
.tip {
.tooltip {
fill: @bgBlack;
stroke: @secondary;
stroke-width: 1px;

View File

@@ -17,7 +17,7 @@
transform:translate(-50%,-50%);
-webkit-transform:translate(-50%,-50%);
width: 800px;
max-height: 100%;
max-height: 90%;
padding: 2em;
background-color: @bgBlack;
box-sizing: border-box;

View File

@@ -235,13 +235,9 @@
font-size: 0.8em;
}
table tbody tr td {
&:nth-child(4) {
span {
vertical-align: middle;
font-size: 1.6em;
}
}
span.btn {
vertical-align: middle;
font-size: 1.6em;
}
});