semantic-ui-react#Table TypeScript Examples

The following examples show how to use semantic-ui-react#Table. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: items.tsx    From website with MIT License 6 votes vote down vote up
renderTableRow(item: any): JSX.Element {
		return (
			<Table.Row key={item.symbol}>
				<Table.Cell>
					<div
						style={{
							display: 'grid',
							gridTemplateColumns: '60px auto',
							gridTemplateAreas: `'icon stats' 'icon description'`,
							gridGap: '1px'
						}}
					>
						<div style={{ gridArea: 'icon' }}>
							<img width={48} src={`${process.env.GATSBY_ASSETS_URL}${item.imageUrl}`} />
						</div>
						<div style={{ gridArea: 'stats' }}>
							<Link to={`/item_info?symbol=${item.symbol}`}>
								<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}>
									{item.rarity > 0 && (
										<span>
											{item.rarity} <Icon name="star" />{' '}
										</span>
									)}
									{item.name}
								</span>
							</Link>
						</div>
						<div style={{ gridArea: 'description' }}>{item.flavor}</div>
					</div>
				</Table.Cell>
				<Table.Cell>{CONFIG.REWARDS_ITEM_TYPE[item.type]}</Table.Cell>
				<Table.Cell>{CONFIG.RARITIES[item.rarity].name}</Table.Cell>
				<Table.Cell>{item.flavor}</Table.Cell>
			</Table.Row>
		);
	}
Example #2
Source File: threshold_rewards.tsx    From website with MIT License 6 votes vote down vote up
function ThresholdRewardsTab({eventData}) {
	const {threshold_rewards} = eventData;

	return (
		<Table celled striped compact='very'>
			<Table.Body>
				{threshold_rewards.map(row => (
					<Table.Row key={row.points}>
						<Table.Cell>{row.points}</Table.Cell>
						<Table.Cell>
							{row.rewards.map(reward => (
								<Label key={`reward_${reward.id}`} color="black">
									<Image
										src={getIconPath(reward.icon)}
										size="small"
										inline
										spaced="right"
										bordered
										style={{
											borderColor: getRarityColor(reward.rarity),
											maxWidth: '27px',
											maxHeight: '27px'
										}}
										alt={reward.full_name}
									/>
									{reward.full_name}
									{reward.quantity > 1 ? ` x ${reward.quantity}` : ''}
								</Label>
							))}
						</Table.Cell>
					</Table.Row>
				))}
			</Table.Body>
		</Table>
	);
}
Example #3
Source File: ranked_rewards.tsx    From website with MIT License 6 votes vote down vote up
function RankedRewardsTab({eventData}) {
	const {ranked_brackets} = eventData;

	return (
		<Table celled striped compact='very'>
			<Table.Body>
				{ranked_brackets.map(row => (
					<Table.Row key={`bracket_${row.first}_${row.last}`}>
						<Table.Cell width={2}>{getBracketLabel(row)}</Table.Cell>
						<Table.Cell width={14}>
							{row.rewards.map(reward => (
								<Label key={`reward_${reward.id}`} color="black">
									<Image
										src={getIconPath(reward.icon)}
										size="small"
										inline
										spaced="right"
										bordered
										style={{
											borderColor: getRarityColor(reward.rarity ?? 0),
											maxWidth: '27px',
											maxHeight: '27px'
										}}
										alt={reward.full_name}
									/>
									{reward.full_name}
									{reward.quantity > 1 ? ` x ${reward.quantity}` : ''}
								</Label>
							))}
						</Table.Cell>
					</Table.Row>
				))}
			</Table.Body>
		</Table>
	);}
Example #4
Source File: crewchallenge.tsx    From website with MIT License 6 votes vote down vote up
GuessTable = (props) => {
	const { solveState, solvedCrew } = props;

	const guessesEvaluated = props.guessesEvaluated.slice();
	if (solveState === SolveState.Loser) guessesEvaluated.push({... solvedCrew, evaluation: { crew: EvaluationState.Exact }});
	if (guessesEvaluated.length === 0) return (<></>);

	return (
		<div style={{ overflow: 'auto' }}>
			<Table striped celled unstackable>
				<Table.Header>
					<Table.Row>
						<Table.HeaderCell>Your Guesses</Table.HeaderCell>
						<Table.HeaderCell textAlign='center'>Series</Table.HeaderCell>
						<Table.HeaderCell>Rarity</Table.HeaderCell>
						<Table.HeaderCell colSpan={3} textAlign='center'>Skill Order</Table.HeaderCell>
						<Table.HeaderCell textAlign='center'>Traits in Common</Table.HeaderCell>
					</Table.Row>
				</Table.Header>
				<Table.Body>
					{guessesEvaluated.map(guess => (
						<GuessRow key={guess.symbol} guess={guess} solveState={solveState} guessCount={props.guessesEvaluated.length} />
					))}
				</Table.Body>
			</Table>
		</div>
	);
}
Example #5
Source File: crewchallenge.tsx    From website with MIT License 5 votes vote down vote up
GuessRow = (props: GuessRowProps) => {
	const { guess, solveState, guessCount } = props;

	const isSolution = guess.evaluation.crew === EvaluationState.Exact;
	const traits = guess.evaluation.traits ?? guess.traits;

	return (
		<Table.Row {...styleRow()}>
			<Table.Cell {...styleCell(guess.evaluation.variant)}>
				{isSolution && (
					<div>
						{solveState === SolveState.Winner && (<span style={{ whiteSpace: 'nowrap' }}>You got it in {guessCount} tr{guessCount !== 1 ? 'ies' : 'y'}!</span>)}
						{solveState === SolveState.Loser && (<span style={{ whiteSpace: 'nowrap' }}>You lose! The correct answer is:</span>)}
					</div>
				)}
				<div style={{ margin: '.5em 0', whiteSpace: 'nowrap' }}>
					<img width={48} height={48} src={`${process.env.GATSBY_ASSETS_URL}${guess.imageUrlPortrait}`} style={{ verticalAlign: 'middle' }} />
					<span style={{ padding: '0 .5em', fontSize: '1.25em' }}>{guess.name}</span>
				</div>
				{isSolution && guess.flavor && (
					<div>{guess.flavor}</div>
				)}
			</Table.Cell>
			<Table.Cell textAlign='center' {...styleCell(guess.evaluation.series)}>
				{guess.series && <Image src={`/media/series/${guess.series}.png`} size='small' style={{ margin: '0 auto' }} />}
			</Table.Cell>
			<Table.Cell {...styleCell(guess.evaluation.rarity)}>
				<Rating defaultRating={guess.rarity} maxRating={guess.rarity} icon='star' size='large' disabled />
			</Table.Cell>
			{guess.skills.map((skill, idx) => (
				<Table.Cell key={idx} textAlign='center' {...styleCell(guess.evaluation.skills ? guess.evaluation.skills[idx] : 0)}>
					{skill.skill !== '' && <img alt={idx} src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${skill.skill}.png`} style={{ height: '2em' }} />}
					{skill.skill === '' && <Icon name='minus' />}
				</Table.Cell>
			))}
			<Table.Cell textAlign='center'>
				{traits.map((trait, idx) => (
					<span key={idx} style={{ whiteSpace: 'nowrap' }}>
						{formatTrait(trait)}{idx < traits.length-1 ? ',' : ''}
					</span>
				)).reduce((prev, curr) => [prev, ' ', curr], [])}
			</Table.Cell>
		</Table.Row>
	);

	function styleRow(): any {
		if (!isSolution) return {};
		const attributes = {};
		attributes.style = solveState === SolveState.Winner ? STYLE_SOLVED : STYLE_LOSER;
		return attributes;
	}

	function styleCell(evaluationState: number): any {
		const attributes = {};
		if (evaluationState === EvaluationState.Exact)
			attributes.style = STYLE_SOLVED;
		else if (evaluationState === EvaluationState.Adjacent)
			attributes.style = STYLE_ADJACENT;
		return attributes;
	}

	function formatTrait(trait: string): string {
		const simpleName = (trait: string) => {
			return trait.replace(/[^A-Z]/gi, '').toLowerCase();
		};
		const properName = (trait: string) => {
			return trait.replace(/_/g, ' ').split(' ').map(word => word.substr(0, 1).toUpperCase()+word.substr(1)).join(' ');
		};
		// Display short_name instead of variant trait when appropriate
		if (guess.variants.includes(trait)) {
			if (simpleName(trait).indexOf(simpleName(guess.short_name)) >= 0
					|| simpleName(guess.short_name).indexOf(simpleName(trait)) >= 0)
				return guess.short_name;
		}
		return properName(trait);
	}
}
Example #6
Source File: leaderboard.tsx    From website with MIT License 5 votes vote down vote up
function LeaderboardTab({leaderboard}) {
	return (
		<>
			<Message>
				If this event is currently active, the leaderboard below might be out of date.
				(Data is updated only a couple of times a week)
			</Message>
			<Table celled striped compact='very'>
				<Table.Body>
					{leaderboard.map(member => (
						<Table.Row key={member.dbid}>
							<Table.Cell style={{ fontSize: '1.25em' }}>
								Rank: {member.rank}
							</Table.Cell>
							<Table.Cell>
								<div
									style={{
										display: 'grid',
										gridTemplateColumns: '60px auto',
										gridTemplateAreas: `'icon stats' 'icon description'`,
										gridGap: '1px'
									}}>
									<div style={{ gridArea: 'icon' }}>
										<img
											width={48}
											src={member.avatar ? getIconPath(member.avatar) : `${process.env.GATSBY_ASSETS_URL}crew_portraits_cm_empty_sm.png`}
										/>
									</div>
									<div style={{ gridArea: 'stats' }}>
										<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}>
											{member.display_name}
										</span>
									</div>
									<div style={{ gridArea: 'description' }}>
										Level {member.level}
									</div>
								</div>
							</Table.Cell>
							<Table.Cell style={{ fontSize: '1.25em' }}>
								Score: {member.score}
							</Table.Cell>
						</Table.Row>
					))}
				</Table.Body>
			</Table>
		</>
	);
}
Example #7
Source File: index.tsx    From website with MIT License 5 votes vote down vote up
renderTableRow(crew: any, idx: number, highlighted: boolean): JSX.Element {
		const { customColumns } = this.state;
		const attributes = {
			positive: highlighted
		};

		const counts = [
			{ name: 'event', count: crew.events },
			{ name: 'collection', count: crew.collections.length }
		];
		const formattedCounts = counts.map((count, idx) => (
			<span key={idx} style={{ whiteSpace: 'nowrap' }}>
				{count.count} {count.name}{count.count != 1 ? 's' : ''}{idx < counts.length-1 ? ',' : ''}
			</span>
		)).reduce((prev, curr) => [prev, ' ', curr]);

		return (
			<Table.Row key={crew.symbol} style={{ cursor: 'zoom-in' }} onClick={() => navigate(`/crew/${crew.symbol}/`)} {...attributes}>
				<Table.Cell>
					<div
						style={{
							display: 'grid',
							gridTemplateColumns: '60px auto',
							gridTemplateAreas: `'icon stats' 'icon description'`,
							gridGap: '1px'
						}}>
						<div style={{ gridArea: 'icon' }}>
							<img width={48} src={`${process.env.GATSBY_ASSETS_URL}${crew.imageUrlPortrait}`} />
						</div>
						<div style={{ gridArea: 'stats' }}>
							<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}><Link to={`/crew/${crew.symbol}/`}>{crew.name}</Link></span>
						</div>
						<div style={{ gridArea: 'description' }}>
							{formattedCounts}
						</div>
					</div>
				</Table.Cell>
				<Table.Cell>
					<Rating icon='star' rating={crew.max_rarity} maxRating={crew.max_rarity} size='large' disabled />
				</Table.Cell>
				<Table.Cell textAlign="center">
					<b>{formatTierLabel(crew.bigbook_tier)}</b>
				</Table.Cell>
				<Table.Cell style={{ textAlign: 'center' }}>
					<b>{crew.cab_ov}</b><br />
					<small>{rarityLabels[parseInt(crew.max_rarity)-1]} #{crew.cab_ov_rank}</small>
				</Table.Cell>
				<Table.Cell style={{ textAlign: 'center' }}>
					<b>#{crew.ranks.voyRank}</b><br />
					{crew.ranks.voyTriplet && <small>Triplet #{crew.ranks.voyTriplet.rank}</small>}
				</Table.Cell>

				{CONFIG.SKILLS_SHORT.map(skill =>
					crew.base_skills[skill.name] ? (
						<Table.Cell key={skill.name} textAlign='center'>
							<b>{crew.base_skills[skill.name].core}</b>
							<br />
							+({crew.base_skills[skill.name].range_min}-{crew.base_skills[skill.name].range_max})
						</Table.Cell>
					) : (
						<Table.Cell key={skill.name} />
					)
				)}

				{customColumns.map(column => {
					const value = column.split('.').reduce((prev, curr) => prev && prev[curr] ? prev[curr] : undefined, crew);
					if (value) {
						return (
							<Table.Cell key={column} textAlign='center'>
								<b>{value}</b>
							</Table.Cell>
						);
					}
					else {
						return (<Table.Cell key={column} />);
					}
				})}
			</Table.Row>
		);
	}
Example #8
Source File: profile_other.tsx    From website with MIT License 5 votes vote down vote up
render() {
		const { playerData } = this.props;
		const { missions } = this.state;

		return (
			<div>
				<Table celled selectable striped collapsing unstackable compact="very">
					<Table.Header>
						<Table.Row>
							<Table.HeaderCell width={2}>Activity</Table.HeaderCell>
							<Table.HeaderCell width={1}>Status</Table.HeaderCell>
							<Table.HeaderCell width={3}>Description</Table.HeaderCell>
						</Table.Row>
					</Table.Header>
					<Table.Body>
						{playerData.player.character.daily_activities.map((da, idx) =>
							(da.status && da.lifetime !== 0) ? (
								<Table.Row key={idx}>
									<Table.Cell>{da.name}</Table.Cell>
									<Table.Cell>{da.status}</Table.Cell>
									<Table.Cell>{da.description}</Table.Cell>
								</Table.Row>
							) : (
								undefined
							)
						)}
					</Table.Body>
				</Table>

				<Table celled selectable striped collapsing unstackable compact="very">
					<Table.Header>
						<Table.Row>
							<Table.HeaderCell width={3}>Completed missions</Table.HeaderCell>
							<Table.HeaderCell width={3}>Status</Table.HeaderCell>
						</Table.Row>
					</Table.Header>
					<Table.Body>
						{missions.map((mission, idx) => (
							<Table.Row key={idx}>
								<Table.Cell>{mission.name}</Table.Cell>
								<Table.Cell>
									Completed {mission.stars_earned} of {mission.total_stars} missions
								</Table.Cell>
							</Table.Row>
						))}
					</Table.Body>
				</Table>
			</div>
		);
	}
Example #9
Source File: voyagestats.tsx    From website with MIT License 5 votes vote down vote up
_renderEstimate(needsRevive: boolean = false) {
		const estimate  = this.props.estimate ?? this.state.estimate;

		if (!estimate)
			return (<div>Calculating estimate. Please wait...</div>);

		const renderEst = (label, refills) => {
			const est = estimate['refills'][refills];
			return (
				<tr>
					<td>{label}: {this._formatTime(est.result)}</td>
					{!isMobile && <td>90%: {this._formatTime(est.safeResult)}</td>}
					<td>99%: {this._formatTime(est.saferResult)}</td>
					<td>Chance of {est.lastDil} hour dilemma: {Math.floor(est.dilChance)}%</td>
					<td>{est.refillCostResult == 0 || 'Costing ' + est.refillCostResult + ' dilithium'}</td>
				</tr>
			);
		};

		if (estimate.deterministic) {
			let extendTime = estimate['refills'][1].result - estimate['refills'][0].result;

			return (
				<div>
					The voyage will end at {this._formatTime(estimate['refills'][0].result)}.
					Subsequent refills will extend it by {this._formatTime(extendTime)}.
					For a 20 hour voyage you need {estimate['20hrrefills']} refills at a cost of {estimate['20hrdil']} dilithium.
				</div>
			);
		} else {
			let refill = 0;

			return (
				<div>
					<Table><tbody>
						{!needsRevive && renderEst("Estimate", refill++)}
						{renderEst("1 Refill", refill++)}
						{renderEst("2 Refills", refill++)}
					</tbody></Table>
					<p>The 20 hour voyage needs {estimate['20hrrefills']} refills at a cost of {estimate['20hrdil']} dilithium.</p>
					{this._renderChart()}
					<small>Powered by Chewable C++</small>
				</div>
			);
		}
	}
Example #10
Source File: eventplanner.tsx    From website with MIT License 5 votes vote down vote up
EventCrewMatrix = (props: EventCrewMatrixProps) => {
	const { crew, bestCombos, phaseType, handleClick } = props;

	return (
		<React.Fragment>
			<Header as='h4'>Skill Matrix</Header>
			<p>This table shows your best crew for each possible skill combination. Use this table to identify your best crew for this event{phaseType === 'shuttles' ? ` and the best candidates to share in a faction event if you are a squad leader` : ''}.</p>
			<Table definition celled striped collapsing unstackable compact='very'>
				<Table.Header>
					<Table.Row>
						<Table.HeaderCell />
						{CONFIG.SKILLS_SHORT.map((skill, cellId) => (
							<Table.HeaderCell key={cellId} textAlign='center'>
								<img alt={`${skill.name}`} src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${skill.name}.png`} style={{ height: '1.1em' }} />
							</Table.HeaderCell>
						))}
					</Table.Row>
				</Table.Header>
				<Table.Body>
					{CONFIG.SKILLS_SHORT.map((skillA, rowId) => (
						<Table.Row key={rowId}>
							<Table.Cell width={1} textAlign='center'><img alt={`${skillA.name}`} src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${skillA.name}.png`} style={{ height: '1.1em' }} /></Table.Cell>
							{CONFIG.SKILLS_SHORT.map((skillB, cellId) => renderCell(skillA.name, skillB.name))}
						</Table.Row>
					))}
				</Table.Body>
			</Table>
		</React.Fragment>
	);

	function renderCell(skillA: string, skillB: string) : JSX.Element {
		let key, best;
		if (skillA === skillB) {
			key = skillA;
			best = bestCombos[skillA];
		}
		else {
			key = skillA+skillB;
			best = bestCombos[skillA+skillB] ?? bestCombos[skillB+skillA];
		}
		if (!best) best = {score: 0};
		if (best.score > 0) {
			let bestCrew = crew.find(c => c.id === best.id);
			let icon = (<></>);
			if (bestCrew.immortal) icon = (<Icon name='snowflake' />);
			if (bestCrew.prospect) icon = (<Icon name='add user' />);
			return (
				<Table.Cell key={key} textAlign='center' style={{ cursor: 'zoom-in' }} onClick={() => handleClick(skillA, skillB)}>
					<img width={36} src={`${process.env.GATSBY_ASSETS_URL}${bestCrew.imageUrlPortrait}`} /><br/>{icon} {bestCrew.name} <small>({phaseType === 'gather' ? `${calculateGalaxyChance(best.score)}%` : Math.floor(best.score)})</small>
				</Table.Cell>
			);
		}
		return (
			<Table.Cell key={key} textAlign='center'>-</Table.Cell>
		);
	}
}
Example #11
Source File: voyagehof.tsx    From website with MIT License 5 votes vote down vote up
VoyageStatsForPeriod = ({ period, stats, allCrew }) => {
  const rankedCrew = stats.map((s) => {
    const crew = allCrew.find((c) => c.symbol === s.crewSymbol);
    if (!crew) {
      return undefined;
    }
    return {
      ...s,
      ...crew
    }
  }).filter((s) => s).sort((a, b) => b.crewCount - a.crewCount).slice(0,100);
  const rowColors = {
    '0': '#AF9500',
    '1': '#B4B4B4',
    '2': '#AD8A56'
  };
  return (
    <>
    <Header textAlign="center">Voyage stats for {niceNamesForPeriod[period]}</Header>
    <Table>
      <Table.Header>
        <Table.Row>
          <Table.HeaderCell>Rank</Table.HeaderCell>
          <Table.HeaderCell textAlign="right">Crew</Table.HeaderCell>
        </Table.Row>
      </Table.Header>
      <Table.Body>
        {rankedCrew.map((crew, index) => (
            <Table.Row>
              <Table.Cell>
                <Header as='h2' textAlign='center' style={{ color: rowColors[index] }}>{index+1}</Header>
              </Table.Cell>
              <Table.Cell>
                <div
										style={{
											display: 'grid',
											gridTemplateColumns: '80px auto',
											gridTemplateAreas: `'icon name'`,
											gridGap: '1px'
										}}
									>
                    <div style={{ gridArea: 'icon' }}>
											<img width={48} src={`${process.env.GATSBY_ASSETS_URL}/${crew.imageUrlPortrait}`} />
										</div>
										<div style={{ gridArea: 'name' }}>
											<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}>{crew.name}</span>
                      <Header as='h3' style={{marginTop: '10px'}}>{crew.crewCount} voyages</Header>
										</div>
									</div>
              </Table.Cell>
            </Table.Row>
          ))}
      </Table.Body>
    </Table>
    </>
  )
}
Example #12
Source File: factions.tsx    From website with MIT License 5 votes vote down vote up
render() {
    const { factionInfo, shuttleBays } = this.props;
    const { successOdds } = this.state;
    const updateSuccessOdds = odds => this.setState({successOdds: odds});

    return (
      <>
        <p><span>Running shuttles at average odds of </span>
          <Dropdown text={`${successOdds}%`}>
            <Dropdown.Menu>
              {oddsValues.map(val => (<Dropdown.Item onClick={(e, { value }) => updateSuccessOdds(value)} text={`${val}%`} value={val} />))}
            </Dropdown.Menu>
          </Dropdown>
          <p>(Note: Shuttles cannot be run with a probability of success less than 14%. Shuttles need a probability of less than 60% to be tanked.)</p>
        </p>
        <Table>
          <Table.Header>
          <Table.Row>
            <Table.HeaderCell>Faction</Table.HeaderCell>
            <Table.HeaderCell>Reputation</Table.HeaderCell>
            <Table.HeaderCell>Shuttles needed</Table.HeaderCell>
            <Table.HeaderCell>Time needed</Table.HeaderCell>
          </Table.Row>
          </Table.Header>
          <Table.Body>
          {factionInfo.map((faction, index) => {
            let shuttlesNeededToMaxRep = this._shuttlesToHonouredStatus(faction.reputation);
            let hoursNeededToMaxRep = Math.ceil(shuttlesNeededToMaxRep/shuttleBays)*3;
            let shuttlesNeededToTank = Math.ceil(faction.completed_shuttle_adventures/this._expectedCSA(successOdds/100));
            let hoursNeededToTank = Math.ceil(shuttlesNeededToTank/shuttleBays)*3;

            return (
              <Table.Row key={index}>
                <Table.Cell><span><Image floated='left' size='mini' src={`${process.env.GATSBY_ASSETS_URL}icons_icon_faction_${factionImageLocations[index]}.png`} />{faction.name}</span></Table.Cell>
                <Table.Cell>{this._reputations(faction.reputation)}</Table.Cell>
                <Table.Cell>
                  {faction.reputation < 980 && <p>You need {shuttlesNeededToMaxRep} successful shuttle missions to achieve honored status.</p>}
                  {shuttlesNeededToTank > 0 && <p>To tank your shuttles you need to run {shuttlesNeededToTank} shuttles.</p>}
                  {shuttlesNeededToTank == 0 && <p>Already tanked</p>}
                </Table.Cell>
                <Table.Cell>
                  {faction.reputation < 980 && <p>{this._formatTime(hoursNeededToMaxRep)}</p>}
                  <p>{this._formatTime(hoursNeededToTank)}</p>
                </Table.Cell>
              </Table.Row>
            );
          })}
          </Table.Body>
        </Table>
        <p>Note: <a href="https://www.reddit.com/r/StarTrekTimelines/comments/aq5qzg/guide_tanked_shuttles_why_and_how/">Tanking</a> shuttles is the process of deliberately failing shuttles so that the difficulty and duration of shuttle missions go down.</p>
      </>
    );
  }
Example #13
Source File: PermanentRoomModal.tsx    From watchparty with MIT License 5 votes vote down vote up
render() {
    const { closeModal } = this.props;
    return (
      <Modal open={true} onClose={closeModal as any} closeIcon>
        <Modal.Header>Permanent Rooms</Modal.Header>
        <Modal.Content image>
          <Modal.Description>
            <div>
              Registered users have the ability to make their rooms permanent.
              Subscribed users can create multiple permanent rooms.
            </div>
            <Table definition unstackable striped celled>
              <Table.Header>
                <Table.Row>
                  <Table.HeaderCell />
                  <Table.HeaderCell>Temporary</Table.HeaderCell>
                  <Table.HeaderCell>Permanent</Table.HeaderCell>
                </Table.Row>
              </Table.Header>

              <Table.Body>
                <Table.Row>
                  <Table.Cell>Expiry</Table.Cell>
                  <Table.Cell>After 24 hours of inactivity</Table.Cell>
                  <Table.Cell>Never</Table.Cell>
                </Table.Row>
                <Table.Row>
                  <Table.Cell>Room Passwords</Table.Cell>
                  <Table.Cell></Table.Cell>
                  <Table.Cell>
                    <Icon name="check" />
                  </Table.Cell>
                </Table.Row>
                <Table.Row>
                  <Table.Cell>Disable Chat</Table.Cell>
                  <Table.Cell></Table.Cell>
                  <Table.Cell>
                    <Icon name="check" />
                  </Table.Cell>
                </Table.Row>
                <Table.Row>
                  <Table.Cell>Kick Users</Table.Cell>
                  <Table.Cell></Table.Cell>
                  <Table.Cell>
                    <Icon name="check" />
                  </Table.Cell>
                </Table.Row>
                <Table.Row>
                  <Table.Cell>Custom Room URLs (subscribers)</Table.Cell>
                  <Table.Cell></Table.Cell>
                  <Table.Cell>
                    <Icon name="check" />
                  </Table.Cell>
                </Table.Row>
                {/* <Table.Row>
                  <Table.Cell>Max Room Capacity (subscribers)</Table.Cell>
                    <Table.Cell>20</Table.Cell>
                    <Table.Cell>100</Table.Cell>
                  </Table.Row> */}
              </Table.Body>
            </Table>
          </Modal.Description>
        </Modal.Content>
      </Modal>
    );
  }
Example #14
Source File: searchabletable.tsx    From website with MIT License 4 votes vote down vote up
SearchableTable = (props: SearchableTableProps) => {
	let data = [...props.data];
	const tableId = props.id ?? '';

	const hasDate = data.length > 0 && data[0].date_added;
	const defaultSort = {
		column: data.length > 0 && hasDate ? 'date_added' : 'name',
		direction: data.length > 0 && hasDate ? 'descending' : 'ascending'
	};

	const [searchFilter, setSearchFilter] = useStateWithStorage(tableId+'searchFilter', '');
	const [filterType, setFilterType] = useStateWithStorage(tableId+'filterType', 'Any match');
	const [column, setColumn] = useStateWithStorage(tableId+'column', defaultSort.column);
	const [direction, setDirection] = useStateWithStorage(tableId+'direction', defaultSort.direction);
	const [pagination_rows, setPaginationRows] = useStateWithStorage(tableId+'paginationRows', 10);
	const [pagination_page, setPaginationPage] = useStateWithStorage(tableId+'paginationPage', 1);

	const [activeLock, setActiveLock] = React.useState(undefined);

	// Override stored values with custom initial options and reset all others to defaults
	//	Previously stored values will be rendered before an override triggers a re-render
	React.useEffect(() => {
		if (props.initOptions) {
			setSearchFilter(props.initOptions['search'] ?? '');
			setFilterType(props.initOptions['filter'] ?? 'Any match');
			setColumn(props.initOptions['column'] ?? defaultSort.column);
			setDirection(props.initOptions['direction'] ?? defaultSort.direction);
			setPaginationRows(props.initOptions['rows'] ?? 10);
			setPaginationPage(props.initOptions['page'] ?? 1);
		}
	}, [props.initOptions]);

	// Activate lock by default if only 1 lockable
	React.useEffect(() => {
		setActiveLock(props.lockable?.length === 1 ? props.lockable[0] : undefined);
	}, [props.lockable]);

	// Update column and/or toggle direction, and store new values in state
	//	Actual sorting of full dataset will occur on next render before filtering and pagination
	function onHeaderClick(newColumn) {
		if (!newColumn.column) return;

		const lastColumn = column, lastDirection = direction;

		const sortConfig = {
			field: newColumn.column,
			direction: lastDirection === 'ascending' ? 'descending' : 'ascending'
		};
		if (newColumn.pseudocolumns && newColumn.pseudocolumns.includes(lastColumn)) {
			if (direction === 'descending') {
				const nextIndex = newColumn.pseudocolumns.indexOf(lastColumn) + 1; // Will be 0 if previous column was not a pseudocolumn
				sortConfig.field = newColumn.pseudocolumns[nextIndex === newColumn.pseudocolumns.length ? 0 : nextIndex];
				sortConfig.direction = 'ascending';
			}
			else {
				sortConfig.field = lastColumn;
				sortConfig.direction = 'descending';
			}
		}
		else if (newColumn.column !== lastColumn) {
			sortConfig.direction = newColumn.reverse ? 'descending' : 'ascending';
		}

		setColumn(sortConfig.field);
		setDirection(sortConfig.direction);
		setPaginationPage(1);
	}

	function onChangeFilter(value) {
		setSearchFilter(value);
		setPaginationPage(1);
	}

	function renderTableHeader(column: any, direction: 'descending' | 'ascending' | null): JSX.Element {
		return (
			<Table.Row>
				{props.config.map((cell, idx) => (
					<Table.HeaderCell
						key={idx}
						width={cell.width as any}
						sorted={((cell.pseudocolumns && cell.pseudocolumns.includes(column)) || (column === cell.column)) ? direction : null}
						onClick={() => onHeaderClick(cell)}
						textAlign={cell.width === 1 ? 'center' : 'left'}
					>
						{cell.title}{cell.pseudocolumns?.includes(column) && <><br/><small>{column.replace('_',' ').replace('.length', '')}</small></>}
					</Table.HeaderCell>
				))}
			</Table.Row>
		);
	}

	function renderPermalink(): JSX.Element {
		// Will not catch custom options (e.g. highlight)
		const params = new URLSearchParams();
		if (searchFilter != '') params.append('search', searchFilter);
		if (filterType != 'Any match') params.append('filter', filterType);
		if (column != defaultSort.column) params.append('column', column);
		if (direction != defaultSort.direction) params.append('direction', direction);
		if (pagination_rows != 10) params.append('rows', pagination_rows);
		if (pagination_page != 1) params.append('page', pagination_page);
		let permalink = window.location.protocol + '//' + window.location.host + window.location.pathname;
		if (params.toString() != '') permalink += '?' + params.toString();
		return (
			<Link to={permalink}>
				<Icon name='linkify' /> Permalink
			</Link>
		);
	}

	function onLockableClick(lock: any): void {
		if (lock) {
			setActiveLock(lock);
		}
		else {
			setActiveLock(undefined);
			// Remember active page after removing lock
			setPaginationPage(activePage);
		}
	}

	function isRowActive(row: any, highlight: any): boolean {
		if (!highlight) return false;
		let isMatch = true;
		Object.keys(highlight).forEach(key => {
			if (row[key] !== highlight[key]) isMatch = false;
		});
		return isMatch;
	}

	// Sorting
	if (column) {
		const sortConfig: IConfigSortData = {
			field: column,
			direction: direction,
			keepSortOptions: true
		};

		// Define tiebreaker rules with names in alphabetical order as default
		//	Hack here to sort rarity in the same direction as max_rarity
		let subsort = [];
		const columnConfig = props.config.find(col => col.column === column);
		if (columnConfig && columnConfig.tiebreakers) {
			subsort = columnConfig.tiebreakers.map(subfield => {
				const subdirection = subfield.substr(subfield.length-6) === 'rarity' ? direction : 'ascending';
				return { field: subfield, direction: subdirection };
			});
		}
		if (column != 'name') subsort.push({ field: 'name', direction: 'ascending' });
		sortConfig.subsort = subsort;

		// Use original dataset for sorting
		const sorted: IResultSortDataBy = sortDataBy([...props.data], sortConfig);
		data = sorted.result;

		// Sorting by pre-calculated ranks should filter out crew without matching skills
		//	Otherwise crew without skills show up first (because 0 comes before 1)
		if (column.substr(0, 5) === 'ranks') {
			const rank = column.split('.')[1];
			data = data.filter(row => row.ranks[rank] > 0);
		}
	}

	// Filtering
	let filters = [];
	if (searchFilter) {
		let grouped = searchFilter.split(/\s+OR\s+/i);
		grouped.forEach(group => {
			filters.push(SearchString.parse(group));
		});
	}
	data = data.filter(row => props.filterRow(row, filters, filterType));

	// Pagination
	let activePage = pagination_page;
	if (activeLock) {
		const index = data.findIndex(row => isRowActive(row, activeLock));
		// Locked crew is not viewable in current filter
		if (index < 0) {
			setActiveLock(undefined);
			return (<></>);
		}
		activePage = Math.floor(index / pagination_rows) + 1;
	}
	let totalPages = Math.ceil(data.length / pagination_rows);
	if (activePage > totalPages) activePage = totalPages;
	data = data.slice(pagination_rows * (activePage - 1), pagination_rows * activePage);

	return (
		<div>
			<Input
				style={{ width: isMobile ? '100%' : '50%' }}
				iconPosition="left"
				placeholder="Search..."
				value={searchFilter}
				onChange={(e, { value }) => onChangeFilter(value)}>
					<input />
					<Icon name='search' />
					<Button icon onClick={() => onChangeFilter('')} >
						<Icon name='delete' />
					</Button>
			</Input>

			{props.showFilterOptions && (
				<span style={{ paddingLeft: '2em' }}>
					<Dropdown inline
								options={filterTypeOptions}
								value={filterType}
								onChange={(event, {value}) => setFilterType(value as number)}
					/>
				</span>
			)}

			<Popup wide trigger={<Icon name="help" />}
				header={'Advanced search'}
				content={props.explanation ? props.explanation : renderDefaultExplanation()}
			/>

			{props.lockable && <LockButtons lockable={props.lockable} activeLock={activeLock} setLock={onLockableClick} />}

			<Table sortable celled selectable striped collapsing unstackable compact="very">
				<Table.Header>{renderTableHeader(column, direction)}</Table.Header>
				<Table.Body>{data.map((row, idx) => props.renderTableRow(row, idx, isRowActive(row, activeLock)))}</Table.Body>
				<Table.Footer>
					<Table.Row>
						<Table.HeaderCell colSpan={props.config.length}>
							<Pagination
								totalPages={totalPages}
								activePage={activePage}
								onPageChange={(event, { activePage }) => {
									setPaginationPage(activePage as number);
									setActiveLock(undefined);	// Remove lock when changing pages
								}}
							/>
							<span style={{ paddingLeft: '2em'}}>
								Rows per page:{' '}
								<Dropdown
									inline
									options={pagingOptions}
									value={pagination_rows}
									onChange={(event, {value}) => {
										setPaginationPage(1);
										setPaginationRows(value as number);
									}}
								/>
							</span>
							{props.showPermalink && (<span style={{ paddingLeft: '5em'}}>{renderPermalink()}</span>)}
						</Table.HeaderCell>
					</Table.Row>
				</Table.Footer>
			</Table>
		</div>
	);
}
Example #15
Source File: event_info.tsx    From website with MIT License 4 votes vote down vote up
render() {
		const { event_instace, errorMessage, event_data } = this.state;

		if (event_instace === undefined || event_data === undefined || errorMessage !== undefined) {
			return (
				<Layout title='Event information'>
					<Header as='h4'>Event information</Header>
					{errorMessage && (
						<Message negative>
							<Message.Header>Unable to load event information</Message.Header>
							<pre>{errorMessage.toString()}</pre>
						</Message>
					)}
					{!errorMessage && (
						<div>
							<Icon loading name='spinner' /> Loading...
						</div>
					)}
				</Layout>
			);
		}

		return (
			<Layout title={event_data.ev_inst.event_name}>
				<Header as='h3'>{event_data.ev_inst.event_name}</Header>
				<Image size='large' src={`${process.env.GATSBY_ASSETS_URL}${event_data.ev_inst.image}`} />

				{this.renderEventDetails()}

				{this.renderEventLog()}

				<Header as='h4'>Leaderboard</Header>
				<Table celled selectable striped collapsing unstackable compact='very'>
					<Table.Header>
						<Table.Row>
							<Table.HeaderCell width={3}>Name</Table.HeaderCell>
							<Table.HeaderCell width={1}>Rank</Table.HeaderCell>
							<Table.HeaderCell width={1}>Score</Table.HeaderCell>
							<Table.HeaderCell width={2}>Fleet</Table.HeaderCell>
						</Table.Row>
					</Table.Header>
					<Table.Body>
						{event_data.ev_lead.leaderboard.map((member, idx) => (
							<Table.Row key={idx}>
								<Table.Cell>
									<div
										style={{
											display: 'grid',
											gridTemplateColumns: '60px auto',
											gridTemplateAreas: `'icon stats' 'icon description'`,
											gridGap: '1px'
										}}>
										<div style={{ gridArea: 'icon' }}>
											<img
												width={48}
												src={`${process.env.GATSBY_ASSETS_URL}${member.avatar ? member.avatar.file.substr(1).replace(/\//g, '_') + '.png' : 'crew_portraits_cm_empty_sm.png'
													}`}
											/>
										</div>
										<div style={{ gridArea: 'stats' }}>
											<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}>
												{member.last_update ? (
													<Link to={`/profile?dbid=${member.dbid}`}>{member.display_name}</Link>
												) : (
														<span>{member.display_name}</span>
													)}
											</span>
										</div>
										<div style={{ gridArea: 'description' }}>
											Level {member.level}
											{member.last_update && (
												<Label size='tiny'>Last profile upload: {new Date(Date.parse(member.last_update)).toLocaleDateString()}</Label>
											)}
										</div>
									</div>
								</Table.Cell>
								<Table.Cell>{member.rank}</Table.Cell>
								<Table.Cell>{member.score}</Table.Cell>
								<Table.Cell>
									{member.fleetname ? <Link to={`/fleet_info?fleetid=${member.fleetid}`}>
										<b>{member.fleetname}</b>
									</Link> : <span>-</span>}
								</Table.Cell>
							</Table.Row>
						))}
					</Table.Body>
				</Table>

				{event_data.ev_flead &&
					<div>
						<Message>
							<Message.Header>TODO: Fleet Leaderboard is experimental</Message.Header>
							This data may be incomplete or out of date!
						</Message>

						<Header as='h4'>Fleet leaderboard</Header>
						<Table celled selectable striped collapsing unstackable compact='very'>
							<Table.Header>
								<Table.Row>
									<Table.HeaderCell width={3}>Name</Table.HeaderCell>
									<Table.HeaderCell width={1}>Rank</Table.HeaderCell>
								</Table.Row>
							</Table.Header>
							<Table.Body>
								{event_data.ev_flead.fleet_ranks.map((fleet, idx) => (
									<Table.Row key={idx}>
										<Table.Cell>{fleet.fleet_rank}</Table.Cell>
										<Table.Cell>
											<Link to={`/fleet_info?fleetid=${fleet.id}`}>
												<b>{fleet.name}</b>
											</Link>
										</Table.Cell>
									</Table.Row>
								))}
							</Table.Body>
						</Table>
					</div>}
			</Layout>
		);
	}
Example #16
Source File: assignmentslist.tsx    From website with MIT License 4 votes vote down vote up
AssignmentsList = (props: AssignmentsList) => {
	const { shuttlers, setShuttlers, assigned, setAssigned, crewScores, updateCrewScores } = props;

	const [shuttleScores, setShuttleScores] = React.useState([]);
	const [editAssignment, setEditAssignment] = React.useState(undefined);
	const [scoreLoadQueue, setScoreLoadQueue] = React.useState('');

	React.useEffect(() => {
		updateShuttleScores();
	}, [assigned]);

	const myCrew = props.crew;
	const SeatAssignmentRow = (props: { shuttleId: string, seatNum: number, seat: ShuttleSeat }) => {
		const { shuttleId, seatNum, seat } = props;

		let assignedCrew;
		const seated = assigned.find(seat => seat.shuttleId === shuttleId && seat.seatNum === seatNum);
		if (seated) {
			assignedCrew = myCrew.find(crew => crew.id === seated.assignedId && crew.symbol === seated.assignedSymbol);
			if (!assignedCrew) assignedCrew = myCrew.find(crew => crew.symbol === seated.assignedSymbol);
		}

		const lockAttributes = {};
		if (seated?.locked) lockAttributes.color = 'yellow';

		return (
			<Table.Row key={seatNum} style={{ cursor: 'pointer' }}
				onClick={() => { if (seat.skillA) setEditAssignment({shuttleId, seatNum}); }}
			>
				<Table.Cell textAlign='center'>
					<SeatSkillView seat={seat} />
				</Table.Cell>
				<Table.Cell textAlign={assignedCrew ? 'left' : 'right'}>
					{assignedCrew && (<SeatCrewView crew={assignedCrew} />)}
					{!assignedCrew && (<span style={{ color: 'gray' }}>(Open seat)</span>)}
				</Table.Cell>
				<Table.Cell>
					{assignedCrew?.immortal > 0 && (<Icon name='snowflake' />)}
					{assignedCrew?.prospect && (<Icon name='add user' />)}
				</Table.Cell>
				<Table.Cell textAlign='center'>
					{assignedCrew && (
						<Button.Group>
							<Button compact icon='lock' {... lockAttributes}
								onClick={(e) => { toggleAssignmentLock(shuttleId, seatNum); e.stopPropagation(); }} />
							<Button compact icon='x'
								onClick={(e) => { updateAssignment(shuttleId, seatNum); e.stopPropagation(); }} />
						</Button.Group>
					)}
				</Table.Cell>
			</Table.Row>
		);
	};

	const SeatAssignmentPicker = () => {
		const { shuttleId, seatNum } = editAssignment;
		const [paginationPage, setPaginationPage] = React.useState(1);

		const seat = shuttlers.shuttles.find(shuttle => shuttle.id === shuttleId).seats[seatNum];
		const ssId = getSkillSetId(seat);
		const scores = crewScores.skillsets[ssId];
		if (!scores) {
			if (scoreLoadQueue === '') {
				setScoreLoadQueue(ssId);
				updateCrewScores([seat], () => setScoreLoadQueue(''));
			}
			return (<></>);
		}

		const shuttle = shuttlers.shuttles.find(shuttle => shuttle.id === shuttleId);
		return (
			<Modal
				open={true}
				onClose={() => setEditAssignment(undefined)}
			>
				<Modal.Header>
					{shuttle.name}
					{shuttleScores[shuttleId] ?
						<span style={{ fontSize: '.95em', fontWeight: 'normal', paddingLeft: '1em' }}>
							({(shuttleScores[shuttleId].chance*100).toFixed(1)}% Chance)
						</span>
						: ''}
				</Modal.Header>
				<Modal.Content scrolling>
					{scores && renderTable()}
				</Modal.Content>
				<Modal.Actions>
					<Button icon='forward' content='Next Seat' onClick={() => cycleShuttleSeat()} />
					<Button positive onClick={() => setEditAssignment(undefined)}>
						Close
					</Button>
				</Modal.Actions>
			</Modal>
		);

		function renderTable(): JSX.Element {
			let assignedCrew;
			const seated = assigned.find(seat => seat.shuttleId === shuttleId && seat.seatNum == seatNum);
			if (seated) {
				assignedCrew = myCrew.find(crew => crew.id === seated.assignedId && crew.symbol === seated.assignedSymbol);
				if (!assignedCrew) assignedCrew = myCrew.find(crew => crew.symbol === seated.assignedSymbol);
			}

			// Pagination
			const rowsPerPage = 10;
			const totalPages = Math.ceil(scores.length / rowsPerPage);
			const data = scores.slice(rowsPerPage * (paginationPage - 1), rowsPerPage * paginationPage).map(score => {
				const scoreCrew = myCrew.find(crew => crew.id === score.id);
				return {...scoreCrew, ...score}
			});

			return (
				<React.Fragment>
					<Table striped selectable singleLine compact='very'>
						<Table.Header>
							<Table.Row>
								<Table.HeaderCell />
								<Table.HeaderCell colSpan={2}>Best <span style={{ padding: '0 .5em' }}><SeatSkillView seat={seat} /></span> Crew</Table.HeaderCell>
								<Table.HeaderCell textAlign='center'>Here<Popup trigger={<Icon name='help' />} content='Using this crew here will result in this net change to the success chance of this shuttle' /></Table.HeaderCell>
								<Table.HeaderCell>Current Assignment</Table.HeaderCell>
								<Table.HeaderCell textAlign='center'>There<Popup trigger={<Icon name='help' />} content='Removing this crew from their current assignment will leave an open seat on that shuttle, resulting in this success chance' /></Table.HeaderCell>
							</Table.Row>
						</Table.Header>
						<Table.Body>
							{data.map((crew, idx) => renderRow(crew, idx, assignedCrew))}
						</Table.Body>
						<Table.Footer>
							<Table.Row>
								<Table.HeaderCell colSpan={6}>
									<Pagination
										totalPages={totalPages}
										activePage={paginationPage}
										onPageChange={(e, { activePage }) => setPaginationPage(activePage)}
									/>
								</Table.HeaderCell>
							</Table.Row>
						</Table.Footer>
					</Table>
				</React.Fragment>
			);
		}

		function renderRow(crew: any, idx: number, assignedCrew: any): JSX.Element {
			const currentSeat = assigned.find(seat => seat.assignedId === crew.id && seat.assignedSymbol === crew.symbol);
			const currentShuttle = currentSeat ? shuttlers.shuttles.find(shuttle => shuttle.id === currentSeat.shuttleId) : undefined;
			return (
				<Table.Row key={idx} style={{ cursor: 'pointer' }}
					onClick={() => {
						if (!assignedCrew || crew.id !== assignedCrew.id)
							updateAssignment(shuttleId, seatNum, crew, true);
						setEditAssignment(undefined);
					}}
				>
					<Table.Cell textAlign='center'>
						{assignedCrew?.id === crew.id && (<Icon color='green' name='check' />)}
					</Table.Cell>
					<Table.Cell><SeatCrewView crew={crew} /></Table.Cell>
					<Table.Cell>
						{crew.immortal > 0 && (<Icon name='snowflake' />)}
						{crew.prospect && (<Icon name='add user' />)}
					</Table.Cell>
					<Table.Cell textAlign='center'>
						{renderScoreChange(shuttleId, seatNum, crew.score)}
					</Table.Cell>
					<Table.Cell>
						{currentShuttle?.name}
						{currentShuttle?.id === shuttleId && <span style={{ paddingLeft: '1em' }}><i>(This Shuttle)</i></span>}
						{currentSeat?.locked && <span style={{ paddingLeft: '1em' }}><Icon name='lock' /></span>}
					</Table.Cell>
					<Table.Cell textAlign='center'>
						{currentSeat && renderScoreChange(currentSeat.shuttleId, currentSeat.seatNum, 0)}
					</Table.Cell>
				</Table.Row>
			);
		}

		function renderScoreChange(shuttleId: string, seatNum: number, replacementScore: number = 0): JSX.Element {
			if (!shuttleScores[shuttleId]) return (<></>);
			const newScores = [...shuttleScores[shuttleId].scores];
			newScores[seatNum] = replacementScore;
			const DIFFICULTY = 2000;
			const dAvgSkill = newScores.reduce((a, b) => (a + b), 0)/newScores.length;
			const dChance = 1/(1+Math.pow(Math.E, 3.5*(0.5-dAvgSkill/DIFFICULTY)));
			const attributes = {};
			if (replacementScore === 0) {
				if (dChance*100 >= 90) attributes.style = { color: 'green', fontWeight: 'bold' };
				return (<span {...attributes}>{Math.floor(dChance*100)}%</span>);
			}
			const dDelta = dChance - shuttleScores[shuttleId].chance;
			if (dDelta > 0 && dChance*100 >= 90)
				attributes.style = { color: 'green', fontWeight: 'bold' };
			return (<span {...attributes}>{dDelta > 0 ? '+' : ''}{(dDelta*100).toFixed(1)}%</span>);
		}

		function cycleShuttleSeat(): void {
			const nextAssignment = {
				shuttleId: shuttleId,
				seatNum: seatNum + 1 >= shuttle.seats.length ? 0 : seatNum + 1
			};
			setEditAssignment(nextAssignment);
		}
	};

	const data = shuttlers.shuttles.slice()
		.filter(shuttle => shuttle.groupId === props.groupId && shuttle.priority > 0)
		.sort((a, b) => a.priority - b.priority);

	return (
		<React.Fragment>
			<p>You can rearrange crew to balance shuttle chances as you see fit. Click a seat to change the crew assigned to it. Lock an assignment to keep that crew in that seat when requesting new recommendations.</p>
			<Table celled striped compact='very'>
				<Table.Header>
					<Table.Row>
						<Table.HeaderCell>Mission</Table.HeaderCell>
						<Table.HeaderCell textAlign='center'>Faction</Table.HeaderCell>
						<Table.HeaderCell textAlign='center'>Seat Assignments</Table.HeaderCell>
						<Table.HeaderCell textAlign='center'>Success Chance</Table.HeaderCell>
						<Table.HeaderCell />
					</Table.Row>
				</Table.Header>
				<Table.Body>
					{data.length === 0 && (
						<Table.Row>
							<Table.Cell colSpan={6} textAlign='center'>
								No missions selected.
							</Table.Cell>
						</Table.Row>
					)}
					{data.map(shuttle => (
						<Table.Row key={shuttle.id}>
							<Table.Cell><b>{shuttle.name}</b></Table.Cell>
							<Table.Cell textAlign='center'>
								<ShuttleFactionView factionId={shuttle.faction} size={3} />
							</Table.Cell>
							<Table.Cell>
								<Table striped selectable singleLine compact='very' style={{ margin: '0 auto' }}>
									<Table.Body>
										{shuttle.seats.map((seat, seatNum) =>
											<SeatAssignmentRow key={seatNum} shuttleId={shuttle.id} seatNum={seatNum} seat={seat} />
										)}
									</Table.Body>
								</Table>
							</Table.Cell>
							<Table.Cell textAlign='center'>
								{shuttleScores[shuttle.id]?.chance > 0 ? <b>{Math.floor(shuttleScores[shuttle.id].chance*100)}%</b> : <></>}
							</Table.Cell>
							<Table.Cell textAlign='right'>
								<Button compact icon='ban' content='Dismiss' onClick={() => dismissShuttle(shuttle.id)} />
							</Table.Cell>
						</Table.Row>
					))}
				</Table.Body>
				<Table.Footer>
					<Table.Row>
						<Table.HeaderCell colSpan={3}>
							<Button compact icon='backward' content='Change Missions' onClick={() => props.setActiveStep('missions')} />
						</Table.HeaderCell>
						<Table.HeaderCell colSpan={3} textAlign='right'>
							{data.length > 0 && (<Button compact icon='rocket' color='green' content='Recommend Crew' onClick={() => props.recommendShuttlers()} />)}
						</Table.HeaderCell>
					</Table.Row>
				</Table.Footer>
			</Table>
			{editAssignment && (<SeatAssignmentPicker />)}
		</React.Fragment>
	);

	function dismissShuttle(shuttleId: string): void {
		shuttlers.shuttles.find(shuttle => shuttle.id === shuttleId).priority = 0;
		setShuttlers({...shuttlers});
	}

	function updateAssignment(shuttleId: string, seatNum: number, assignedCrew: any, locked: boolean): void {
		// Unassign crew from previously assigned seat, if necessary
		if (assignedCrew) {
			const current = assigned.find(seat => seat.assignedId === assignedCrew.id);
			if (current) {
				current.assignedId = -1;
				current.assignedSymbol = '';
				current.seatScore = 0;
				current.locked = false;
			}
		}

		const seated = assigned.find(seat => seat.shuttleId === shuttleId && seat.seatNum === seatNum);
		if (assignedCrew && !seated) {
			assigned.push({
				shuttleId,
				seatNum,
				ssId: assignedCrew.ssId,
				assignedId: assignedCrew.id,
				assignedSymbol: assignedCrew.symbol,
				seatScore: assignedCrew.score,
				locked
			});
		}
		else if (assignedCrew) {
			seated.assignedId = assignedCrew.id;
			seated.assignedSymbol = assignedCrew.symbol;
			seated.seatScore = assignedCrew.score;
			seated.locked = locked;
		}
		else {
			seated.assignedId = -1;
			seated.assignedSymbol = '';
			seated.seatScore = 0;
			seated.locked = false;
		}
		setAssigned([...assigned]);
	}

	function toggleAssignmentLock(shuttleId: string, seatNum: number): void {
		const seated = assigned.find(seat => seat.shuttleId === shuttleId && seat.seatNum === seatNum);
		seated.locked = !seated.locked;
		setAssigned([...assigned]);
	}

	function updateShuttleScores(): void {
		const DIFFICULTY = 2000;
		const newScores = [];
		assigned.forEach(seated => {
			if (!newScores[seated.shuttleId]) {
				const seatCount = shuttlers.shuttles.find(shuttle => shuttle.id === seated.shuttleId).seats.length;
				newScores[seated.shuttleId] = { chance: 0, scores: Array(seatCount).fill(0) };
			}
			newScores[seated.shuttleId].scores[seated.seatNum] = seated.seatScore;
			const dAvgSkill = newScores[seated.shuttleId].scores.reduce((a, b) => (a + b), 0)/newScores[seated.shuttleId].scores.length;
			const dChance = 1/(1+Math.pow(Math.E, 3.5*(0.5-dAvgSkill/DIFFICULTY)));
			newScores[seated.shuttleId].chance = dAvgSkill > 0 ? dChance : 0;
		});
		setShuttleScores(newScores);
	}
}
Example #17
Source File: missionslist.tsx    From website with MIT License 4 votes vote down vote up
MissionsList = (props: MissionsListProps) => {
	const { groupId, shuttlers, setShuttlers, activeShuttles } = props;

	const [editMission, setEditMission] = React.useState(undefined);
	const [state, dispatch] = React.useReducer(reducer, {
		data: shuttlers.shuttles.filter(shuttle => shuttle.groupId === groupId),
		column: null,
		direction: null
	});
	const { data, column, direction } = state;

	React.useEffect(() => {
		dispatch({ type: 'UPDATE_DATA', data: shuttlers.shuttles.filter(shuttle => shuttle.groupId === groupId), column, direction });
	}, [shuttlers]);

	const CheckDropdown = () => {
		if (data.length === 0) return (<></>);

		const checkOptions = [];

		const threeSeaters = [], fourSeaters = [];
		data.forEach(shuttle => {
			if (shuttle.seats.length <= 4)
				fourSeaters.push(shuttle.id);
			if (shuttle.seats.length === 3)
				threeSeaters.push(shuttle.id);
		});
		if (threeSeaters.length > 0)
			checkOptions.push({ key: 'three-seaters', text: `Select only 3-seaters (${threeSeaters.length})`, ids: threeSeaters });
		if (fourSeaters.length > 0)
			checkOptions.push({ key: 'four-seaters', text: `Select only 3- and 4- seaters (${fourSeaters.length})`, ids: fourSeaters });

		if (activeShuttles?.length > 0) {
			const openIds = activeShuttles.map(adventure => adventure.symbol);
			checkOptions.push({ key: `open-adventures`, text: `Select only open in-game (${openIds.length})`, ids: openIds });
		}

		const factions = [];
		data.forEach(shuttle => {
			if (shuttle.faction > 0 && !factions.includes(shuttle.faction)) factions.push(shuttle.faction);
		});
		if (factions.length > 1) {
			factions.forEach(factionId => {
				const ids = data.filter(shuttle => shuttle.faction === factionId).map(shuttle => shuttle.id);
				const faction = allFactions.find(af => af.id === factionId);
				checkOptions.push({ key: `faction-${factionId}`, text: `Select only ${faction.name} (${ids.length})`, ids });
			});
		}

		return (
			<Dropdown
				icon='check'
				floating
			>
				<Dropdown.Menu>
					<Dropdown.Item icon='check' text={`Select all (${data.length})`} onClick={() => checkMissions([])} />
					{missionsSelected > 0 && (
						<Dropdown.Item icon='x' text='Unselect all' onClick={() => checkMissions([], false)} />
					)}
					{checkOptions.length > 0 && <Dropdown.Divider />}
					{checkOptions.map(option => (
						<Dropdown.Item key={option.key} text={option.text} onClick={() => checkMissions(option.ids)} />
					))}
				</Dropdown.Menu>
			</Dropdown>
		);
	};

	const tableConfig = [
		{ title: <CheckDropdown />, align: 'center' },
		{ column: 'name', title: 'Mission' },
		{ column: 'faction', title: 'Faction', align: 'center' },
		{ column: 'seats.length', title: 'Seats', align: 'center' },
		{ column: 'skills', title: 'Skills', span: 5 },
		{ title: '' }
	];

	const MissionEditor = (props: { shuttle: Shuttle }) => {
		const [shuttle, setShuttle] = React.useState(JSON.parse(JSON.stringify(props.shuttle)));

		const factionOptions = allFactions.sort((a, b) => a.name.localeCompare(b.name)).map(faction => {
			return { key: faction.id, value: faction.id, text: (<span style={{ whiteSpace: 'nowrap' }}>{faction.name}</span>) };
		});

		const EditorSeat = (props: { seat: ShuttleSeat, seatNum: number }) => {
			const { seatNum, seat } = props;

			const skillOptions = [
				{ key: 'CMD', text: 'CMD', value: 'command_skill' },
				{ key: 'DIP', text: 'DIP', value: 'diplomacy_skill' },
				{ key: 'ENG', text: 'ENG', value: 'engineering_skill' },
				{ key: 'MED', text: 'MED', value: 'medicine_skill' },
				{ key: 'SCI', text: 'SCI', value: 'science_skill' },
				{ key: 'SEC', text: 'SEC', value: 'security_skill' }
			];

			return (
				<Grid textAlign='center' columns={3}>
					<Grid.Column>
						<Dropdown
							direction='right'
							compact
							selection
							options={skillOptions}
							value={seat.skillA}
							onChange={(e, { value }) => updateMissionSeat(seatNum, 'skillA', value)}
						/>
					</Grid.Column>
					<Grid.Column>
						<Button circular
							disabled={seat.skillB == '' ? true : false}
							onClick={() => updateMissionSeat(seatNum, 'operand', seat.operand == 'AND' ? 'OR' : 'AND')}
						>
							{seat.skillB == '' ? '' : seat.operand}
						</Button>
					</Grid.Column>
					<Grid.Column>
						<Dropdown
							compact
							selection
							clearable
							options={skillOptions}
							value={seat.skillB}
							onChange={(e, { value }) => updateMissionSeat(seatNum, 'skillB', value)}
						/>
					</Grid.Column>
				</Grid>
			);
		};

		return (
			<Modal
				open={true}
				onClose={() => applyEdits()}
			>
				<Modal.Header>Edit Mission</Modal.Header>
				<Modal.Content scrolling>
					{renderContent()}
				</Modal.Content>
				<Modal.Actions>
					<Button positive onClick={() => applyEdits()}>
						Close
					</Button>
				</Modal.Actions>
			</Modal>
		);

		function renderContent(): void {
			return (
				<React.Fragment>
					<Grid columns={2} divided stackable>
						<Grid.Column>
							<div>
								<Header as='h4'>Mission name</Header>
								<Input style={{ marginTop: '-1em' }}
									placeholder='Mission name...'
									value={shuttle.name}
									onChange={(e, { value }) => updateMissionName(value)}>
										<input />
										<Button icon onClick={() => updateMissionName('')} >
											<Icon name='delete' />
										</Button>
								</Input>
							</div>
							<div style={{ marginTop: '1em' }}>
								<Header as='h4'>Faction</Header>
								<Dropdown
									style={{ marginTop: '-1em' }}
									selection
									options={factionOptions}
									value={shuttle.faction}
									onChange={(e, { value }) => updateFaction(value)}
								/>
							</div>
						</Grid.Column>
						<Grid.Column>
							<Header as='h4'>Seats</Header>
							<p style={{ marginTop: '-1em' }}>Set each seat to the skills required. Add seats as necessary.</p>
							<Table collapsing unstackable compact='very' size='small'>
								<Table.Body>
									{shuttle.seats.map((seat, seatNum) => (
										<Table.Row key={seatNum}>
											<Table.Cell textAlign='right'>{seatNum+1}</Table.Cell>
											<Table.Cell textAlign='center'>
												<EditorSeat seatNum={seatNum} seat={seat} />
											</Table.Cell>
											<Table.Cell textAlign='right'>
												{shuttle.seats.length > 1 && <Button compact icon='trash' color='red' onClick={() => deleteMissionSeat(seatNum)} />}
											</Table.Cell>
										</Table.Row>
									))}
								</Table.Body>
							</Table>
							<Button compact icon='plus square outline' content='Add Seat' onClick={() => addMissionSeat()} />
						</Grid.Column>
					</Grid>
					<div style={{ marginTop: '1em' }}>
						<Divider />
						<p>If you no longer need this mission, you can delete it here. Note: missions will be automatically deleted after the event has concluded.</p>
						<p><Button icon='trash' color='red' content='Delete Mission' onClick={() => deleteMission(shuttle.id)} /></p>
					</div>
				</React.Fragment>
			);
		}

		function updateMissionName(newName: string): void {
			shuttle.name = newName;
			setShuttle({...shuttle});
		}

		function updateFaction(newFaction: number): void {
			shuttle.faction = newFaction;
			setShuttle({...shuttle});
		}

		function updateMissionSeat(seatNum: number, key: string, value: string): void {
			shuttle.seats[seatNum][key] = value;
			setShuttle({...shuttle});
		}

		function addMissionSeat(): void {
			shuttle.seats.push(new ShuttleSeat());
			setShuttle({...shuttle});
		}

		function deleteMissionSeat(seatNum: number): void {
			shuttle.seats.splice(seatNum, 1);
			setShuttle({...shuttle});
		}

		function applyEdits(): void {
			if (shuttle.priority === 0) shuttle.priority = missionsSelected + 1;
			const shuttleNum = shuttlers.shuttles.findIndex(s => s.id === shuttle.id);
			shuttlers.shuttles[shuttleNum] = shuttle;
			updateShuttlers();
			setEditMission(undefined);
		}
	};

	const missionsSelected = data.filter(shuttle => shuttle.priority > 0).length;

	return (
		<React.Fragment>
			<div>Click all the missions that you want to run, then click 'Recommend Crew' to see the best seats for your crew.</div>
			<Table celled striped selectable sortable singleLine>
				<Table.Header>
					<Table.Row>
						{tableConfig.map((cell, idx) => (
							<Table.HeaderCell key={idx}
								sorted={column === cell.column ? direction : null}
								onClick={() => dispatch({ type: 'CHANGE_SORT', column: cell.column, reverse: cell.reverse })}
								colSpan={cell.span ?? 1}
								textAlign={cell.align ?? 'left'}
							>
								{cell.title}
							</Table.HeaderCell>
						))}
					</Table.Row>
				</Table.Header>
				<Table.Body>
					{data.length === 0 && (
						<Table.Row>
							<Table.Cell colSpan={10} textAlign='center'>
								No missions available.
							</Table.Cell>
						</Table.Row>
					)}
					{data.map(shuttle => (
						<Table.Row key={shuttle.id} style={{ cursor: 'pointer' }}
							onClick={() => toggleMissionStatus(shuttle.id)}
							onDoubleClick={() => { toggleMissionStatus(shuttle.id); props.recommendShuttlers(); }}
						>
							<Table.Cell textAlign='center'>
								{shuttle.priority > 0 && (<Icon color='green' name='check' />)}
							</Table.Cell>
							<Table.Cell>
								<span style={{ fontSize: '1.1em' }}><b>{shuttle.name}</b></span>
							</Table.Cell>
							<Table.Cell textAlign='center'>
								<ShuttleFactionView factionId={shuttle.faction} size={1.5} />
							</Table.Cell>
							<Table.Cell textAlign='center'>{shuttle.seats.length}</Table.Cell>
							{[0, 1, 2, 3, 4].map(seatNum => (
								<Table.Cell key={seatNum} textAlign='center'>
									{shuttle.seats.length > seatNum && (
										<SeatSkillView seat={shuttle.seats[seatNum]} />
									)}
								</Table.Cell>
							))}
							<Table.Cell textAlign='right'>
								{!shuttle.readonly && (
									<Button icon='edit' content='Edit' onClick={(e) => { setEditMission(shuttle); e.stopPropagation(); }}/>
								)}
							</Table.Cell>
						</Table.Row>
					))}
				</Table.Body>
				<Table.Footer>
					<Table.Row>
						<Table.HeaderCell colSpan={10} textAlign='right'>
							{missionsSelected > 0 && (<Button compact icon='rocket' color='green' content='Recommend Crew' onClick={() => props.recommendShuttlers()} />)}
							{missionsSelected === 0 && (<Button compact icon='rocket' content='Recommend Crew' />)}
						</Table.HeaderCell>
					</Table.Row>
				</Table.Footer>
			</Table>
			{editMission && <MissionEditor shuttle={editMission} />}
			<p>If the mission you want isn't listed here, click 'Create Mission' to input the mission parameters manually. Tip: open shuttle missions in-game before uploading your player data to DataCore so that this tool can import the missions automatically.</p>
			<Button icon='plus square' content='Create Mission' onClick={() => createMission() } />
		</React.Fragment>
	);

	function reducer(state, action): any {
		switch (action.type) {
			case 'UPDATE_DATA':
				//const defaultColumn = action.data.filter(shuttle => shuttle.priority > 0).length ? 'priority' : 'name';
				const updatedData = action.data.slice();
				firstSort(updatedData, action.column ?? 'name', action.direction ?? 'ascending');
				return {
					column: action.column ?? 'name',
					data: updatedData,
					direction: action.direction ?? 'ascending'
				};
			case 'CHANGE_SORT':
				if (!action.column) {
					return {
						column: state.column,
						data: state.data,
						direction: state.direction
					};
				}
				if (state.column === action.column && action.column !== 'priority') {
					return {
						...state,
						data: state.data.slice().reverse(),
						direction: state.direction === 'ascending' ? 'descending' : 'ascending'
					};
				}
				else {
					const data = state.data.slice();
					firstSort(data, action.column, action.reverse);
					return {
						column: action.column,
						data: data,
						direction: action.reverse ? 'descending' : 'ascending'
					};
				}
			default:
				throw new Error();
		}
	}

	function firstSort(data: any[], column: string, reverse: boolean = false): any[] {
		data.sort((a, b) => {
			if (column === 'name') return a.name.localeCompare(b.name);
			let aValue = column.split('.').reduce((prev, curr) => prev.hasOwnProperty(curr) ? prev[curr] : undefined, a);
			let bValue = column.split('.').reduce((prev, curr) => prev.hasOwnProperty(curr) ? prev[curr] : undefined, b);
			// Always show selected missions at the top when sorting by priority
			if (column === 'priority') {
				if (aValue === 0) aValue = 100;
				if (bValue === 0) bValue = 100;
			}
			if (column === 'skills') {
				aValue = a.seats.length;
				bValue = b.seats.length;
			}
			// Tiebreaker goes to name ascending
			if (aValue === bValue) return a.name.localeCompare(b.name);
			if (reverse) bValue - aValue;
			return aValue - bValue;
		});
	}

	function checkMissions(shuttleIds: string[], checkState: boolean = true): void {
		let priority = 0;
		shuttlers.shuttles.forEach(shuttle => {
			if (shuttleIds.length === 0)
				shuttle.priority = checkState ? ++priority : 0;
			else
				shuttle.priority = checkState && shuttleIds.includes(shuttle.id) ? ++priority : 0;
		});
		updateShuttlers();
		if (shuttleIds.length !== 0)
			dispatch({ type: 'CHANGE_SORT', column: 'priority' });
	}

	function createMission(): void {
		const shuttle = new Shuttle(groupId);
		shuttle.seats.push(new ShuttleSeat());
		shuttlers.shuttles.push(shuttle);
		updateShuttlers();
		setEditMission(shuttle);
	}

	function deleteMission(shuttleId: string): void {
		const shuttleNum = shuttlers.shuttles.findIndex(shuttle => shuttle.id === shuttleId);
		shuttlers.shuttles.splice(shuttleNum, 1);
		updateShuttlers();
		setEditMission(undefined);
	}

	function toggleMissionStatus(shuttleId: string): void {
		const shuttle = shuttlers.shuttles.find(shuttle => shuttle.id === shuttleId);
		shuttle.priority = shuttle.priority === 0 ? missionsSelected+1 : 0;
		updateShuttlers();
	}

	function updateShuttlers(): void {
		setShuttlers({...shuttlers});
	}
}
Example #18
Source File: voyagecalculator.tsx    From website with MIT License 4 votes vote down vote up
VoyageEditConfigModal = (props: VoyageEditConfigModalProps) => {
	const { updateConfig } = props;

	const [voyageConfig, setVoyageConfig] = React.useState(props.voyageConfig);

	const [modalIsOpen, setModalIsOpen] = React.useState(false);
	const [updateOnClose, setUpdateOnClose] = React.useState(false);
	const [options, setOptions] = React.useState(undefined);

	React.useEffect(() => {
		if (!modalIsOpen && updateOnClose) {
			updateConfig(voyageConfig);
			setUpdateOnClose(false);
		}

	}, [modalIsOpen]);

	const defaultSlots = [
		{ symbol: 'captain_slot', name: 'First Officer', skill: 'command_skill', trait: '' },
		{ symbol: 'first_officer', name: 'Helm Officer', skill: 'command_skill', trait: '' },
		{ symbol: 'chief_communications_officer', name: 'Communications Officer', skill: 'diplomacy_skill', trait: '' },
		{ symbol: 'communications_officer', name: 'Diplomat', skill: 'diplomacy_skill', trait: '' },
		{ symbol: 'chief_security_officer', name: 'Chief Security Officer', skill: 'security_skill', trait: '' },
		{ symbol: 'security_officer', name: 'Tactical Officer', skill: 'security_skill', trait: '' },
		{ symbol: 'chief_engineering_officer', name: 'Chief Engineer', skill: 'engineering_skill', trait: '' },
		{ symbol: 'engineering_officer', name: 'Engineer', skill: 'engineering_skill', trait: '' },
		{ symbol: 'chief_science_officer', name: 'Chief Science Officer', skill: 'science_skill', trait: '' },
		{ symbol: 'science_officer', name: 'Deputy Science Officer', skill: 'science_skill', trait: '' },
		{ symbol: 'chief_medical_officer', name: 'Chief Medical Officer', skill: 'medicine_skill', trait: '' },
		{ symbol: 'medical_officer', name: 'Ship\'s Counselor', skill: 'medicine_skill', trait: '' }
	];
	const crewSlots = voyageConfig.crew_slots ?? defaultSlots;
	crewSlots.sort((s1, s2) => CONFIG.VOYAGE_CREW_SLOTS.indexOf(s1.symbol) - CONFIG.VOYAGE_CREW_SLOTS.indexOf(s2.symbol));

	return (
		<Modal
			open={modalIsOpen}
			onClose={() => setModalIsOpen(false)}
			onOpen={() => setModalIsOpen(true)}
			trigger={<Button size='small'><Icon name='edit' />Edit</Button>}
		>
			<Modal.Header>Edit Voyage</Modal.Header>
			<Modal.Content scrolling>
				{renderContent()}
			</Modal.Content>
			<Modal.Actions>
				<Button positive onClick={() => setModalIsOpen(false)}>
					Close
				</Button>
			</Modal.Actions>
		</Modal>
	);

	function renderContent(): JSX.Element {
		if (!modalIsOpen) return (<></>);

		if (!options) {
			// Renders a lot faster by using known voyage traits rather than calculate list from all possible traits
			const knownShipTraits = ['andorian','battle_cruiser','borg','breen','cardassian','cloaking_device',
				'dominion','emp','explorer','federation','ferengi','freighter','historic','hologram',
				'klingon','malon','maquis','orion_syndicate','pioneer','reman','romulan','ruthless',
				'scout','spore_drive','terran','tholian','transwarp','vulcan','warship','war_veteran','xindi'];
			const knownCrewTraits = ['android','astrophysicist','bajoran','borg','brutal',
				'cardassian','civilian','communicator','costumed','crafty','cultural_figure','cyberneticist',
				'desperate','diplomat','doctor','duelist','exobiology','explorer','federation','ferengi',
				'gambler','hero','hologram','human','hunter','innovator','inspiring','jury_rigger','klingon',
				'marksman','maverick','pilot','prodigy','resourceful','romantic','romulan',
				'saboteur','scoundrel','starfleet','survivalist','tactician','telepath','undercover_operative',
				'veteran','villain','vulcan'];

			const skillsList = [];
			for (let skill in CONFIG.SKILLS) {
				skillsList.push({
					key: skill,
					value: skill,
					text: CONFIG.SKILLS[skill]
				});
			}

			const shipTraitsList = knownShipTraits.map(trait => {
				return {
					key: trait,
					value: trait,
					text: allTraits.ship_trait_names[trait]
				};
			});
			shipTraitsList.sort((a, b) => a.text.localeCompare(b.text));

			const crewTraitsList = knownCrewTraits.map(trait => {
				return {
					key: trait,
					value: trait,
					text: allTraits.trait_names[trait]
				};
			});
			crewTraitsList.sort((a, b) => a.text.localeCompare(b.text));

			setOptions({ skills: skillsList, ships: shipTraitsList, traits: crewTraitsList });
			return (<></>);
		}

		return (
			<React.Fragment>
				<Message>Editing this voyage will reset all existing recommendations and estimates.</Message>
				<Form>
					<Form.Group>
						<Form.Select
							label='Primary skill'
							options={options.skills}
							value={voyageConfig.skills.primary_skill ?? 'command_skill'}
							onChange={(e, { value }) => setSkill('primary_skill', value)}
							placeholder='Primary'
						/>
						<Form.Select
							label='Secondary skill'
							options={options.skills}
							value={voyageConfig.skills.secondary_skill ?? 'science_skill'}
							onChange={(e, { value }) => setSkill('secondary_skill', value)}
							placeholder='Secondary'
						/>
						<Form.Select
							search clearable
							label='Ship trait'
							options={options.ships}
							value={voyageConfig.ship_trait}
							onChange={(e, { value }) => setShipTrait(value)}
							placeholder='Ship trait'
						/>
					</Form.Group>
				</Form>
				<Table compact striped>
					<Table.Header>
						<Table.Row>
							<Table.HeaderCell textAlign='center'>Skill</Table.HeaderCell>
							<Table.HeaderCell>Seat</Table.HeaderCell>
							<Table.HeaderCell>Trait</Table.HeaderCell>
						</Table.Row>
					</Table.Header>
					<Table.Body>
						{crewSlots.map((seat, idx) => (
							<Table.Row key={seat.symbol}>
								{ idx % 2 == 0 ?
									(
										<Table.Cell rowSpan='2' textAlign='center'>
											<img alt="{seat.skill}" src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${seat.skill}.png`} style={{ height: '2em' }} />
										</Table.Cell>
									)
									: (<></>)
								}
								<Table.Cell>{seat.name}</Table.Cell>
								<Table.Cell>
									<Dropdown search selection clearable
										options={options.traits}
										value={seat.trait}
										onChange={(e, { value }) => setSeatTrait(seat.symbol, value)}
										placeholder='Trait'
									/>
								</Table.Cell>
							</Table.Row>
						))}
					</Table.Body>
				</Table>
			</React.Fragment>
		);
	}

	function setSkill(prime: string, value: string): void {
		// Flip skill values if changing to value that's currently set as the other prime
		if (prime == 'primary_skill' && value == voyageConfig.skills.secondary_skill)
			voyageConfig.skills.secondary_skill = voyageConfig.skills.primary_skill;
		else if (prime == 'secondary_skill' && value == voyageConfig.skills.primary_skill)
			voyageConfig.skills.primary_skill = voyageConfig.skills.secondary_skill;
		voyageConfig.skills[prime] = value;
		setVoyageConfig({...voyageConfig});
		setUpdateOnClose(true);
	}

	function setShipTrait(value: string): void {
		voyageConfig.ship_trait = value;
		setVoyageConfig({...voyageConfig});
		setUpdateOnClose(true);
	}

	function setSeatTrait(seat: symbol, value: string): void {
		voyageConfig.crew_slots.find(s => s.symbol === seat).trait = value;
		setVoyageConfig({...voyageConfig});
		setUpdateOnClose(true);
	}
}
Example #19
Source File: profile_items.tsx    From website with MIT License 4 votes vote down vote up
render() {
		const { column, direction, pagination_rows, pagination_page } = this.state;
		let { data } = this.state;

		let totalPages = Math.ceil(data.length / this.state.pagination_rows);

		// Pagination
		data = data.slice(pagination_rows * (pagination_page - 1), pagination_rows * pagination_page);

		return (
			<Table sortable celled selectable striped collapsing unstackable compact="very">
				<Table.Header>
					<Table.Row>
						<Table.HeaderCell
							width={3}
							sorted={column === 'name' ? direction : null}
							onClick={() => this._handleSort('name')}
						>
							Item
						</Table.HeaderCell>
						<Table.HeaderCell
							width={1}
							sorted={column === 'quantity' ? direction : null}
							onClick={() => this._handleSort('quantity')}
						>
							Quantity
						</Table.HeaderCell>
						<Table.HeaderCell
							width={1}
							sorted={column === 'type' ? direction : null}
							onClick={() => this._handleSort('type')}
						>
							Item type
						</Table.HeaderCell>
						<Table.HeaderCell
							width={1}
							sorted={column === 'rarity' ? direction : null}
							onClick={() => this._handleSort('rarity')}
						>
							Rarity
						</Table.HeaderCell>
					</Table.Row>
				</Table.Header>
				<Table.Body>
					{data.map((item, idx) => (
						<Table.Row key={idx}>
							<Table.Cell>
								<div
									style={{
										display: 'grid',
										gridTemplateColumns: '60px auto',
										gridTemplateAreas: `'icon stats' 'icon description'`,
										gridGap: '1px'
									}}
								>
									<div style={{ gridArea: 'icon' }}>
										<img width={48} src={`${process.env.GATSBY_ASSETS_URL}${item.imageUrl}`} />
									</div>
									<div style={{ gridArea: 'stats' }}>
										<Link to={`/item_info?symbol=${item.symbol}`}>
											<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}>
												{item.rarity > 0 && (
													<span>
														{item.rarity} <Icon name="star" />{' '}
													</span>
												)}
												{item.name}
											</span>
										</Link>
									</div>
									<div style={{ gridArea: 'description' }}>{item.flavor}</div>
								</div>
							</Table.Cell>
							<Table.Cell>{item.quantity}</Table.Cell>
							<Table.Cell>{CONFIG.REWARDS_ITEM_TYPE[item.type]}</Table.Cell>
							<Table.Cell>{CONFIG.RARITIES[item.rarity].name}</Table.Cell>
						</Table.Row>
					))}
				</Table.Body>
				<Table.Footer>
					<Table.Row>
						<Table.HeaderCell colSpan="8">
							<Pagination
								totalPages={totalPages}
								activePage={pagination_page}
								onPageChange={(event, { activePage }) => this._onChangePage(activePage)}
							/>
							<span style={{ paddingLeft: '2em' }}>
								Items per page:{' '}
								<Dropdown
									inline
									options={pagingOptions}
									value={pagination_rows}
									onChange={(event, { value }) =>
										this.setState({ pagination_page: 1, pagination_rows: value as number })
									}
								/>
							</span>
						</Table.HeaderCell>
					</Table.Row>
				</Table.Footer>
			</Table>
		);
	}
Example #20
Source File: fleet_info.tsx    From website with MIT License 4 votes vote down vote up
render() {
		const { fleet_id, errorMessage, fleet_data, factions, events } = this.state;

		if (fleet_id === undefined || fleet_data === undefined || errorMessage !== undefined) {
			return (
				<Layout title='Fleet information'>
					<Header as="h4">Fleet information</Header>
					{errorMessage && (
						<Message negative>
							<Message.Header>Unable to load fleet profile</Message.Header>
							<pre>{errorMessage.toString()}</pre>
						</Message>
					)}
					{!errorMessage && (
						<div>
							<Icon loading name="spinner" /> Loading...
						</div>
					)}
					<p>
						Are you looking to share your player profile? Go to the <Link to={`/playertools`}>Player Tools page</Link> to
							upload your player.json and access other useful player tools.
						</p>
				</Layout>
			);
		}

		let imageUrl = 'icons_icon_faction_starfleet.png';
		if (factions && factions[fleet_data.nicon_index]) {
			imageUrl = factions[fleet_data.nicon_index].icon;
		}

		let event1;
		let event2;
		let event3;

		if (events[0].event_name === fleet_data.leaderboard[0].event_name) {
			event1 = events[0];
			event2 = events[1];
			event3 = events[2];
		} else {
			event1 = events.find(ev => ev.event_name === fleet_data.leaderboard[0].event_name);
			event2 = events.find(ev => ev.event_name === fleet_data.leaderboard[1].event_name);
			event3 = events.find(ev => ev.event_name === fleet_data.leaderboard[2].event_name);
		}

		return (
			<Layout title={fleet_data.name}>
				<Item.Group>
					<Item>
						<Item.Image size="tiny" src={`${process.env.GATSBY_ASSETS_URL}${imageUrl}`} />

						<Item.Content>
							<Item.Header>{fleet_data.name}</Item.Header>
							<Item.Meta>
								<Label>Starbase level: {fleet_data.nstarbase_level}</Label>
								<Label>
									Size: {fleet_data.cursize} / {fleet_data.maxsize}
								</Label>
								<Label>Created: {new Date(fleet_data.created).toLocaleDateString()}</Label>
								<Label>
									Enrollment {fleet_data.enrollment} (min level: {fleet_data.nmin_level})
									</Label>
							</Item.Meta>
						</Item.Content>
					</Item>
				</Item.Group>

				{event1 && <table>
					<tbody>
						<tr>
							<th>
								{' '}
								<Link to={`/event_info?instance_id=${event1.instance_id}`}>
									{fleet_data.leaderboard[0].event_name}
								</Link>
							</th>
							<th>
								{' '}
								<Link to={`/event_info?instance_id=${event2.instance_id}`}>
									{fleet_data.leaderboard[1].event_name}
								</Link>
							</th>
							<th>
								{' '}
								<Link to={`/event_info?instance_id=${event3.instance_id}`}>
									{fleet_data.leaderboard[2].event_name}
								</Link>
							</th>
						</tr>
						<tr>
							<td><Image size="medium" src={`${process.env.GATSBY_ASSETS_URL}${event1.image}`} /></td>
							<td><Image size="medium" src={`${process.env.GATSBY_ASSETS_URL}${event2.image}`} /></td>
							<td><Image size="medium" src={`${process.env.GATSBY_ASSETS_URL}${event3.image}`} /></td>
						</tr>
						<tr>
							<td align="center">Fleet rank: {fleet_data.leaderboard[0].fleet_rank}</td>
							<td align="center">Fleet rank: {fleet_data.leaderboard[1].fleet_rank}</td>
							<td align="center">Fleet rank: {fleet_data.leaderboard[2].fleet_rank}</td>
						</tr>
					</tbody>
				</table>}

				<Header as="h4">Members</Header>

				<Table celled selectable striped collapsing unstackable compact="very">
					<Table.Header>
						<Table.Row>
							<Table.HeaderCell width={3}>Name</Table.HeaderCell>
							<Table.HeaderCell width={1}>Rank</Table.HeaderCell>
							<Table.HeaderCell width={1}>Profile</Table.HeaderCell>
						</Table.Row>
					</Table.Header>
					<Table.Body>
						{fleet_data.members.map((member, idx) => (
							<Table.Row key={idx}>
								<Table.Cell>
									<div
										style={{
											display: 'grid',
											gridTemplateColumns: '60px auto',
											gridTemplateAreas: `'icon stats' 'icon description'`,
											gridGap: '1px'
										}}
									>
										<div style={{ gridArea: 'icon' }}>
											<img
												width={48}
												src={`${process.env.GATSBY_ASSETS_URL}${member.crew_avatar || 'crew_portraits_cm_empty_sm.png'}`}
											/>
										</div>
										<div style={{ gridArea: 'stats' }}>
											<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}>
												{member.last_update ? (
													<Link to={`/profile?dbid=${member.dbid}`}>{member.display_name}</Link>
												) : (
														<span>{member.display_name}</span>
													)}
											</span>
										</div>
										<div style={{ gridArea: 'description' }}>
											{member.last_update && (
												<Label size="tiny">
													Last profile upload: {new Date(Date.parse(member.last_update)).toLocaleDateString()}
												</Label>
											)}
										</div>
									</div>
								</Table.Cell>
								<Table.Cell>{member.rank}</Table.Cell>
								<Table.Cell>
									{member.last_update
										? `Last profile upload: ${new Date(Date.parse(member.last_update)).toLocaleDateString()}`
										: 'Never'}
								</Table.Cell>
							</Table.Row>
						))}
					</Table.Body>
				</Table>
			</Layout>
		);
	}
Example #21
Source File: event_info.tsx    From website with MIT License 4 votes vote down vote up
renderEventDetails() {
		const { event_data } = this.state;

		if (event_data === undefined || event_data.event_details === undefined) {
			return <span />;
		}

		let event = event_data.event_details;

		return (
			<div>
				<Message icon warning>
					<Icon name='exclamation triangle' />
					<Message.Content>
						<Message.Header>Work in progress!</Message.Header>
						This section is under development and not fully functional yet.
					</Message.Content>
				</Message>
				<p>{event.description}</p>
				<p>{event.rules}</p>

				<Label>{event.bonus_text}</Label>

				{event.content.map((cnt, idx) => {
					let crew_bonuses = undefined;
					if (cnt.shuttles) {
						crew_bonuses = cnt.shuttles[0].crew_bonuses;
					} else if (cnt.crew_bonuses) {
						crew_bonuses = cnt.crew_bonuses;
					} else if (cnt.bonus_crew && cnt.bonus_traits) {
						// Skirmishes
						crew_bonuses = {};
						cnt.bonus_crew.forEach(element => {
							crew_bonuses[element] = 10;
						});

						// TODO: crew from traits
					} else if (cnt.special_crew) {
						// Expeditions
						crew_bonuses = {};
						cnt.special_crew.forEach(element => {
							crew_bonuses[element] = 50;
						});
					}
					return (
						<div key={idx}>
							<Header as='h5'>Phase {idx + 1}</Header>
							<Label>Type: {cnt.content_type}</Label>
							<Header as='h6'>Bonus crew</Header>
							{crew_bonuses && (
								<p>
									{Object.entries(crew_bonuses).map(([bonus, val], idx) => (
										<span key={idx}>
											{bonus} ({val}),
										</span>
									))}
								</p>
							)}
						</div>
					);
				})}

				<Header as='h4'>Threshold rewards</Header>
				<Table celled selectable striped collapsing unstackable compact='very'>
					<Table.Header>
						<Table.Row>
							<Table.HeaderCell width={2}>Points</Table.HeaderCell>
							<Table.HeaderCell width={4}>Reward</Table.HeaderCell>
						</Table.Row>
					</Table.Header>
					<Table.Body>
						{event.threshold_rewards.filter(reward => reward.rewards && reward.rewards.length > 0).map((reward, idx) => (
							<Table.Row key={idx}>
								<Table.Cell>{reward.points}</Table.Cell>
								<Table.Cell>
									{reward.rewards[0].quantity} {reward.rewards[0].full_name}
								</Table.Cell>
							</Table.Row>
						))}
					</Table.Body>
				</Table>

				<Header as='h4'>Ranked rewards</Header>
				<Table celled selectable striped collapsing unstackable compact='very'>
					<Table.Header>
						<Table.Row>
							<Table.HeaderCell width={2}>Ranks</Table.HeaderCell>
							<Table.HeaderCell width={4}>Rewards</Table.HeaderCell>
						</Table.Row>
					</Table.Header>
					<Table.Body>
						{event.ranked_brackets.map((bracket, idx) => (
							<Table.Row key={idx}>
								<Table.Cell>
									{bracket.first} - {bracket.last}
								</Table.Cell>
								<Table.Cell>
									{bracket.rewards.map((reward, idx) => (
										<span key={idx}>
											{reward.quantity} {reward.full_name},
										</span>
									))}
								</Table.Cell>
							</Table.Row>
						))}
					</Table.Body>
				</Table>

				<Header as='h4'>Quest</Header>

				{event.quest ? event.quest.map((quest, idx) => (
					<div key={idx}>
						{quest && quest.screens && quest.screens.map((screen, idx) => (
							<p key={idx}>
								<b>{screen.speaker_name}: </b>
								{screen.text}
							</p>
						))}
					</div>
				)) : <span>Mini-events don't include quest information.</span>}

				<Message>
					<Message.Header>TODO: Leaderboard out of date</Message.Header>
					If this event is currently active, the leaderboard below is out of date (updated only a couple of times a week).
				</Message>
			</div>
		);
	}
Example #22
Source File: prospectpicker.tsx    From website with MIT License 4 votes vote down vote up
ProspectPicker = (props: ProspectPickerProps) => {
	const { pool, prospects, setProspects } = props;

	enum OptionsState {
		Uninitialized,
		Initializing,
		Ready
	};

	const [selection, setSelection] = React.useState('');
	const [options, setOptions] = React.useState({
		state: OptionsState.Uninitialized,
		list: []
	});

	if (pool.length == 0) return (<></>);

	const placeholder = options.state === OptionsState.Initializing ? 'Loading. Please wait...' : 'Select Crew';

	return (
		<React.Fragment>
			<Dropdown search selection clearable
				placeholder={placeholder}
				options={options.list}
				value={selection}
				onFocus={() => { if (options.state === OptionsState.Uninitialized) populateOptions(); }}
				onChange={(e, { value }) => setSelection(value)}
			/>
			<Button compact icon='add user' color='green' content='Add Crew' onClick={() => { addProspect(); }} style={{ marginLeft: '1em' }} />
			<Table celled striped collapsing unstackable compact="very">
				<Table.Body>
					{prospects.map((p, prospectNum) => (
						<Table.Row key={prospectNum}>
							<Table.Cell><img width={24} src={`${process.env.GATSBY_ASSETS_URL}${p.imageUrlPortrait}`} /></Table.Cell>
							<Table.Cell><Link to={`/crew/${p.symbol}/`}>{p.name}</Link></Table.Cell>
							<Table.Cell>
								<Rating size='large' icon='star' rating={p.rarity} maxRating={p.max_rarity}
									onRate={(e, {rating, maxRating}) => { fuseProspect(prospectNum, rating); }} />
							</Table.Cell>
							<Table.Cell>
								<Button compact icon='trash' color='red' onClick={() => deleteProspect(prospectNum)} />
							</Table.Cell>
						</Table.Row>
					))}
				</Table.Body>
			</Table>
		</React.Fragment>
	);

	function populateOptions(): void {
		setOptions({
			state: OptionsState.Initializing,
			list: []
		});
		// Populate inside a timeout so that UI can update with a "Loading" placeholder first
		setTimeout(() => {
			const populatePromise = new Promise((resolve, reject) => {
				const poolList = pool.map((c) => (
					{
						key: c.symbol,
						value: c.symbol,
						image: { avatar: true, src: `${process.env.GATSBY_ASSETS_URL}${c.imageUrlPortrait}` },
						text: c.name
					}
				));
				resolve(poolList);
			});
			populatePromise.then((poolList) => {
				setOptions({
					state: OptionsState.Initialized,
					list: poolList
				});
			});
		}, 0);
	}

	function addProspect(): void {
		if (selection == '') return;
		let valid = pool.find((c) => c.symbol == selection);
		if (valid) {
			let prospect = {
				symbol: valid.symbol,
				name: valid.name,
				imageUrlPortrait: valid.imageUrlPortrait,
				rarity: valid.max_rarity,
				max_rarity: valid.max_rarity
			};
			prospects.push(prospect);
			setProspects([...prospects]);
		};
		setSelection('');
	}

	function fuseProspect(prospectNum: number, rarity: number): void {
		if (rarity == 0) return;
		prospects[prospectNum].rarity = rarity;
		setProspects([...prospects]);
	}

	function deleteProspect(prospectNum: number): void {
		prospects.splice(prospectNum, 1);
		setProspects([...prospects]);
	}
}
Example #23
Source File: profile_ships.tsx    From website with MIT License 4 votes vote down vote up
render() {
		const { column, direction, pagination_rows, pagination_page } = this.state;
		let { data } = this.state;

		let totalPages = Math.ceil(data.length / this.state.pagination_rows);

		// Pagination
		data = data.slice(pagination_rows * (pagination_page - 1), pagination_rows * pagination_page);

		return (
			<Table sortable celled selectable striped collapsing unstackable compact="very">
				<Table.Header>
					<Table.Row>
						<Table.HeaderCell
							width={3}
							sorted={column === 'name' ? direction : null}
							onClick={() => this._handleSort('name')}
						>
							Ship
						</Table.HeaderCell>
						<Table.HeaderCell
							width={1}
							sorted={column === 'antimatter' ? direction : null}
							onClick={() => this._handleSort('antimatter')}
						>
							Antimatter
						</Table.HeaderCell>
						<Table.HeaderCell
							width={1}
							sorted={column === 'accuracy' ? direction : null}
							onClick={() => this._handleSort('accuracy')}
						>
							Accuracy
						</Table.HeaderCell>
						<Table.HeaderCell
							width={1}
							sorted={column === 'attack' ? direction : null}
							onClick={() => this._handleSort('attack')}
						>
							Attack
						</Table.HeaderCell>
						<Table.HeaderCell
							width={1}
							sorted={column === 'evasion' ? direction : null}
							onClick={() => this._handleSort('evasion')}
						>
							Evasion
						</Table.HeaderCell>
						<Table.HeaderCell
							width={1}
							sorted={column === 'hull' ? direction : null}
							onClick={() => this._handleSort('hull')}
						>
							Hull
						</Table.HeaderCell>
						<Table.HeaderCell
							width={1}
							sorted={column === 'shields' ? direction : null}
							onClick={() => this._handleSort('shields')}
						>
							Shields
						</Table.HeaderCell>
						<Table.HeaderCell
							width={1}
							sorted={column === 'max_level' ? direction : null}
							onClick={() => this._handleSort('max_level')}
						>
							Level
						</Table.HeaderCell>
					</Table.Row>
				</Table.Header>
				<Table.Body>
					{data.map((ship, idx) => (
						<Table.Row key={idx}>
							<Table.Cell>
								<div
									style={{
										display: 'grid',
										gridTemplateColumns: '60px auto',
										gridTemplateAreas: `'icon stats' 'icon description'`,
										gridGap: '1px'
									}}
								>
									<div style={{ gridArea: 'icon' }}>
										<img width={48} src={`${process.env.GATSBY_ASSETS_URL}${ship.icon.file.substr(1).replace('/', '_')}.png`} />
									</div>
									<div style={{ gridArea: 'stats' }}>
										<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}>{ship.name}</span>
									</div>
									<div style={{ gridArea: 'description' }}>{ship.traits_named.join(', ')}</div>
								</div>
							</Table.Cell>
							<Table.Cell>{ship.antimatter}</Table.Cell>
							<Table.Cell>{ship.accuracy}</Table.Cell>
							<Table.Cell>{ship.attack} ({ship.attacks_per_second}/s)</Table.Cell>
							<Table.Cell>{ship.evasion}</Table.Cell>
							<Table.Cell>{ship.hull}</Table.Cell>
							<Table.Cell>{ship.shields} (regen {ship.shield_regen})</Table.Cell>
							<Table.Cell>{ship.level} / {ship.max_level}</Table.Cell>
						</Table.Row>
					))}
				</Table.Body>
				<Table.Footer>
					<Table.Row>
						<Table.HeaderCell colSpan="8">
							<Pagination
								totalPages={totalPages}
								activePage={pagination_page}
								onPageChange={(event, { activePage }) => this._onChangePage(activePage)}
							/>
							<span style={{ paddingLeft: '2em' }}>
								Ships per page:{' '}
								<Dropdown
									inline
									options={pagingOptions}
									value={pagination_rows}
									onChange={(event, { value }) =>
										this.setState({ pagination_page: 1, pagination_rows: value as number })
									}
								/>
							</span>
						</Table.HeaderCell>
					</Table.Row>
				</Table.Footer>
			</Table>
		);
	}
Example #24
Source File: profile_crew.tsx    From website with MIT License 4 votes vote down vote up
ProfileCrewTable = (props: ProfileCrewTableProps) => {
	const pageId = props.pageId ?? 'crew';
	const [showFrozen, setShowFrozen] = useStateWithStorage(pageId+'/showFrozen', true);
	const [findDupes, setFindDupes] = useStateWithStorage(pageId+'/findDupes', false);

	const myCrew = JSON.parse(JSON.stringify(props.crew));

	const tableConfig: ITableConfigRow[] = [
		{ width: 3, column: 'name', title: 'Crew', pseudocolumns: ['name', 'events', 'collections.length'] },
		{ width: 1, column: 'max_rarity', title: 'Rarity', reverse: true, tiebreakers: ['rarity'] },
		{ width: 1, column: 'bigbook_tier', title: 'Tier' },
		{ width: 1, column: 'cab_ov', title: <span>CAB <CABExplanation /></span>, reverse: true, tiebreakers: ['cab_ov_rank'] },
		{ width: 1, column: 'ranks.voyRank', title: 'Voyage' }
	];
	CONFIG.SKILLS_SHORT.forEach((skill) => {
		tableConfig.push({
			width: 1,
			column: `${skill.name}.core`,
			title: <img alt={CONFIG.SKILLS[skill.name]} src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${skill.name}.png`} style={{ height: '1.1em' }} />,
			reverse: true
		});
	});

	function showThisCrew(crew: any, filters: [], filterType: string): boolean {
		if (!showFrozen && crew.immortal > 0) {
			return false;
		}

		if (findDupes) {
			if (myCrew.filter((c) => c.symbol === crew.symbol).length === 1)
				return false;
		}

		return crewMatchesSearchFilter(crew, filters, filterType);
	}

	function renderTableRow(crew: any, idx: number, highlighted: boolean): JSX.Element {
		const attributes = {
			positive: highlighted
		};

		return (
			<Table.Row key={idx} style={{ cursor: 'zoom-in' }} onClick={() => navigate(`/crew/${crew.symbol}/`)} {...attributes}>
				<Table.Cell>
					<div
						style={{
							display: 'grid',
							gridTemplateColumns: '60px auto',
							gridTemplateAreas: `'icon stats' 'icon description'`,
							gridGap: '1px'
						}}
					>
						<div style={{ gridArea: 'icon' }}>
							<img width={48} src={`${process.env.GATSBY_ASSETS_URL}${crew.imageUrlPortrait}`} />
						</div>
						<div style={{ gridArea: 'stats' }}>
							<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}><Link to={`/crew/${crew.symbol}/`}>{crew.name}</Link></span>
						</div>
						<div style={{ gridArea: 'description' }}>{descriptionLabel(crew)}</div>
					</div>
				</Table.Cell>
				<Table.Cell>
					<Rating icon='star' rating={crew.rarity} maxRating={crew.max_rarity} size="large" disabled />
				</Table.Cell>
				<Table.Cell textAlign="center">
					<b>{formatTierLabel(crew.bigbook_tier)}</b>
				</Table.Cell>
				<Table.Cell style={{ textAlign: 'center' }}>
					<b>{crew.cab_ov}</b><br />
					<small>{rarityLabels[parseInt(crew.max_rarity)-1]} #{crew.cab_ov_rank}</small>
				</Table.Cell>
				<Table.Cell style={{ textAlign: 'center' }}>
					<b>#{crew.ranks.voyRank}</b><br />
					{crew.ranks.voyTriplet && <small>Triplet #{crew.ranks.voyTriplet.rank}</small>}
				</Table.Cell>
				{CONFIG.SKILLS_SHORT.map(skill =>
					crew[skill.name].core > 0 ? (
						<Table.Cell key={skill.name} textAlign='center'>
							<b>{crew[skill.name].core}</b>
							<br />
							+({crew[skill.name].min}-{crew[skill.name].max})
						</Table.Cell>
					) : (
						<Table.Cell key={skill.name} />
					)
				)}
			</Table.Row>
		);
	}

	function descriptionLabel(crew: any): JSX.Element {
		if (crew.immortal) {
			return (
				<div>
					<Icon name="snowflake" /> <span>{crew.immortal} frozen</span>
				</div>
			);
		} else {
			const counts = [
				{ name: 'event', count: crew.events },
				{ name: 'collection', count: crew.collections.length }
			];
			const formattedCounts = counts.map((count, idx) => (
				<span key={idx} style={{ whiteSpace: 'nowrap' }}>
					{count.count} {count.name}{count.count != 1 ? 's' : ''}{idx < counts.length-1 ? ',' : ''}
				</span>
			)).reduce((prev, curr) => [prev, ' ', curr]);

			return (
				<div>
					{crew.favorite && <Icon name="heart" />}
					{crew.prospect && <Icon name="add user" />}
					<span>Level {crew.level}, </span>
					{formattedCounts}
				</div>
			);
		}
	}

	return (
		<React.Fragment>
			<div style={{ margin: '.5em 0' }}>
				<Form.Group grouped>
					<Form.Field
						control={Checkbox}
						label='Show frozen (vaulted) crew'
						checked={showFrozen}
						onChange={(e, { checked }) => setShowFrozen(checked)}
					/>
					<Form.Field
						control={Checkbox}
						label='Only show duplicate crew'
						checked={findDupes}
						onChange={(e, { checked }) => setFindDupes(checked)}
					/>
				</Form.Group>
			</div>
			<SearchableTable
				id={`${pageId}/table_`}
				data={myCrew}
				config={tableConfig}
				renderTableRow={(crew, idx, highlighted) => renderTableRow(crew, idx, highlighted)}
				filterRow={(crew, filters, filterType) => showThisCrew(crew, filters, filterType)}
				showFilterOptions="true"
				initOptions={props.initOptions}
				lockable={props.lockable}
			/>
		</React.Fragment>
	);
}
Example #25
Source File: profile_charts.tsx    From website with MIT License 4 votes vote down vote up
render() {
		const {
			data_ownership,
			skill_distribution,
			flat_skill_distribution,
			r4_stars,
			r5_stars,
			radar_skill_rarity,
			radar_skill_rarity_owned,
			honordebt,
			excludeFulfilled,
		} = this.state;

		let { demands } = this.state;

		let totalHonorDebt = 0;
		let readableHonorDebt = '';

		if (honordebt) {
			totalHonorDebt = honordebt.totalStars
				.map((val, idx) => (val - honordebt.ownedStars[idx]) * CONFIG.CITATION_COST[idx])
				.reduce((a, b) => a + b, 0);

			let totalHonorDebtDays = totalHonorDebt / 2000;

			let years = Math.floor(totalHonorDebtDays / 365);
			let months = Math.floor((totalHonorDebtDays - years * 365) / 30);
			let days = totalHonorDebtDays - years * 365 - months * 30;

			readableHonorDebt = `${years} years ${months} months ${Math.floor(days)} days`;
		}

		let totalChronCost = 0;
		let factionRec = [];
		demands.forEach((entry) => {
			let cost = entry.equipment.item_sources.map((its: any) => its.avg_cost).filter((cost) => !!cost);
			if (cost && cost.length > 0) {
				totalChronCost += Math.min(...cost) * entry.count;
			} else {
				let factions = entry.equipment.item_sources.filter((e) => e.type === 1);
				if (factions && factions.length > 0) {
					let fe = factionRec.find((e: any) => e.name === factions[0].name);
					if (fe) {
						fe.count += entry.count;
					} else {
						factionRec.push({
							name: factions[0].name,
							count: entry.count,
						});
					}
				}
			}
		});

		if (excludeFulfilled) {
			demands = demands.filter((d) => d.count > d.have);
		}

		factionRec = factionRec.sort((a, b) => b.count - a.count).filter((e) => e.count > 0);

		totalChronCost = Math.floor(totalChronCost);

		return (
			<ErrorBoundary>
				<h3>Owned vs. Not Owned crew per rarity</h3>
				<div style={{ height: '320px' }}>
					<ResponsiveBar
						data={data_ownership}
						theme={themes.dark}
						keys={['Owned', 'Not Owned', 'Not Owned - Portal']}
						indexBy='rarity'
						layout='horizontal'
						margin={{ top: 50, right: 130, bottom: 50, left: 100 }}
						padding={0.3}
						axisBottom={{
							legend: 'Number of crew',
							legendPosition: 'middle',
							legendOffset: 32,
						}}
						labelSkipWidth={12}
						labelSkipHeight={12}
						legends={[
							{
								dataFrom: 'keys',
								anchor: 'bottom-right',
								direction: 'column',
								justify: false,
								translateX: 120,
								translateY: 0,
								itemsSpacing: 2,
								itemWidth: 100,
								itemHeight: 20,
								itemDirection: 'left-to-right',
								symbolSize: 20,
								effects: [
									{
										on: 'hover',
										style: {
											itemOpacity: 1,
										},
									},
								],
							},
						]}
						animate={false}
					/>
				</div>

				<h3>Honor debt</h3>
				{honordebt && (
					<div>
						<Table basic='very' striped>
							<Table.Header>
								<Table.Row>
									<Table.HeaderCell>Rarity</Table.HeaderCell>
									<Table.HeaderCell>Required stars</Table.HeaderCell>
									<Table.HeaderCell>Honor cost</Table.HeaderCell>
								</Table.Row>
							</Table.Header>

							<Table.Body>
								{honordebt.totalStars.map((val, idx) => (
									<Table.Row key={idx}>
										<Table.Cell>
											<Header as='h4'>{CONFIG.RARITIES[idx + 1].name}</Header>
										</Table.Cell>
										<Table.Cell>
											{val - honordebt.ownedStars[idx]}{' '}
											<span>
												<i>
													({honordebt.ownedStars[idx]} / {val})
												</i>
											</span>
										</Table.Cell>
										<Table.Cell>{(val - honordebt.ownedStars[idx]) * CONFIG.CITATION_COST[idx]}</Table.Cell>
									</Table.Row>
								))}
							</Table.Body>

							<Table.Footer>
								<Table.Row>
									<Table.HeaderCell />
									<Table.HeaderCell>
										Owned {honordebt.ownedStars.reduce((a, b) => a + b, 0)} out of {honordebt.totalStars.reduce((a, b) => a + b, 0)}
									</Table.HeaderCell>
									<Table.HeaderCell>{totalHonorDebt}</Table.HeaderCell>
								</Table.Row>
							</Table.Footer>
						</Table>

						<Message info>
							<Message.Header>{readableHonorDebt}</Message.Header>
							<p>That's how long will it take you to max all remaining crew in the vault at 2000 honor / day</p>
						</Message>
					</div>
				)}

				<h3>Items required to level all owned crew</h3>
				<h5>Note: this may over-include already equipped items from previous level bands for certain crew</h5>
				<Message info>
					<Message.Header>Cost and faction recommendations</Message.Header>
					<p>
						Total chroniton cost to farm all these items: {totalChronCost}{' '}
						<span style={{ display: 'inline-block' }}>
							<img src={`${process.env.GATSBY_ASSETS_URL}atlas/energy_icon.png`} height={14} />
						</span>
					</p>
					{honordebt && (
						<p>
							Total number of credits required to craft all the recipes: {honordebt.craftCost}{' '}
							<span style={{ display: 'inline-block' }}>
								<img src={`${process.env.GATSBY_ASSETS_URL}atlas/soft_currency_icon.png`} height={14} />
							</span>
						</p>
					)}
				</Message>

				<h4>Factions with most needed non-mission items</h4>
				<ul>
					{factionRec.map((e) => (
						<li key={e.name}>
							{e.name}: {e.count} items
						</li>
					))}
				</ul>

				<div>
					<Checkbox
						label='Exclude already fulfilled'
						onChange={() => this.setState({ excludeFulfilled: !excludeFulfilled })}
						checked={this.state.excludeFulfilled}
					/>
					<Grid columns={3} centered padded>
						{demands.map((entry, idx) => (
							<Grid.Column key={idx}>
								<Popup
									trigger={
										<Header
											style={{ display: 'flex', cursor: 'zoom-in' }}
											icon={
												<ItemDisplay
													src={`${process.env.GATSBY_ASSETS_URL}${entry.equipment.imageUrl}`}
													size={48}
													maxRarity={entry.equipment.rarity}
													rarity={entry.equipment.rarity}
												/>
											}
											content={entry.equipment.name}
											subheader={`Need ${entry.count} ${entry.factionOnly ? ' (FACTION)' : ''} (have ${entry.have})`}
										/>
									}
									header={CONFIG.RARITIES[entry.equipment.rarity].name + ' ' + entry.equipment.name}
									content={<ItemSources item_sources={entry.equipment.item_sources} />}
									on='click'
									wide
								/>
							</Grid.Column>
						))}
					</Grid>
				</div>

				<h3>Skill coverage per rarity (yours vs. every crew in vault)</h3>
				<div>
					<div style={{ height: '320px', width: '50%', display: 'inline-block' }}>
						<ResponsiveRadar
							data={radar_skill_rarity_owned}
							theme={themes.dark}
							keys={['Common', 'Uncommon', 'Rare', 'Super Rare', 'Legendary']}
							indexBy='name'
							maxValue='auto'
							margin={{ top: 70, right: 80, bottom: 40, left: 80 }}
							curve='linearClosed'
							borderWidth={2}
							borderColor={{ from: 'color' }}
							gridLevels={5}
							gridShape='circular'
							gridLabelOffset={36}
							enableDots={true}
							dotSize={10}
							dotColor={{ theme: 'background' }}
							dotBorderWidth={2}
							dotBorderColor={{ from: 'color' }}
							enableDotLabel={true}
							dotLabel='value'
							dotLabelYOffset={-12}
							colors={{ scheme: 'nivo' }}
							fillOpacity={0.25}
							blendMode='multiply'
							animate={true}
							motionStiffness={90}
							motionDamping={15}
							isInteractive={true}
							legends={[
								{
									anchor: 'top-left',
									direction: 'column',
									translateX: -50,
									translateY: -40,
									itemWidth: 80,
									itemHeight: 20,
									itemTextColor: '#999',
									symbolSize: 12,
									symbolShape: 'circle',
									effects: [
										{
											on: 'hover',
											style: {
												itemTextColor: '#000',
											},
										},
									],
								},
							]}
						/>
					</div>
					<div style={{ height: '320px', width: '50%', display: 'inline-block' }}>
						<ResponsiveRadar
							data={radar_skill_rarity}
							theme={themes.dark}
							keys={['Common', 'Uncommon', 'Rare', 'Super Rare', 'Legendary']}
							indexBy='name'
							maxValue='auto'
							margin={{ top: 70, right: 80, bottom: 40, left: 80 }}
							curve='linearClosed'
							borderWidth={2}
							borderColor={{ from: 'color' }}
							gridLevels={5}
							gridShape='circular'
							gridLabelOffset={36}
							enableDots={true}
							dotSize={10}
							dotColor={{ theme: 'background' }}
							dotBorderWidth={2}
							dotBorderColor={{ from: 'color' }}
							enableDotLabel={true}
							dotLabel='value'
							dotLabelYOffset={-12}
							colors={{ scheme: 'nivo' }}
							fillOpacity={0.25}
							blendMode='multiply'
							animate={true}
							motionStiffness={90}
							motionDamping={15}
							isInteractive={true}
							legends={[
								{
									anchor: 'top-left',
									direction: 'column',
									translateX: -50,
									translateY: -40,
									itemWidth: 80,
									itemHeight: 20,
									itemTextColor: '#999',
									symbolSize: 12,
									symbolShape: 'circle',
									effects: [
										{
											on: 'hover',
											style: {
												itemTextColor: '#000',
											},
										},
									],
								},
							]}
						/>
					</div>
				</div>

				<h3>Number of stars (fused rarity) for your Super Rare and Legendary crew</h3>
				<div>
					<div style={{ height: '320px', width: '50%', display: 'inline-block' }}>
						<ResponsivePie
							data={r4_stars}
							theme={themes.dark}
							margin={{ top: 40, right: 80, bottom: 80, left: 80 }}
							innerRadius={0.2}
							padAngle={2}
							cornerRadius={2}
							borderWidth={1}
							animate={false}
							slicesLabelsTextColor='#333333'
							legends={[
								{
									anchor: 'bottom',
									direction: 'row',
									translateY: 56,
									itemWidth: 100,
									itemHeight: 18,
									itemTextColor: '#999',
									symbolSize: 18,
									symbolShape: 'circle',
									effects: [
										{
											on: 'hover',
											style: {
												itemTextColor: '#000',
											},
										},
									],
								},
							]}
						/>
					</div>
					<div style={{ height: '320px', width: '50%', display: 'inline-block' }}>
						<ResponsivePie
							data={r5_stars}
							theme={themes.dark}
							margin={{ top: 40, right: 80, bottom: 80, left: 80 }}
							innerRadius={0.2}
							padAngle={2}
							cornerRadius={2}
							borderWidth={1}
							animate={false}
							slicesLabelsTextColor='#333333'
							legends={[
								{
									anchor: 'bottom',
									direction: 'row',
									translateY: 56,
									itemWidth: 100,
									itemHeight: 18,
									itemTextColor: '#999',
									symbolSize: 18,
									symbolShape: 'circle',
									effects: [
										{
											on: 'hover',
											style: {
												itemTextColor: '#000',
											},
										},
									],
								},
							]}
						/>
					</div>
				</div>

				<h3>Skill distribution for owned crew (number of characters per skill combos Primary > Secondary)</h3>
				<Checkbox label='Include tertiary skill' onChange={() => this._onIncludeTertiary()} checked={this.state.includeTertiary} />
				<div>
					<div style={{ height: '420px', width: '50%', display: 'inline-block' }}>
						<ResponsiveSunburst
							data={skill_distribution}
							theme={themes.dark}
							margin={{ top: 40, right: 20, bottom: 20, left: 20 }}
							identity='name'
							value='loc'
							cornerRadius={2}
							borderWidth={1}
							borderColor='white'
							colors={{ scheme: 'nivo' }}
							childColor={{ from: 'color' }}
							animate={true}
							motionStiffness={90}
							motionDamping={15}
							isInteractive={true}
						/>
					</div>
					<div style={{ height: '420px', width: '50%', display: 'inline-block' }}>
						<ResponsiveBar
							data={flat_skill_distribution}
							keys={['Count']}
							theme={themes.dark}
							indexBy='name'
							layout='horizontal'
							margin={{ top: 50, right: 130, bottom: 50, left: 100 }}
							padding={0.3}
							axisBottom={{
								legend: 'Number of crew',
								legendPosition: 'middle',
								legendOffset: 32,
							}}
							labelSkipWidth={12}
							labelSkipHeight={12}
							legends={[
								{
									dataFrom: 'keys',
									anchor: 'bottom-right',
									direction: 'column',
									justify: false,
									translateX: 120,
									translateY: 0,
									itemsSpacing: 2,
									itemWidth: 100,
									itemHeight: 20,
									itemDirection: 'left-to-right',
									symbolSize: 20,
									effects: [
										{
											on: 'hover',
											style: {
												itemOpacity: 1,
											},
										},
									],
								},
							]}
							animate={false}
						/>
					</div>
				</div>
			</ErrorBoundary>
		);
	}
Example #26
Source File: extracrewdetails.tsx    From website with MIT License 4 votes vote down vote up
renderOptimalPolestars() {
		if (!this.state.constellation || !this.state.optimalpolestars) {
			return <span />;
		}

		const { optimalpolestars, constellation, pagination_rows, pagination_page } = this.state;

		let data = JSON.parse(JSON.stringify(optimalpolestars));

		let crewPolestars = constellation.keystones.concat(constellation.raritystone.concat(constellation.skillstones));
		data.forEach((optimal) => {
			optimal.combos = optimal.polestars.map((trait) => {
				const polestar = crewPolestars.find((op) => filterTraits(op, trait));
				// Catch when optimal combos include a polestar that isn't yet in DataCore's keystones list
				return polestar ?? {
					short_name: trait.substr(0, 1).toUpperCase()+trait.substr(1),
					icon: {
						file: '/items_keystones_'+trait+'.png'
					}
				};
			})
		});

		// Pagination
		let totalPages = Math.ceil(data.length / this.state.pagination_rows);
		data = data.slice(pagination_rows * (pagination_page - 1), pagination_rows * pagination_page);

		return (
			<Segment>
				<Header as='h4'>Optimal Polestars for Crew Retrieval</Header>
				<div>All the polestars that give the best chance of retrieving this crew</div>
				<Table celled selectable striped collapsing unstackable compact='very'>
					<Table.Header>
						<Table.Row>
							<Table.HeaderCell width={1}>Best Chance</Table.HeaderCell>
							<Table.HeaderCell width={3} textAlign='center'>Polestar Combination</Table.HeaderCell>
						</Table.Row>
					</Table.Header>
					<Table.Body>
						{data.map((optimal, idx) => (
							<Table.Row key={idx}>
								<Table.Cell>
									<div style={{ fontWeight: 'bolder', fontSize: '1.25em' }}>
										{(1/optimal.count*100).toFixed()}%
									</div>
									{optimal.count > 1 && (
									<div style={{ gridArea: 'description' }}>Shared with{' '}
										{optimal.alts.map((alt) => (
											<Link key={alt.symbol} to={`/crew/${alt.symbol}/`}>
												{alt.name}
											</Link>
										)).reduce((prev, curr) => [prev, ', ', curr])}
									</div>
									)}
								</Table.Cell>
								<Table.Cell>
									<Grid columns={4} centered padded>
									{optimal.combos.map((polestar, idx) => (
										<Grid.Column key={idx} textAlign='center' mobile={8} tablet={5} computer={4}>
											<img width={32} src={`${process.env.GATSBY_ASSETS_URL}${polestar.icon.file.substr(1).replace(/\//g, '_')}`} />
											<br />{polestar.short_name}
										</Grid.Column>
									))}
									</Grid>
								</Table.Cell>
							</Table.Row>
						))}
					</Table.Body>
					<Table.Footer>
						<Table.Row>
							<Table.HeaderCell colSpan="8">
								<Pagination
									totalPages={totalPages}
									activePage={pagination_page}
									onPageChange={(event, { activePage }) => this._onChangePage(activePage)}
								/>
								<span style={{ paddingLeft: '2em' }}>
									Rows per page:{' '}
									<Dropdown
										inline
										options={pagingOptions}
										value={pagination_rows}
										onChange={(event, { value }) =>
											this.setState({ pagination_page: 1, pagination_rows: value as number })
										}
									/>
								</span>
							</Table.HeaderCell>
						</Table.Row>
					</Table.Footer>
				</Table>
			</Segment>
		);
	}
Example #27
Source File: eventplanner.tsx    From website with MIT License 4 votes vote down vote up
EventCrewTable = (props: EventCrewTableProps) => {
	const { eventData, phaseIndex, buffConfig } = props;

	const [showBonus, setShowBonus] = useStateWithStorage('eventplanner/showBonus', true);
	const [applyBonus, setApplyBonus] = useStateWithStorage('eventplanner/applyBonus', true);
	const [showPotential, setShowPotential] = useStateWithStorage('eventplanner/showPotential', false);
	const [showFrozen, setShowFrozen] = useStateWithStorage('eventplanner/showFrozen', true);
	const [initOptions, setInitOptions] = React.useState({});

	const crewAnchor = React.useRef(null);

	React.useEffect(() => {
		setInitOptions({});
	}, [eventData, phaseIndex]);

	if (eventData.bonus.length == 0)
		return (
			<div style={{ marginTop: '1em' }}>
				Featured crew not yet identified for this event.
			</div>
		);

	const tableConfig: ITableConfigRow[] = [
		{ width: 3, column: 'name', title: 'Crew', pseudocolumns: ['name', 'max_rarity', 'level'] },
		{ width: 1, column: 'bonus', title: 'Bonus', reverse: true },
		{ width: 1, column: 'bestSkill.score', title: 'Best', reverse: true },
		{ width: 1, column: 'bestPair.score', title: 'Pair', reverse: true }
	];
	CONFIG.SKILLS_SHORT.forEach((skill) => {
		tableConfig.push({
			width: 1,
			column: `${skill.name}.core`,
			title: <img alt={CONFIG.SKILLS[skill.name]} src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${skill.name}.png`} style={{ height: '1.1em' }} />,
			reverse: true
		});
	});

	// Check for custom column (i.e. combo from crew matrix click)
	let customColumn = '';
	if (initOptions?.column && tableConfig.findIndex(col => col.column === initOptions.column) == -1) {
		customColumn = initOptions.column;
		const customSkills = customColumn.replace('combos.', '').split(',');
		tableConfig.push({
			width: 1,
			column: customColumn,
			title:
				<span>
					<img alt='Skill' src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${customSkills[0]}.png`} style={{ height: '1.1em' }} />
					+<img alt='Skill' src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${customSkills[1]}.png`} style={{ height: '1.1em' }} />
				</span>,
			reverse: true
		});
	}

	const phaseType = phaseIndex < eventData.content_types.length ? eventData.content_types[phaseIndex] : eventData.content_types[0];

	let bestCombos = {};

	const zeroCombos = {};
	for (let first = 0; first < CONFIG.SKILLS_SHORT.length; first++) {
		let firstSkill = CONFIG.SKILLS_SHORT[first];
		zeroCombos[firstSkill.name] = 0;
		for (let second = first+1; second < CONFIG.SKILLS_SHORT.length; second++) {
			let secondSkill = CONFIG.SKILLS_SHORT[second];
			zeroCombos[firstSkill.name+','+secondSkill.name] = 0;
		}
	}

	let myCrew = JSON.parse(JSON.stringify(props.crew));

	// Filter crew by bonus, frozen here instead of searchabletable callback so matrix can use filtered crew list
	if (showBonus) myCrew = myCrew.filter((c) => eventData.bonus.indexOf(c.symbol) >= 0);
	if (!showFrozen) myCrew = myCrew.filter((c) => c.immortal == 0);

	const getPairScore = (crew: any, primary: string, secondary: string) => {
		if (phaseType === 'shuttles') {
			if (secondary) return crew[primary].core+(crew[secondary].core/4);
			return crew[primary].core;
		}
		if (secondary) return (crew[primary].core+crew[secondary].core)/2;
		return crew[primary].core/2;
	};

	myCrew.forEach(crew => {
		// First adjust skill scores as necessary
		if (applyBonus || showPotential) {
			crew.bonus = 1;
			if (applyBonus && eventData.featured.indexOf(crew.symbol) >= 0) {
				if (phaseType == 'gather') crew.bonus = 10;
				else if (phaseType == 'shuttles') crew.bonus = 3;
			}
			else if (applyBonus && eventData.bonus.indexOf(crew.symbol) >= 0) {
				if (phaseType == 'gather') crew.bonus = 5;
				else if (phaseType == 'shuttles') crew.bonus = 2;
			}
			if (crew.bonus > 1 || showPotential) {
				CONFIG.SKILLS_SHORT.forEach(skill => {
					if (crew[skill.name].core > 0) {
						if (showPotential && crew.immortal === 0 && !crew.prospect) {
							crew[skill.name].current = crew[skill.name].core*crew.bonus;
							crew[skill.name] = applySkillBuff(buffConfig, skill.name, crew.skill_data[crew.rarity-1].base_skills[skill.name]);
						}
						crew[skill.name].core = crew[skill.name].core*crew.bonus;
					}
				});
			}
		}
		// Then calculate skill combination scores
		let combos = {...zeroCombos};
		let bestPair = { score: 0 };
		let bestSkill = { score: 0 };
		for (let first = 0; first < CONFIG.SKILLS_SHORT.length; first++) {
			const firstSkill = CONFIG.SKILLS_SHORT[first];
			const single = {
				score: crew[firstSkill.name].core,
				skillA: firstSkill.name
			};
			combos[firstSkill.name] = single.score;
			if (!bestCombos[firstSkill.name] || single.score > bestCombos[firstSkill.name].score)
				bestCombos[firstSkill.name] = { id: crew.id, score: single.score };
			if (single.score > bestSkill.score) bestSkill = { score: single.score, skill: single.skillA };
			for (let second = first+1; second < CONFIG.SKILLS_SHORT.length; second++) {
				const secondSkill = CONFIG.SKILLS_SHORT[second];
				let pair = {
					score: getPairScore(crew, firstSkill.name, secondSkill.name),
					skillA: firstSkill.name,
					skillB: secondSkill.name
				}
				if (crew[secondSkill.name].core > crew[firstSkill.name].core) {
					pair = {
						score: getPairScore(crew, secondSkill.name, firstSkill.name),
						skillA: secondSkill.name,
						skillB: firstSkill.name
					}
				}
				combos[firstSkill.name+','+secondSkill.name] = pair.score;
				if (pair.score > bestPair.score) bestPair = pair;
				const pairId = firstSkill.name+secondSkill.name;
				if (!bestCombos[pairId] || pair.score > bestCombos[pairId].score)
					bestCombos[pairId] = { id: crew.id, score: pair.score };
			}
		}
		crew.combos = combos;
		crew.bestPair = bestPair;
		crew.bestSkill = bestSkill;
	});

	return (
		<React.Fragment>
			<div ref={crewAnchor} />
			<Header as='h4'>Your Crew</Header>
			<div style={{ margin: '.5em 0' }}>
				<Form.Group grouped>
					<Form.Field
						control={Checkbox}
						label={`Only show event crew (${eventData.bonus_text.replace('Crew Bonus: ', '')})`}
						checked={showBonus}
						onChange={(e, { checked }) => setShowBonus(checked)}
					/>
					<Form.Field
						control={Checkbox}
						label='Apply event bonus to skills'
						checked={applyBonus}
						onChange={(e, { checked }) => setApplyBonus(checked)}
					/>
					<Form.Field
						control={Checkbox}
						label='Show potential skills of unleveled crew'
						checked={showPotential}
						onChange={(e, { checked }) => setShowPotential(checked)}
					/>
					<Form.Field
						control={Checkbox}
						label='Show frozen (vaulted) crew'
						checked={showFrozen}
						onChange={(e, { checked }) => setShowFrozen(checked)}
					/>
				</Form.Group>
			</div>
			<SearchableTable
				id='eventplanner'
				data={myCrew}
				config={tableConfig}
				renderTableRow={(crew, idx, highlighted) => renderTableRow(crew, idx, highlighted)}
				filterRow={(crew, filters, filterType) => showThisCrew(crew, filters, filterType)}
				initOptions={initOptions}
				showFilterOptions={true}
				lockable={props.lockable}
			/>
			{phaseType !== 'skirmish' && (<EventCrewMatrix crew={myCrew} bestCombos={bestCombos} phaseType={phaseType} handleClick={sortByCombo} />)}
		</React.Fragment>
	);

	function renderTableRow(crew: any, idx: number, highlighted: boolean): JSX.Element {
		const attributes = {
			positive: highlighted
		};

		return (
			<Table.Row key={idx} style={{ cursor: 'zoom-in' }} onClick={() => navigate(`/crew/${crew.symbol}/`)} {...attributes}>
				<Table.Cell>
					<div
						style={{
							display: 'grid',
							gridTemplateColumns: '60px auto',
							gridTemplateAreas: `'icon stats' 'icon description'`,
							gridGap: '1px'
						}}
					>
						<div style={{ gridArea: 'icon' }}>
							<img width={48} src={`${process.env.GATSBY_ASSETS_URL}${crew.imageUrlPortrait}`} />
						</div>
						<div style={{ gridArea: 'stats' }}>
							<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}><Link to={`/crew/${crew.symbol}/`}>{crew.name}</Link></span>
						</div>
						<div style={{ gridArea: 'description' }}>{descriptionLabel(crew)}</div>
					</div>
				</Table.Cell>
				<Table.Cell textAlign='center'>
					{crew.bonus > 1 ? `x${crew.bonus}` : ''}
				</Table.Cell>
				<Table.Cell textAlign='center'>
					<b>{scoreLabel(crew.bestSkill.score)}</b>
					<br /><img alt='Skill' src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${crew.bestSkill.skill}.png`} style={{ height: '1em' }} />
				</Table.Cell>
				<Table.Cell textAlign='center'>
					<b>{scoreLabel(crew.bestPair.score)}</b>
					<br /><img alt='Skill' src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${crew.bestPair.skillA}.png`} style={{ height: '1em' }} />
					{crew.bestPair.skillB != '' && (<span>+<img alt='Skill' src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${crew.bestPair.skillB}.png`} style={{ height: '1em' }} /></span>)}
				</Table.Cell>
				{CONFIG.SKILLS_SHORT.map(skill =>
					crew.base_skills[skill.name] ? (
						<Table.Cell key={skill.name} textAlign='center'>
							<b>{scoreLabel(crew[skill.name].core)}</b>
							{phaseType != 'gather' && (<span><br /><small>+({crew[skill.name].min}-{crew[skill.name].max})</small></span>)}
						</Table.Cell>
					) : (
						<Table.Cell key={skill.name} />
					)
				)}
				{customColumn != '' && (
						<Table.Cell key='custom' textAlign='center'>
							<b>{scoreLabel(customColumn.split('.').reduce((prev, curr) => prev.hasOwnProperty(curr) ? prev[curr] : undefined, crew))}</b>
						</Table.Cell>
					)
				}
			</Table.Row>
		);
	}

	function descriptionLabel(crew: any): JSX.Element {
		return (
			<div>
				<div><Rating icon='star' rating={crew.rarity} maxRating={crew.max_rarity} size='large' disabled /></div>
				<div>
					{crew.favorite && <Icon name='heart' />}
					{crew.immortal > 0 && <Icon name='snowflake' />}
					{crew.prospect && <Icon name='add user' />}
					<span>{crew.immortal ? (`${crew.immortal} frozen`) : (`Level ${crew.level}`)}</span>
				</div>
			</div>
		);
	}

	function scoreLabel(score: number): JSX.Element {
		if (!score || score === 0) return (<></>);
		if (phaseType === 'gather') return (<>{`${calculateGalaxyChance(score)}%`}</>);
		return (<>{Math.floor(score)}</>);
	}

	function showThisCrew(crew: any, filters: [], filterType: string): boolean {
		// Bonus, frozen crew filtering now handled before rendering entire table instead of each row
		return crewMatchesSearchFilter(crew, filters, filterType);
	}

	function sortByCombo(skillA: string, skillB: string): void {
		if (skillA === skillB) {
			setInitOptions({
				column: `${skillA}.core`,
				direction: 'descending'
			});
		}
		else {
			// Order of combo match order of skills in CONFIG
			const customSkills = [];
			CONFIG.SKILLS_SHORT.forEach((skill) => {
				if (skillA === skill.name || skillB === skill.name)
					customSkills.push(skill.name);
			});
			setInitOptions({
				column: `combos.${customSkills[0]},${customSkills[1]}`,
				direction: 'descending'
			});
		}
		if (!crewAnchor.current) return;
		crewAnchor.current.scrollIntoView({
			behavior: 'smooth'
		}, 500);
	}
}
Example #28
Source File: crewretrieval.tsx    From website with MIT License 4 votes vote down vote up
CrewTable = (props: CrewTableProps) => {
	const { data, polestars } = props;

	const [activeCrew, setActiveCrew] = React.useState(null);
	const [activeCollections, setActiveCollections] = React.useState(null);

	if (!data) return (<></>);

	const tableConfig: ITableConfigRow[] = [
		{ width: 3, column: 'name', title: 'Crew' },
		{ width: 1, column: 'max_rarity', title: 'Rarity', reverse: true, tiebreakers: ['highest_owned_rarity'] },
		{ width: 1, column: 'bigbook_tier', title: 'Tier' },
		{ width: 1, column: 'cab_ov', title: 'CAB', reverse: true, tiebreakers: ['cab_ov_rank'] },
		{ width: 1, column: 'ranks.voyRank', title: 'Voyage' },
		{ width: 1, column: 'collections.length', title: 'Collections', reverse: true },
		{ width: 1, title: 'Useable Combos' }
	];

	return (
		<SearchableTable
			id={"crewretrieval"}
			data={data}
			config={tableConfig}
			renderTableRow={(crew, idx) => renderTableRow(crew, idx)}
			filterRow={(crew, filters, filterType) => crewMatchesSearchFilter(crew, filters, filterType)}
			showFilterOptions={true}
		/>
	);

	function renderTableRow(crew: any, idx: number): JSX.Element {
		return (
			<Table.Row key={idx}>
				<Table.Cell>
					<div
						style={{
							display: 'grid',
							gridTemplateColumns: '60px auto',
							gridTemplateAreas: `'icon stats' 'icon description'`,
							gridGap: '1px'
						}}
					>
						<div style={{ gridArea: 'icon' }}>
							<img width={48} src={`${process.env.GATSBY_ASSETS_URL}${crew.imageUrlPortrait}`} />
						</div>
						<div style={{ gridArea: 'stats' }}>
							<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}><Link to={`/crew/${crew.symbol}/`}>{crew.name}</Link></span>
						</div>
						<div style={{ gridArea: 'description' }}>{getCoolStats(crew, false, false)}</div>
					</div>
				</Table.Cell>
				<Table.Cell>
					<Rating icon='star' rating={crew.highest_owned_rarity} maxRating={crew.max_rarity} size="large" disabled />
				</Table.Cell>
				<Table.Cell textAlign="center" style={{ display: activeCrew === crew.symbol ? 'none' : 'table-cell' }}>
					<b>{formatTierLabel(crew.bigbook_tier)}</b>
				</Table.Cell>
				<Table.Cell textAlign="center" style={{ display: activeCrew === crew.symbol ? 'none' : 'table-cell' }}>
					<b>{crew.cab_ov}</b><br />
					<small>{rarityLabels[parseInt(crew.max_rarity)-1]} #{crew.cab_ov_rank}</small>
				</Table.Cell>
				<Table.Cell textAlign="center" style={{ display: activeCrew === crew.symbol ? 'none' : 'table-cell' }}>
					<b>#{crew.ranks.voyRank}</b><br />
					{crew.ranks.voyTriplet && <small>Triplet #{crew.ranks.voyTriplet.rank}</small>}
				</Table.Cell>
				<Table.Cell textAlign="center"
					style={{ cursor: activeCollections === crew.symbol ? 'zoom-out' : 'zoom-in', display: activeCrew === crew.symbol ? 'none' : 'table-cell' }}
					onClick={() => { setActiveCollections(activeCollections === crew.symbol ? null : crew.symbol) }}
				>
					{showCollectionsForCrew(crew)}
				</Table.Cell>
				<Table.Cell textAlign="center" style={{ display: activeCrew === crew.symbol ? 'table-cell' : 'none' }}
					colSpan={activeCrew === crew.symbol ? 4 : undefined}
				>
					{showCombosForCrew(crew)}
				</Table.Cell>
				<Table.Cell textAlign="center" style={{ cursor: activeCrew === crew.symbol ? 'zoom-out' : 'zoom-in' }}
					onClick={(e) => { setActiveCrew(activeCrew === crew.symbol ? null : crew.symbol); e.stopPropagation(); }}
				>
					{activeCrew === crew.symbol ? 'Hide' : 'View'}
				</Table.Cell>
			</Table.Row>
		);
	}

	function showCombosForCrew(crew: any): JSX.Element {
		if (activeCrew !== crew.symbol) return (<></>);

		let combos = crew.unique_polestar_combos?.filter(
			(upc) => upc.every(
				(trait) => polestars.some(op => filterTraits(op, trait))
			)
		).map((upc) => upc.map((trait) => polestars.find((op) => filterTraits(op, trait))));

		// Exit here if activecrew has 0 combos after changing filters
		if (combos.length == 0) return (<></>);

		let fuseGroups = groupByFuses(combos, 0, []);

		return (<ComboGrid crew={crew} combos={combos} fuseGroups={fuseGroups} />);
	}

	function groupByFuses(combos: any[], start: number, group: number[]): any {
		const fuseGroups = {};
		const consumed = {};
		group.forEach((comboId) => {
			combos[comboId].forEach((polestar) => {
				if (consumed[polestar.symbol])
					consumed[polestar.symbol]++;
				else
					consumed[polestar.symbol] = 1;
			});
		});
		combos.forEach((combo, comboId) => {
			if (comboId >= start) {
				let consumable = 0;
				combo.forEach((polestar) => {
					if (!consumed[polestar.symbol] || polestar.quantity-consumed[polestar.symbol] >= 1)
						consumable++;
				});
				if (consumable == combo.length) {
					const parentGroup = [...group, comboId];
					const parentId = 'x'+parentGroup.length;
					if (fuseGroups[parentId])
						fuseGroups[parentId].push(parentGroup);
					else
						fuseGroups[parentId] = [parentGroup];
					// Only collect combo groups up to 5 fuses
					if (parentGroup.length < 5) {
						let childGroups = groupByFuses(combos, comboId, parentGroup);
						for (let childId in childGroups) {
							if (fuseGroups[childId])
								fuseGroups[childId] = fuseGroups[childId].concat(childGroups[childId]);
							else
								fuseGroups[childId] = childGroups[childId];
						}
					}
				}
			}
		});
		return fuseGroups;
	}

	function showCollectionsForCrew(crew: any): JSX.Element {
		if (activeCollections !== crew.symbol || crew.collections.length == 0)
			return (<b>{crew.collections.length}</b>);

		const formattedCollections = crew.collections.map((c, idx) => (
			<span key={idx}>{c}{idx < crew.collections.length-1 ? ',' : ''}</span>
		)).reduce((prev, curr) => [prev, ' ', curr]);

		return (
			<div>
				{formattedCollections}
			</div>
		);
	}
}
Example #29
Source File: crewretrieval.tsx    From website with MIT License 4 votes vote down vote up
PolestarProspectModal = (props: PolestarProspectModalProps) => {
	const { ownedPolestars, allCrew, updateProspects } = props;

	const [addedPolestars, setAddedPolestars] = React.useState(props.addedPolestars);

	const [modalIsOpen, setModalIsOpen] = React.useState(false);
	const [activeCrew, setActiveCrew] = React.useState('');
	const [activeConstellation, setActiveConstellation] = React.useState('');
	const [activePolestar, setActivePolestar] = React.useState('');

	const [allKeystones, setAllKeystones] = React.useState(undefined);
	const [control, setControl] = React.useState([]);
	const [crewCrates, setCrewCrates] = React.useState(0);
	const [ownedConstellations, setOwnedConstellations] = React.useState([]);

	React.useEffect(() => {
		if (allKeystones) {
			// Chances assume you can't get rarity, skill constellations from scans
			setCrewCrates(allKeystones.filter(k => k.type == 'crew_keystone_crate').length);
			const owned = allKeystones.filter(k => (k.type == 'crew_keystone_crate' || k.type == 'keystone_crate') && k.quantity > 0)
				.sort((a, b) => a.name.localeCompare(b.name));
			setOwnedConstellations([...owned]);
		}
	}, [allKeystones]);

	// Recalculate combos only when modal gets closed
	React.useEffect(() => {
		if (!modalIsOpen) {
			updateProspects([...addedPolestars]);
		}
	}, [modalIsOpen]);

	return (
		<Modal
			open={modalIsOpen}
			onClose={() => setModalIsOpen(false)}
			onOpen={() => setModalIsOpen(true)}
			trigger={<Button><Icon name='add' />{addedPolestars.length}</Button>}
			size='large'
		>
			<Modal.Header>Add Prospective Polestars</Modal.Header>
			<Modal.Content scrolling>
				{renderContent()}
			</Modal.Content>
			<Modal.Actions>
				{activePolestar != '' && (<Button icon='backward' content='Return to polestars' onClick={() => setActivePolestar('')} />)}
				<Button positive onClick={() => setModalIsOpen(false)}>Close</Button>
			</Modal.Actions>
		</Modal>
	);

	function renderContent(): JSX.Element {
		if (!modalIsOpen) return (<></>);

		if (!allKeystones) {
			calculateKeystoneOdds();
			calculateControl();
			return (<></>);
		}

		if (activePolestar != '')
			return renderPolestarDetail();

		return renderPolestarFinder();
	}

	function calculateKeystoneOdds(): void {
		const allkeystones = JSON.parse(JSON.stringify(props.allKeystones));
		let totalCrates = 0, totalDrops = 0;
		allkeystones.forEach(keystone => {
			if (keystone.type == 'crew_keystone_crate') {
				totalCrates++;
				totalDrops += keystone.keystones.length;
			}
		});
		allkeystones.filter(k => k.type == 'keystone').forEach(polestar => {
			const crates = allkeystones.filter(k => (k.type == 'crew_keystone_crate' || k.type == 'keystone_crate') && k.keystones.includes(polestar.id));
			const nochance = polestar.filter.type == 'rarity' || polestar.filter.type == 'skill' || crates.length == 0;
			polestar.crate_count = nochance ? 0 : crates.length;
			//polestar.scan_odds = nochance ? 0 : crates.length/totalDrops; // equal chance of dropping
			polestar.scan_odds = nochance ? 0 : crates.reduce((prev, curr) => prev + (1/curr.keystones.length), 0)/totalCrates; // conditional probability
			const owned = crates.filter(k => k.quantity > 0);
			polestar.owned_crate_count = owned.reduce((prev, curr) => prev + curr.quantity, 0);
			polestar.owned_best_odds = owned.length == 0 ? 0 : 1/owned.reduce((prev, curr) => Math.min(prev, curr.keystones.length), 100);
			polestar.owned_total_odds = owned.length == 0 ? 0 : 1-owned.reduce((prev, curr) => prev*(((curr.keystones.length-1)/curr.keystones.length)**curr.quantity), 1);
			if (polestar.filter.type === 'rarity')
				polestar.crew_count = allCrew.filter(c => c.in_portal && c.max_rarity == polestar.filter.rarity).length;
			else if (polestar.filter.type === 'skill')
				polestar.crew_count = allCrew.filter(c => c.in_portal && c.base_skills[polestar.filter.skill]).length;
			else if (polestar.filter.type === 'trait')
				polestar.crew_count = allCrew.filter(c => c.in_portal && c.traits.some(trait => trait === polestar.filter.trait)).length;
		});
		setAllKeystones([...allkeystones]);
	}

	function calculateControl(): void {
		// Control is a list of crew that you can't retrieve, which includes crew not in portal
		const retrievable = getRetrievable(allCrew, ownedPolestars);
		const unretrievable = allCrew.filter(pc => !retrievable.some(cc => cc === pc));
		setControl([...unretrievable]);
	}

	function renderPolestarFinder(): JSX.Element {
		const polestarTable: ITableConfigRow[] = [
			{ width: 2, column: 'name', title: 'Polestar' },
			{ width: 1, column: 'crew_count', title: 'Crew in Portal', reverse: true },
			{ width: 1, column: 'crate_count', title: 'Constellation Chance', reverse: true },
			{ width: 1, column: 'scan_odds', title: 'Scan Chance', reverse: true },
			{ width: 1, column: 'owned_best_odds', title: 'Best Chance', reverse: true },
			{ width: 1, column: 'quantity', title: 'Owned', reverse: true },
			{ width: 1, column: 'loaned', title: 'Added', reverse: true }
		];

		const constellationList = ownedConstellations.map(c => {
				return { key: c.symbol, value: c.symbol, text: c.name };
			});

		// !! Always filter polestars by crew_count to hide deprecated polestars !!
		let data = allKeystones.filter(k => k.type == 'keystone' && k.crew_count > 0);
		if (activeCrew != '') {
			const crew = allCrew.find(c => c.symbol === activeCrew);
			data = data.filter(k => (k.filter.type == 'trait' && crew.traits.includes(k.filter.trait))
				|| (k.filter.type == 'rarity' && k.filter.rarity == crew.max_rarity)
				|| (k.filter.type == 'skill' && k.filter.skill in crew.base_skills));
		}
		if (activeConstellation != '') {
			const crewKeystones = allKeystones.find(k => k.symbol === activeConstellation).keystones;
			data = data.filter(k => crewKeystones.includes(k.id));
		}
		data.forEach(p => {
			p.loaned = addedPolestars.filter(added => added === p.symbol).length;
		});

		return (
			<React.Fragment>
				<CrewPicker crew={control} value={activeCrew} updateCrew={updateCrew} />
				{activeCrew == '' && constellationList.length > 0 && (
					<React.Fragment>
						<span style={{ margin: '0 1em' }}>or</span>
						<Dropdown
							placeholder='Filter polestars by owned constellation'
							style={{ minWidth: '20em' }}
							selection
							clearable
							options={constellationList}
							value={activeConstellation}
							onChange={(e, { value }) => setAsActive('constellation', value) }
						/>
					</React.Fragment>
				)}
				{renderCrewMessage(data)}
				{renderConstellationMessage(data)}
				<div style={{ marginTop: '1em' }}>
					<SearchableTable
						data={data}
						config={polestarTable}
						renderTableRow={(polestar, idx) => renderPolestarRow(polestar, idx)}
						filterRow={(polestar, filter) => filterText(polestar, filter)}
						explanation={
							<div>
								<p>Search for polestars by name.</p>
							</div>
						}
					/>
					<p>
						<i>Constellation Chance</i>: your chance of acquiring any constellation with the polestar from a successful scan.
						<br /><i>Scan Chance</i>: your overall chance of acquiring the polestar from a successful scan.
						<br /><i>Best Chance</i>: your best chance of acquiring the polestar from a constellation in your inventory.
					</p>
				</div>
			</React.Fragment>
		);
	}

	function filterText(polestar: any, filters: []): boolean {
		if (filters.length == 0) return true;

		const matchesFilter = (input: string, searchString: string) =>
			input.toLowerCase().indexOf(searchString.toLowerCase()) >= 0;

		let meetsAnyCondition = false;

		for (let filter of filters) {
			let meetsAllConditions = true;
			if (filter.conditionArray.length === 0) {
				// text search only
				for (let segment of filter.textSegments) {
					let segmentResult = matchesFilter(polestar.name, segment.text);
					meetsAllConditions = meetsAllConditions && (segment.negated ? !segmentResult : segmentResult);
				}
			}
			if (meetsAllConditions) {
				meetsAnyCondition = true;
				break;
			}
		}

		return meetsAnyCondition;
	}

	function renderPolestarRow(polestar: any, idx: number): JSX.Element {
		return (
			<Table.Row key={polestar.symbol}
				style={{ cursor: activePolestar != polestar.symbol ? 'zoom-in' : 'zoom-out' }}
				onClick={() => setActivePolestar(activePolestar != polestar.symbol ? polestar.symbol : '')}
			>
				<Table.Cell>
					<div
						style={{
							display: 'grid',
							gridTemplateColumns: '30px auto',
							gridTemplateAreas: `'icon stats'`,
							gridGap: '1px'
						}}
					>
						<div style={{ gridArea: 'icon' }}>
							<img width={24} src={`${process.env.GATSBY_ASSETS_URL}${polestar.icon.file.substr(1).replace(/\//g, '_')}`} />
						</div>
						<div style={{ gridArea: 'stats' }}>
							<span style={{ fontWeight: 'bolder', fontSize: '1.1em' }}>{polestar.short_name}</span>
						</div>
					</div>
				</Table.Cell>
				<Table.Cell textAlign='center'>{polestar.crew_count}</Table.Cell>
				<Table.Cell textAlign='center'>{(polestar.crate_count/crewCrates*100).toFixed(1)}%</Table.Cell>
				<Table.Cell textAlign='center'>{(polestar.scan_odds*100).toFixed(2)}%</Table.Cell>
				<Table.Cell textAlign='center'>{(polestar.owned_best_odds*100).toFixed(1)}%</Table.Cell>
				<Table.Cell textAlign='center'>{polestar.quantity}</Table.Cell>
				<Table.Cell textAlign='center'>
					<ProspectInventory polestar={polestar.symbol} loaned={polestar.loaned} updateProspect={updateProspect} />
				</Table.Cell>
			</Table.Row>
		);
	}

	function renderCrewMessage(data: any[]): JSX.Element {
		if (activeCrew == '') return (<></>);

		const crew = allCrew.find(c => c.symbol === activeCrew);

		if (!crew.in_portal)
			return (<Message>{crew.name} is not available by crew retrieval.</Message>);

		if (crew.unique_polestar_combos?.length == 0)
			return (<Message>{crew.name} has no guaranteed retrieval options.</Message>);

		const unownedPolestars = data.filter(p => p.quantity === 0);
		if (unownedPolestars.length == 0)
			return (<Message>You can already retrieve {crew.name} with the polestars in your inventory.</Message>);

		crew.unique_polestar_combos.forEach(upc => {
			const needs = unownedPolestars.filter(p => upc.some(trait => filterTraits(p, trait)));
			needs.forEach(p => {
				p.useful = p.useful ? p.useful + 1: 1;
				if (needs.length == 1) p.useful_alone = true;
			});
		});

		// "Useful" polestars are all unowned polestars that unlock retrievals by themselves (i.e. `useful_alone`)
		//	or other unowned polestars that together unlock retrievals WITHOUT also relying on a `useful_alone` polestar
		const usefulPolestars = unownedPolestars.filter(p => p.useful_alone ||
			crew.unique_polestar_combos.filter(upc => !upc.some(trait =>
				unownedPolestars.filter(p => p.useful_alone).some(p => filterTraits(p, trait))
			)).some(upc => upc.some(trait => filterTraits(p, trait))))
			.sort((a, b) => b.useful - a.useful);

		const showUsefulPolestars = () => {
			if (usefulPolestars.length == 0)
				return (<p>No unowned polestars will help you retrieve {crew.name}.</p>); // What case is this?

			const usefulAlone = usefulPolestars.filter(p => p.useful_alone);
			const usefulWithOthers = usefulPolestars.filter(p => !p.useful_alone); // Should either be 0 or 2+

			return (
				<p>
					{usefulAlone.length > 0 && (<span>You need exactly one of the following polestars to retrieve {crew.name}: {renderPolestarsInline(usefulAlone)}</span>)}
					{usefulAlone.length > 0 && usefulWithOthers.length > 0 && (<span><br />Or some combination of the following polestars: {renderPolestarsInline(usefulWithOthers)}</span>)}
					{usefulAlone.length == 0 && (<span>You need some combination of the following polestars to retrieve {crew.name}: {renderPolestarsInline(usefulWithOthers)}</span>)}
				</p>
			);
		};

		// "Usable" constellations are owned constellations that include useful polestars
		const showUsableConstellations = () => {
			const usablePolestars = usefulPolestars.filter(p => p.owned_crate_count > 0);
			if (usablePolestars.length == 0) return (<></>);

			const constellations = ownedConstellations.filter(k => k.keystones.some(kId => usablePolestars.find(p => p.id === kId)));
			if (constellations.length == 1)
				return constellations.map(k => renderPolestarsFromConstellation(k, usablePolestars.filter(p => k.keystones.some(kId => kId === p.id))));

			return usablePolestars.sort((a, b) => {
					if (b.owned_total_odds == a.owned_total_odds)
						return b.owned_best_odds - a.owned_best_odds;
					return b.owned_total_odds - a.owned_total_odds;
				}).map(p =>
					renderConstellationsWithPolestar(p)
				);
		};

		return (
			<Message>
				{showUsefulPolestars()}
				{showUsableConstellations()}
			</Message>
		);
	}

	function renderPolestarsInline(polestars: any[]): JSX.Element[] {
		return polestars.map((p, pdx) => (
				<span key={pdx} onClick={() => setActivePolestar(p.symbol) }>
					<b>{p.short_name}</b>{pdx < polestars.length-1 ? ',' : ''}
				</span>
				))
			.reduce((prev, curr) => [prev, ' ', curr]);
	}

	function renderConstellationMessage(data: any[]): JSX.Element {
		if (activeConstellation == '') return (<></>);

		const constellation = allKeystones.find(k => k.symbol === activeConstellation);

		const unownedPolestars = data.filter(p => p.quantity === 0);

		if (unownedPolestars.length == 0)
			return (<Message>You already own all polestars in the {constellation.name}.</Message>);

		return (
			<Message>
				{renderPolestarsFromConstellation(constellation, unownedPolestars)}
			</Message>
		);
	}

	function renderPolestarDetail(): JSX.Element {
		const polestar = allKeystones.find(k => k.symbol === activePolestar);
		polestar.loaned = addedPolestars.filter(added => added === polestar.symbol).length;

		return (
			<div style={{ marginTop: '1em' }}>
				<Table celled striped unstackable compact="very">
					<Table.Header>
						<Table.Row>
							<Table.HeaderCell>Polestar</Table.HeaderCell>
							<Table.HeaderCell textAlign='center'>Crew in Portal</Table.HeaderCell>
							<Table.HeaderCell textAlign='center'>Constellation Chance</Table.HeaderCell>
							<Table.HeaderCell textAlign='center'>Scan Chance</Table.HeaderCell>
							<Table.HeaderCell textAlign='center'>Best Chance</Table.HeaderCell>
							<Table.HeaderCell textAlign='center'>Owned</Table.HeaderCell>
							<Table.HeaderCell textAlign='center'>Added</Table.HeaderCell>
						</Table.Row>
					</Table.Header>
					<Table.Body>
						{renderPolestarRow(polestar, 1)}
					</Table.Body>
				</Table>
				{polestar.owned_crate_count > 0 && (<Message>{renderConstellationsWithPolestar(polestar)}</Message>)}
				{renderNewRetrievals(polestar)}
			</div>
		);
	}

	function renderPolestarsFromConstellation(constellation: any, polestars: any[]): JSX.Element {
		const clarify = activeCrew != '' ? 'a needed' : 'an unowned';

		return (
			<div key={constellation.symbol}>
				Open the <b><span onClick={() => setAsActive('constellation', constellation.symbol) }>{constellation.name}</span></b>{` `}
				for a <b>{(polestars.length/constellation.keystones.length*100).toFixed(1)}%</b> chance of acquiring {clarify} polestar:{` `}
				<Grid centered padded stackable>
				{
					polestars.map((p, pdx) => (
						<Grid.Column key={pdx} width={2} textAlign='center' onClick={() => setActivePolestar(p.symbol)}>
							<img width={32} src={`${process.env.GATSBY_ASSETS_URL}${p.icon.file.substr(1).replace(/\//g, '_')}`} />
							<br /><b>{p.short_name}</b><br /><small>({(1/constellation.keystones.length*100).toFixed(1)}%)</small>
						</Grid.Column>
					))
				}
				</Grid>
				{activeCrew == '' && constellation.quantity > 1 && (<p>You own {constellation.quantity} of this constellation.</p>)}
			</div>
		);
	}

	function renderConstellationsWithPolestar(polestar: any): JSX.Element {
		const constellations = [];
		ownedConstellations.filter(k => k.keystones.includes(polestar.id))
			.forEach(k => {
				for (let i = 0; i < k.quantity; i++) {
					const newName = k.quantity > 1 ? k.name + " #"+(i+1) : k.name;
					constellations.push({...k, name: newName});
				}
			});

		return (
			<p key={polestar.symbol}>
				Open{` `}
				{
					constellations.sort((a, b) => 1/b.keystones.length - 1/a.keystones.length).map((k, kdx) => (
						<span key={kdx} onClick={() => setAsActive('constellation', k.symbol) }>
							<b>{k.name}</b> ({(1/k.keystones.length*100).toFixed(1)}%){kdx < constellations.length-1 ? ' or ' : ''}
						</span>
					)).reduce((prev, curr) => [prev, ' ', curr])
				}{` `}
				for a chance of acquiring the <b><span onClick={() => setActivePolestar(polestar.symbol)}>{polestar.name}</span></b>
				{constellations.length > 1 && (<span>; open all for a <b>{(polestar.owned_total_odds*100).toFixed(1)}%</b> chance</span>)}
			</p>
		);
	}

	function renderNewRetrievals(polestar: any): JSX.Element {
		const ownedPlus = JSON.parse(JSON.stringify(ownedPolestars));
		ownedPlus.push({...polestar, quantity: 1});
		const newRetrievables = getRetrievable(control, ownedPlus).filter(c => c.in_portal);

		if (newRetrievables.length == 0)
			return (
				<p>
					{polestar.quantity > 0 ? `You own ${polestar.quantity} of the ${polestar.name}. ` : ''}
					Acquiring{polestar.quantity > 0 ? ` more of ` : ` `}this polestar will not unlock guaranteed retrievals for any new crew.
				</p>
			);

		return (
			<React.Fragment>
				<p>Acquire the <b>{polestar.name}</b> to unlock guaranteed retrievals for the following crew:</p>
				<Grid centered padded stackable>
					{newRetrievables.sort((a, b) => a.name.localeCompare(b.name)).map((crew, cdx) => (
						<Grid.Column key={crew.symbol} width={2} textAlign='center' onClick={() => setAsActive('crew', crew.symbol) }>
							<ItemDisplay
								src={`${process.env.GATSBY_ASSETS_URL}${crew.imageUrlPortrait}`}
								size={64}
								maxRarity={crew.max_rarity}
								rarity={crew.highest_owned_rarity}
							/>
							<div>{crew.name}</div>
						</Grid.Column>
					))}
				</Grid>
			</React.Fragment>
		);
	}

	function getRetrievable(crewpool: any[], polestars: any[]): any[] {
		return crewpool.filter(crew =>
			crew.unique_polestar_combos?.some(upc =>
				upc.every(trait => polestars.some(op => filterTraits(op, trait)))
			));
	}

	function setAsActive(activeType: string, activeValue: string): void {
		setActiveCrew(activeType == 'crew' ? activeValue : '');
		setActiveConstellation(activeType == 'constellation' ? activeValue : '');
		setActivePolestar('');
	}

	function updateCrew(symbol: string): void {
		setAsActive('crew', symbol);
	}

	function updateProspect(polestar: string, increase: boolean): void {
		if (polestar == '') return;
		if (increase) {
			addedPolestars.push(polestar);
		}
		else {
			const prospectNum = addedPolestars.indexOf(polestar);
			if (prospectNum >= 0) addedPolestars.splice(prospectNum, 1);
		}
	}
}