semantic-ui-react#Form TypeScript Examples

The following examples show how to use semantic-ui-react#Form. 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: SendTextAccordion.tsx    From FLECT_Amazon_Chime_Meeting with Apache License 2.0 6 votes vote down vote up
////////////////////////////////
  /// UI
  ///////////////////////////////
  generateAccordion = () =>{
    const props = this.props as any
    const appState = props.appState as AppState

    const grid = (
        <Accordion>
        <Accordion.Title
            active={this.state.open}
            index={0}
            onClick={()=>{this.handleClick()}}
        >
            <Icon name='dropdown' />
            SendText
        </Accordion.Title>
        <Accordion.Content active={this.state.open}>
          <Form onSubmit={(e)=>{props.sendText( appState.focusedMeeting, appState.joinedMeetings[appState.focusedMeeting].focusAttendeeId, this.state.message); this.setState({message: ""})}}>
            <Form.Input  placeholder='message' value={this.state.message} onChange={(e)=>{this.setState({message: e.target.value})}} />
          </Form>
        </Accordion.Content>
        </Accordion>
    )
    return grid
  }
Example #2
Source File: index.tsx    From frontegg-react with MIT License 6 votes vote down vote up
render() {
    const { children, inForm } = this.props;
    const buttonProps = mapper(this.props);
    let ButtonComponent: any = SemanticButton;
    if (inForm) {
      ButtonComponent = Form.Button;
    }

    return <ButtonComponent {...buttonProps}>{children}</ButtonComponent>;
  }
Example #3
Source File: Entrance.tsx    From FLECT_Amazon_Chime_Meeting with Apache License 2.0 6 votes vote down vote up
render() {
        return (
          <div style={{ width: "60%", height: "100%",  margin: "auto"}}>
            <Form>
              <Form.Field>
                <label>User Name</label>
                <input placeholder='name' ref={this.userNameRef}/>
              </Form.Field>
              <Form.Field>
                <label>Code</label>
                <input placeholder='code' ref={this.codeRef}/>
              </Form.Field>
              <Button type='submit' onClick={()=>this.login()}>Submit</Button>
            </Form>
          </div>
        )
    }
Example #4
Source File: LocalSettings.tsx    From watchparty with MIT License 6 votes vote down vote up
SettingsModal = ({ trigger }: any) => (
  <Modal trigger={trigger} basic closeIcon size="small">
    <Header icon="setting" content="Settings" />
    <Modal.Content>
      <Form>
        <TextArea rows={10} id="settings_textarea">
          {window.localStorage.getItem('watchparty-setting') ||
            JSON.stringify(getDefaultSettings(), null, 2)}
        </TextArea>
      </Form>
    </Modal.Content>
    <Modal.Actions>
      <Button
        color="green"
        inverted
        onClick={() => {
          const newSetting = (document.getElementById(
            'settings_textarea'
          ) as HTMLTextAreaElement)!.value;
          try {
            validateSettingsString(newSetting);
            updateSettings(newSetting);
            window.location.reload();
          } catch (e) {
            alert(e);
          }
        }}
      >
        <Icon name="checkmark" />
        Save
      </Button>
    </Modal.Actions>
  </Modal>
)
Example #5
Source File: Comments.tsx    From communitymap-ui with Apache License 2.0 6 votes vote down vote up
PostCommentWidget: React.FC<{
  onComment: (comment: string) => Promise<any>;
}> = ({ onComment }) => {
  const [comment, setComment] = useState<string | null>(null);
  return (
    <div
      onClick={(e) => {
        // when put into map item click in form input causes the item to expand
        e.stopPropagation();
      }}
    >
      <Form
        onSubmit={(e) => {
          e.preventDefault();
          !!comment &&
            onComment(comment)
              .then(() => setComment(null))
              .catch(reportError);
        }}
      >
        <Form.Input
          value={comment || ''}
          placeholder="Your comment here"
          action={<Button icon="send" />}
          onChange={(e) => setComment(e.target.value)}
        />
      </Form>
    </div>
  );
}
Example #6
Source File: DepositForm.tsx    From multi-sig-wallet with MIT License 5 votes vote down vote up
DepositForm: React.FC<Props> = () => {
  const {
    state: { web3, account },
  } = useWeb3Context();

  const [input, setInput] = useState("");
  const { pending, call } = useAsync<DepositParams, void>(
    ({ web3, account, value }) => deposit(web3, account, { value })
  );

  function onChange(e: React.ChangeEvent<HTMLInputElement>) {
    setInput(e.target.value);
  }

  async function onSubmit(_e: React.FormEvent<HTMLFormElement>) {
    if (pending) {
      return;
    }

    if (!web3) {
      alert("No web3");
      return;
    }

    const value = Web3.utils.toBN(input);
    const zero = Web3.utils.toBN(0);

    if (value.gt(zero)) {
      const { error } = await call({
        web3,
        account,
        value,
      });

      if (error) {
        alert(`Error: ${error.message}`);
      } else {
        setInput("");
      }
    }
  }

  return (
    <Form onSubmit={onSubmit}>
      <Form.Field>
        <Form.Input
          placeholder="Amount to deposit wei"
          type="number"
          min={0}
          value={input}
          onChange={onChange}
        />
      </Form.Field>
      <Button color="green" disabled={pending} loading={pending}>
        Deposit
      </Button>
    </Form>
  );
}
Example #7
Source File: topmenu.tsx    From website with MIT License 5 votes vote down vote up
render() {
		const { user, password, loginDialogOpen, loggingIn, errorMessage, messageModalOpen } = this.state;
		const { narrowLayout, children } = this.props;
		const windowGlobal = typeof window !== 'undefined' && window;
		let isLoggedIn = windowGlobal && window.localStorage && window.localStorage.getItem('token') && window.localStorage.getItem('username');
		const userName = isLoggedIn ? window.localStorage.getItem('username') : '';

		return (
			<React.Fragment>
				<NavBar narrowLayout={narrowLayout} onMessageClicked={() => this.setState({ messageModalOpen: true })}>
					{children}
				</NavBar>

				<Modal open={loginDialogOpen} onClose={() => this._closeLoginDialog()} size='tiny'>
					<Modal.Header>Log-in to your account</Modal.Header>
					<Modal.Content>
						<Grid textAlign='center' verticalAlign='middle'>
							<Grid.Column style={{ maxWidth: 450 }}>
								<Form size='large' loading={loggingIn}>
									<Segment>
										<Form.Input
											fluid
											icon='user'
											iconPosition='left'
											placeholder='Username'
											value={user}
											onChange={(e, { value }) => this.setState({ user: value })}
										/>
										<Form.Input
											fluid
											icon='lock'
											iconPosition='left'
											placeholder='Password'
											type='password'
											value={password}
											onChange={(e, { value }) => this.setState({ password: value })}
										/>
									</Segment>
								</Form>
								{errorMessage && <Message error>{errorMessage}</Message>}
								{!errorMessage && (
									<Message>If you are an approved book editor, log in here to submit changes directly from the site.</Message>
								)}
							</Grid.Column>
						</Grid>
					</Modal.Content>
					<Modal.Actions>
						<Button content='Cancel' onClick={() => this._closeLoginDialog()} />
						<Button positive content='Login' onClick={() => this._doLogin()} />
					</Modal.Actions>
				</Modal>

				<Modal open={messageModalOpen} closeOnEscape={false} closeOnDimmerClick={false} onClose={() => this._closeMessageDialog()}>
					<Modal.Header>The DataCore website and bot are in need of software engineers!</Modal.Header>
					<Modal.Content>
						<p>
							We need your help! The project is <a href='https://github.com/stt-datacore'>open source</a> so we're open for contributions
							from software engineers, designers, devops, testers and so on. Reach out on our{' '}
							<a href='https://discord.gg/2SY8W7Aeme'>development Discord</a> if you're not sure where to start.
						</p>
						<p>
							If you've always wanted a feature on DataCore, here's your chance to hack on the project and implement it yourself! Most of
							the project is written in TypeScript, with node.js on the backend and React with Gatsby on the frontend.
						</p>
					</Modal.Content>
					<Modal.Actions>
						<Button icon='checkmark' onClick={() => this._closeMessageDialog()} content='Ok' />
					</Modal.Actions>
				</Modal>
			</React.Fragment>
		);
	}
Example #8
Source File: voyagecalculator.tsx    From website with MIT License 5 votes vote down vote up
InputCrewOptions = (props: InputCrewOptionsProps) => {
	const { myCrew, updateConsideredCrew } = props;

	const [considerActive, setConsiderActive] = React.useState(false);
	const [considerFrozen, setConsiderFrozen] = React.useState(false);
	const [excludedCrew, setExcludedCrew] = React.useState([]);

	React.useEffect(() => {
		const consideredCrew = myCrew.filter(crewman => {
			if (!considerActive && crewman.active_status == 2)
				return false;

			if (!considerFrozen && crewman.immortal > 0)
				return false;

			if (excludedCrew && excludedCrew.includes(crewman.id))
				return false;

			return true;
		});
		updateConsideredCrew(consideredCrew);
	}, [considerActive, considerFrozen, excludedCrew]);

	const activeCount = myCrew.filter(crew => crew.active_status == 2).length;

	return (
		<React.Fragment>
			<Form.Group grouped>
				{activeCount > 0 && (
					<Form.Field
						control={Checkbox}
						label='Consider crew on active shuttles'
						checked={considerActive}
						onChange={(e, { checked }) => setConsiderActive(checked)}
					/>
				)}
				<Form.Field
					control={Checkbox}
					label='Consider frozen (vaulted) crew'
					checked={considerFrozen}
					onChange={(e, { checked }) => setConsiderFrozen(checked)}
				/>
			</Form.Group>
			<InputCrewExcluder myCrew={myCrew} excludedCrew={excludedCrew} updateExclusions={setExcludedCrew} showFrozen={considerFrozen} />
		</React.Fragment>
	);
}
Example #9
Source File: behold.tsx    From website with MIT License 5 votes vote down vote up
render() {
		if (this.state.allcrew.length === 0) {
			return (
				<Layout title='Behold helper / crew comparison'>
					<div className='ui medium centered text active inline loader'>Loading data...</div>
				</Layout>
			);
		}

		let peopleToShow = [...this.state.peopleList];
		if (this.state.minRarity) {
			peopleToShow = peopleToShow.filter((crew) => crew.max_rarity >= this.state.minRarity);
		}

		return (
			<Layout title='Behold helper / crew comparison'>
				<Header as='h2'>Behold helper / crew comparison</Header>
				<Form>
					<Form.Group>
						<Dropdown
							clearable
							fluid
							multiple
							search
							selection
							closeOnChange
							options={peopleToShow}
							placeholder='Search for crew to compare'
							label='Behold crew'
							value={this.state.currentSelectedItems}
							onChange={(e, { value }) => this._selectionChanged(value)}
						/>
						<Form.Field
							control={Dropdown}
							placeholder={this.state.minRarity ? `Minimum rarity: ${this.state.minRarity}` : `Minimum rarity`}
							selection
							options={rarityOptions}
							value={this.state.minRarity}
							onChange={(e, { value }) => this.setState({ minRarity: value })}
						/>
					</Form.Group>
				</Form>
				{this.state.currentSelectedItems?.length > 0 && (
					<Button compact icon='add user' color='green' content='Preview all in your roster' onClick={() => { this._addProspects(); }} />
				)}

				<Divider horizontal hidden />

				<Grid columns={3} stackable centered padded divided>
					{this.state.entries.map((entry, idx) => (
						<Grid.Column key={idx}>
							<Header as='h5'>
								<Link to={`/crew/${entry.crew.symbol}/`}>
									{entry.crew.name}{' '}
									<Rating defaultRating={entry.crew.max_rarity} maxRating={entry.crew.max_rarity} icon='star' size='small' disabled />
								</Link>
							</Header>
							<CommonCrewData compact={true} crewDemands={entry.crewDemands} crew={entry.crew} markdownRemark={entry.markdownRemark} roster={this.state.roster}/>
							{entry.markdown && (
								<React.Fragment>
									<div dangerouslySetInnerHTML={{ __html: entry.markdown }} />
									<div style={{ marginTop: '1em' }}>
										<a href={`https://www.bigbook.app/crew/${entry.crew.symbol}`}>View {entry.crew.name} on Big Book</a>
									</div>
								</React.Fragment>
							)}
						</Grid.Column>
					))}
				</Grid>
			</Layout>
		);
	}
Example #10
Source File: bridgecrew.tsx    From website with MIT License 5 votes vote down vote up
render() {
		if (this.state.allcrew.length === 0) {
			return (
				<Layout title='Bridge crew assembly'>
					<div className='ui medium centered text active inline loader'>Loading data...</div>
				</Layout>
			);
		}

		let peopleToShow = [...this.state.peopleList];

		return (
			<Layout title='Bridge crew assembly'>
				<Header as='h4'>Bridge crew assembly</Header>
				<p>Assemble your bridge crew.</p>
				<Form>
					<Form.Group>
						<Dropdown
							clearable
							fluid
							multiple
							search
							selection
							closeOnChange
							options={peopleToShow}
							placeholder='Select or search for crew'
							label='Bridge crew'
							value={this.state.currentSelectedItems}
							onChange={(e, { value }) => this._selectionChanged(value)}
						/>
					</Form.Group>
				</Form>

				<Divider horizontal hidden />

				<div style={{ height: '500px', overflow: 'hidden', textAlign: 'center', padding: '25px', backgroundColor: '#203147', border: '2px solid lightblue' }}>
					<Header as='h3'>Bridge Crew</Header>
					{this.state.entries.map((entry, idx) => (
							<img src={`${process.env.GATSBY_ASSETS_URL}${entry.crew.imageUrlFullBody}`} style={{ height: '725px', margin: '0 -6.5%' }} />
					))}
				</div>
			</Layout>
		);
	}
Example #11
Source File: Story.tsx    From communitymap-ui with Apache License 2.0 5 votes vote down vote up
AddNewStoryObject: React.FC<{
  type: ObjectItemInput['type'];
  onPost: (data: ObjectItemInput) => void;
}> = ({ type, onPost }) => {
  const [state, setState] = useState({ valid_until: 12 * 60 } as any);
  const onChange = (e: any) => {
    const { name, value } = e.target;
    console.debug(e.target.name, e.target.value);
    setState({ ...state, [name]: value });
  };
  return (
    <div className="add-new-story">
      <h4>
        <Icon name="edit outline" /> Create story
      </h4>
      <Form
        onSubmit={(e) => {
          e.preventDefault();
          console.debug('submit', state);
          const { topic, message, valid_until } = state;

          onPost({
            type,
            title: topic || message,
            description: message,
            valid_until: dayjs().add(valid_until, 'minute').toISOString(),
          });
        }}
      >
        <Form.Input
          autoComplete="off"
          label="Subject"
          name="topic"
          onChange={onChange}
        />
        <Form.TextArea
          autoFocus
          label="Story"
          name="message"
          onChange={onChange}
        />

        <Form.Button primary>Post</Form.Button>
      </Form>
    </div>
  );
}
Example #12
Source File: Chat.tsx    From communitymap-ui with Apache License 2.0 5 votes vote down vote up
AddNewChatObject: React.FC<{
  type: ObjectItemInput['type'];
  onPost: (data: ObjectItemInput) => void;
}> = ({ type, onPost }) => {
  const [state, setState] = useState({ valid_until: 12 * 60 } as any);
  const onChange = (e: any) => {
    const { name, value } = e.target;
    console.debug(e.target.name, e.target.value);
    setState({ ...state, [name]: value });
  };
  return (
    <div className="add-new-chat">
      <h4>
        <Icon name={type2icon(type)} /> {type2title(type)}
      </h4>
      <Form
        onSubmit={(e) => {
          e.preventDefault();
          console.debug('submit', state);
          const { topic, message, valid_until } = state;

          onPost({
            type,
            title: topic || message,
            description: message,
            valid_until: dayjs().add(valid_until, 'minute').toISOString(),
          });
        }}
      >
        <Form.Input
          autoComplete="off"
          label="Topic (optional)"
          name="topic"
          onChange={onChange}
        />
        <Form.TextArea
          autoFocus
          label="Message"
          name="message"
          onChange={onChange}
        />
        <Form.Dropdown
          options={validUntilOptions}
          label="Valid for next"
          selection
          defaultValue={state.valid_until}
          onChange={(e, { value }) =>
            setState({ ...state, valid_until: value })
          }
        />

        <Form.Button primary>Post</Form.Button>
      </Form>
    </div>
  );
}
Example #13
Source File: LoginModal.tsx    From watchparty with MIT License 5 votes vote down vote up
render() {
    const { closeLogin } = this.props;
    return (
      <Modal open={true} onClose={closeLogin as any}>
        <Modal.Header>
          {this.state.isCreateMode ? 'Create an account' : 'Login'}
        </Modal.Header>
        <Modal.Content>
          {this.state.error && (
            <Message color="red" header="Error" content={this.state.error} />
          )}
          <Form>
            <Form.Field>
              <label>Email</label>
              <input
                placeholder="Email"
                value={this.state.email}
                onChange={(e) => this.setState({ email: e.target.value })}
              />
            </Form.Field>
            <Form.Field>
              <label>Password</label>
              <input
                type="password"
                placeholder="Password"
                value={this.state.password}
                onChange={(e) => this.setState({ password: e.target.value })}
              />
            </Form.Field>
            {!this.state.isCreateMode && (
              <div>
                Don't have an account?{' '}
                <button
                  type="button"
                  className="linkButton"
                  onClick={() => this.setState({ isCreateMode: true })}
                >
                  Create one.
                </button>{' '}
                Forgot your password? Enter your email and{' '}
                <button
                  type="button"
                  className="linkButton"
                  onClick={this.resetPassword}
                >
                  reset it.
                </button>
              </div>
            )}
            {this.state.isCreateMode ? (
              <Button
                type="submit"
                onClick={() =>
                  this.createAccount(this.state.email, this.state.password)
                }
              >
                Create
              </Button>
            ) : (
              <Button
                type="submit"
                onClick={() =>
                  this.emailSignIn(this.state.email, this.state.password)
                }
              >
                Login
              </Button>
            )}
          </Form>
        </Modal.Content>
      </Modal>
    );
  }
Example #14
Source File: index.tsx    From frontegg-react with MIT License 5 votes vote down vote up
Input = forwardRef<HTMLInputElement, InputProps>(
  ({ children, labelButton, multiline, label, ...restProps }, forwardRef) => {
    const inputLabel = useMemo(
      () =>
        labelButton ? (
          <label className='fe-label__with-button'>
            {label}
            <Button {...labelButton} />
          </label>
        ) : (
          label
        ),
      [labelButton, label]
    );

    const iconContent = useMemo(() => restProps.prefixIcon ?? restProps.suffixIcon, [restProps]);

    const inputProps = { ...mapper(restProps), label: inputLabel };

    return multiline ? (
      <Form.TextArea {...(inputProps as StrictTextAreaProps)} ref={forwardRef}></Form.TextArea>
    ) : restProps.iconAction ? (
      <Form.Input
        {...(inputProps as StrictInputProps | StrictFormInputProps)}
        icon
        action={{
          icon: iconContent,
          onClick: restProps.iconAction,
        }}
        ref={forwardRef}
      >
        {children}
      </Form.Input>
    ) : (
      <Form.Input {...(inputProps as StrictInputProps | StrictFormInputProps)} icon>
        <input ref={forwardRef} />
        {iconContent}
        {children}
      </Form.Input>
    );
  }
)
Example #15
Source File: index.tsx    From frontegg-react with MIT License 5 votes vote down vote up
Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
  if (props.fullWidth) {
    return <Form.Checkbox ref={ref as any} {...mapper(props)} />;
  }
  return <SemanticCheckbox label ref={ref as any} {...mapper(props)} />;
})
Example #16
Source File: ExampleList.tsx    From plugin-vscode with Apache License 2.0 5 votes vote down vote up
public render() {
        return (
            <Grid className="welcome-page" divided>
                <Grid.Row className="welcome-navbar" columns={1}>
                    <Grid.Column>
                        <Header as="h3" dividing>
                            Ballerina Examples
                        </Header>
                        {this.state && this.state.noSearchReults ?
                            (<>No search results found!</>) : null
                        }
                        {this.state && this.state.samples && this.state.samples.length > 0 ?
                            (
                                <Form>
                                    <Form.Field inline>
                                        <Input
                                            ref={(ref) => {
                                                this.searchInput = ref as Input;
                                            }}
                                            loading={!this.state || !this.state.samples}
                                            placeholder="Search"
                                            onChange={(event: React.SyntheticEvent<HTMLInputElement>) => {
                                                this.setState({
                                                    searchQuery: event.currentTarget.value,
                                                });
                                                this.onSearchQueryEdit();
                                            }}
                                            className="search-control"
                                        />
                                    </Form.Field>
                                </Form>
                            ) : (
                                <>No Samples found in [BALLERINA_HOME]/examples folder.</>
                            )
                        }
                    </Grid.Column>
                </Grid.Row>
                <Grid.Row className="welcome-content-wrapper">
                    <Grid.Column mobile={16} tablet={16} computer={16} className="rightContainer">
                        <Grid>
                            {this.state && this.state.samples &&
                                <Grid.Row columns={4} className="sample-wrapper">
                                    {
                                        this.getColumnContents().map((column, index) => {
                                            return (
                                                <Grid.Column key={index} mobile={16} tablet={8} computer={4}>
                                                    {column.map((columnItem) => this.renderColumnItem(columnItem))}
                                                </Grid.Column>
                                            );
                                        })
                                    }
                                </Grid.Row>
                            }
                        </Grid>
                    </Grid.Column>
                </Grid.Row>
            </Grid>);
    }
Example #17
Source File: CreateTxModal.tsx    From multi-sig-wallet with MIT License 4 votes vote down vote up
CreateTxModal: React.FC<Props> = ({ open, onClose }) => {
  const {
    state: { web3, account },
  } = useWeb3Context();

  const { pending, error, call } = useAsync<SubmitTxParams, any>(
    async (params) => {
      if (!web3) {
        throw new Error("No web3");
      }

      await submitTx(web3, account, params);
    }
  );

  const [inputs, setInputs] = useState({
    to: "",
    value: 0,
    data: "",
  });

  function onChange(name: string, e: React.ChangeEvent<HTMLInputElement>) {
    setInputs({
      ...inputs,
      [name]: e.target.value,
    });
  }

  async function onSubmit() {
    if (pending) {
      return;
    }

    const { error } = await call({
      ...inputs,
      value: inputs.value.toString(),
    });

    if (!error) {
      onClose();
    }
  }

  return (
    <Modal open={open} onClose={onClose}>
      <Modal.Header>Create Transaction</Modal.Header>
      <Modal.Content>
        {error && <Message error>{error.message}</Message>}
        <Form onSubmit={onSubmit}>
          <Form.Field>
            <label>To</label>
            <Form.Input
              type="text"
              value={inputs.to}
              onChange={(e) => onChange("to", e)}
            />
          </Form.Field>
          <Form.Field>
            <label>Value</label>
            <Form.Input
              type="number"
              min={0}
              value={inputs.value}
              onChange={(e) => onChange("value", e)}
            />
          </Form.Field>
          <Form.Field>
            <label>Data</label>
            <Form.Input
              value={inputs.data}
              onChange={(e) => onChange("data", e)}
            />
          </Form.Field>
        </Form>
      </Modal.Content>
      <Modal.Actions>
        <Button onClick={onClose} disabled={pending}>
          Cancel
        </Button>
        <Button
          color="green"
          onClick={onSubmit}
          disabled={pending}
          loading={pending}
        >
          Create
        </Button>
      </Modal.Actions>
    </Modal>
  );
}
Example #18
Source File: playertools.tsx    From website with MIT License 4 votes vote down vote up
PlayerToolsForm = (props: PlayerToolsFormProps) => {
	const PLAYERLINK = 'https://stt.disruptorbeam.com/player?client_api=17';

	const { setValidInput } = props;

	const [inputPlayerData, setInputPlayerData] = React.useState(undefined);
	const [fullInput, setFullInput] = React.useState('');
	const [displayedInput, setDisplayedInput] = React.useState('');
	const [errorMessage, setErrorMessage] = React.useState(undefined);

	let inputUploadFile = null;

	if (fullInput != "")
		parseInput();

	React.useEffect(() => {
		if (inputPlayerData) {
			setValidInput(inputPlayerData);
			setInputPlayerData(undefined);
		}
	}, [inputPlayerData]);

	return (
		<Layout title='Player tools'>
			<Header as='h2'>Player tools</Header>
			<p>You can access some of your player data from the game's website and import it here to calculate optimal voyage lineups, identify unnecessary items, export your crew list as a CSV, or share your profile with other players, among other tools. This website cannot make direct requests to the game's servers due to security configurations and unclear terms of service interpretations, so there are a few manual steps required to import your data.</p>
			<p>If you have multiple accounts, we recommend using your browser in InPrivate mode (Edge) or Incognito mode (Firefox / Chrome) to avoid caching your account credentials, making it easier to change accounts.</p>
			<ul>
				<li>
					Open this page in your browser:{' '}
					<a href={PLAYERLINK} target='_blank'>
						https://stt.disruptorbeam.com/player
						</a>
				</li>
				<li>
					Log in if asked, then wait for the page to finish loading. It should start with:{' '}
					<span style={{ fontFamily: 'monospace' }}>{'{"action":"update","player":'}</span> ...
					</li>
				<li>Select everything in the page (Ctrl+A) and copy it (Ctrl+C)</li>
				<li>Paste it (Ctrl+V) in the text box below. Note that DataCore will intentionally display less data here to speed up the process</li>
				<li>Click the 'Import data' button</li>
			</ul>

			<Form>
				<TextArea
					placeholder='Paste your player data here'
					value={displayedInput}
					onChange={(e, { value }) => setDisplayedInput(value)}
					onPaste={(e) => { return onInputPaste(e) }}
				/>
				<input
					type='file'
					onChange={(e) => { handleFileUpload(e) }}
					style={{ display: 'none' }}
					ref={e => inputUploadFile = e}
				/>
			</Form>

			<Button
				onClick={() => parseInput()}
				style={{ marginTop: '1em' }}
				content='Import data'
				icon='paste'
				labelPosition='right'
			/>

			{errorMessage && (
				<Message negative>
					<Message.Header>Error</Message.Header>
					<p>{errorMessage}</p>
				</Message>
			)}

			<p style={{ marginTop: '2em' }}>To circumvent the long text copy limitations on mobile devices, download{' '}
				<a href={PLAYERLINK} target='_blank'>
					your player data
						</a>
				{' '}to your device, then click the 'Upload data file' button.
					</p>
			<p>
				<Modal
					trigger={<a href="#">Click here for detailed instructions for Apple iOS devices.</a>}
					header='Player data upload on iOS'
					content={<ul>
						<li>Go to your player data using the link provided, logging in if asked.</li>
						<li>Wait for the page to finish loading. It should start with:{' '}
							<span style={{ fontFamily: 'monospace' }}>{'{"action":"update","player":'}</span> ...
								</li>
						<li>Press the share icon while viewing the page.</li>
						<li>Tap 'options' and choose 'Web Archive', tap 'save to files', choose a location and save.</li>
						<li>Come back to this page (DataCore.app player tools).</li>
						<li>Tap the 'Upload data file' button.</li>
						<li>Choose the file starting with 'player?client_api...' from where you saved it.</li>
					</ul>}
				/>
			</p>

			<Button
				onClick={() => inputUploadFile.click()}
				content='Upload data file'
				icon='file'
				labelPosition='right'
			/>
		</Layout>
	);

	function parseInput() {
		let testInput = fullInput;

		// Use inputted text if no pasted text detected
		if (testInput == '') testInput = displayedInput;

		try {
			let testData = JSON.parse(testInput as string);

			if (testData) {
				// Test for playerData array glitch
				if (Array.isArray(testData)) {
					testData = {...testData[0]};
				}
				if (testData.player && testData.player.display_name) {
					if (testData.player.character && testData.player.character.crew && (testData.player.character.crew.length > 0)) {
						setInputPlayerData(testData);
						setDisplayedInput('');
						setErrorMessage(undefined);
					} else {
						setErrorMessage('Failed to parse player data from the text you pasted. Make sure you are logged in with the correct account.');
					}
				}
				else {
					setErrorMessage('Failed to parse player data from the text you pasted. Make sure the page is loaded correctly and you copied the entire contents!');
				}
			} else {
				setErrorMessage('Failed to parse player data from the text you pasted. Make sure the page is loaded correctly and you copied the entire contents!');
			}
		} catch (err) {
			if ((/Log in to CS Tools/).test(testInput)) {
				setErrorMessage('You are not logged in! Open the player data link above and log in to the game as instructed. Then return to this DataCore page and repeat all the steps to import your data.');
			}
			else {
				setErrorMessage(`Failed to read the data. Make sure the page is loaded correctly and you copied the entire contents! (${err})`);
			}
		}

		setFullInput('');
	}

	function onInputPaste(event) {
		let paste = event.clipboardData || window.clipboardData;
		if (paste) {
			let fullPaste = paste.getData('text');
			setFullInput(fullPaste);
			setDisplayedInput(`${fullPaste.substr(0, 300)} [ ... ] ${fullPaste.substr(-100)}\n/* Note that DataCore is intentionally displaying less data here to speed up the process */`);
			event.preventDefault();
			return false;
		}
		return true;
	}

	function handleFileUpload(event) {
		// use FileReader to read file content in browser
		const fReader = new FileReader();
		fReader.onload = (e) => {
			let data = e.target.result.toString();
			// Handle Apple webarchive wrapping
			if (data.match(/^bplist00/)) {
				// Find where the JSON begins and ends, and extract just that from the larger string.
				data = data.substring(data.indexOf('>{') + 1, data.lastIndexOf('}}') + 2);
			}
			setFullInput(data);
		};
		fReader.readAsText(event.target.files[0]);
	}
}
Example #19
Source File: playertools.tsx    From website with MIT License 4 votes vote down vote up
PlayerToolsPanes = (props: PlayerToolsPanesProps) => {
	const { playerData, strippedPlayerData, voyageData, eventData, activeCrew, dataSource,
			allCrew, allItems, requestShowForm, requestClearData } = props;

	const [showIfStale, setShowIfStale] = useStateWithStorage('tools/showStale', true);

	const [showShare, setShowShare] = useStateWithStorage(playerData.player.dbid+'/tools/showShare', true, { rememberForever: true, onInitialize: variableReady });
	const [profileAutoUpdate, setProfileAutoUpdate] = useStateWithStorage(playerData.player.dbid+'/tools/profileAutoUpdate', false, { rememberForever: true });
	const [profileUploaded, setProfileUploaded] = React.useState(false);
	const [profileUploading, setProfileUploading] = React.useState(false);
	const [profileShared, setProfileShared] = useStateWithStorage('tools/profileShared', false);

	const [varsReady, setVarsReady] = React.useState(false);
	const [activeTool, setActiveTool] = React.useState('voyage');

	React.useEffect(() => {
		if (dataSource == 'input' && profileAutoUpdate && !profileUploaded) {
			console.log('Uploading profile');
			shareProfile();
		}
	}, [profileAutoUpdate, strippedPlayerData]);

	const tools = playerTools;
	React.useEffect(() => {
		const urlParams = new URLSearchParams(window.location.search);
		if (urlParams.has('tool') && tools[urlParams.get('tool')])
			setActiveTool(urlParams.get('tool'));
	}, [window.location.search]);

	const StaleMessage = () => {
		const STALETHRESHOLD = 3;	// in hours
		if (showIfStale && new Date().getTime()-playerData.calc.lastModified.getTime() > STALETHRESHOLD*60*60*1000) {
			return (
				<Message
					warning
					icon='clock'
					header='Update your player data'
					content="It's been a few hours since you last updated your player data. We recommend that you update now to make sure our tools are providing you recent information about your crew."
					onDismiss={() => setShowIfStale(false)}
				/>
			);
		}
		else {
			return (<></>);
		}
	};

	const ShareMessage = () => {
		if (!showShare) return (<></>);

		// The option to auto-share profile only appears after a profile is uploaded or if previously set to auto-update
		const bShowUploaded = profileUploaded || profileAutoUpdate;

		return (
			<Message icon onDismiss={() => setShowShare(false)}>
				<Icon name='share alternate' />
				<Message.Content>
					<Message.Header>Share your player profile!</Message.Header>
					{!bShowUploaded && (
						<div>
							<p>
								Click here to{' '}
								<Button size='small' color='green' onClick={() => shareProfile()}>
									{profileUploading && <Icon loading name='spinner' />}share your profile
									</Button>{' '}
									and unlock more tools and export options for items and ships. More details:
							</p>
							<Message.List>
								<Message.Item>
									Once shared, the profile will be publicly accessible, will be accessible by your DBID link, and linked on related pages (such as fleet pages & event pages)
									</Message.Item>
								<Message.Item>
									There is no private information included in the player profile; information being shared is limited to:{' '}
									<b>captain name, level, vip level, fleet name and role, achievements, completed missions, your crew, items and ships.</b>
								</Message.Item>
							</Message.List>
						</div>
					)}
					{bShowUploaded && (
						<Form.Group>
							<p>
								Your profile was uploaded. Share the link:{' '}
								<a
									href={`${process.env.GATSBY_DATACORE_URL}profile/?dbid=${playerData.player.dbid}`}
									target='_blank'>{`${process.env.GATSBY_DATACORE_URL}profile/?dbid=${playerData.player.dbid}`}</a>
							</p>
							<Form.Field
								control={Checkbox}
								label='Automatically share profile after every import'
								checked={profileAutoUpdate}
								onChange={(e, { checked }) => setProfileAutoUpdate(checked)}
							/>
						</Form.Group>
					)}
				</Message.Content>
			</Message>
		);
	};

	if (!varsReady)
		return (<PlayerToolsLoading />);

	const PlayerLevelProgress = () => {
		const endingValue = playerData.player.character.xp_for_next_level - playerData.player.character.xp_for_current_level;
		const currentValue = playerData.player.character.xp - playerData.player.character.xp_for_current_level;
		const percent = (currentValue / endingValue) * 100;
		return (
		  <Progress
			percent={percent.toPrecision(3)}
			label={`Level ${playerData.player.character.level}: ${playerData.player.character.xp} / ${playerData.player.character.xp_for_next_level}`}
			progress
		  />
		);
	 };

	return (
		<Layout title='Player tools'>
			<Header as='h4'>Hello, {playerData.player.character.display_name}</Header>
			<PlayerLevelProgress />
			<StaleMessage />
			<Menu compact stackable>
				<Menu.Item>
					Last imported: {playerData.calc.lastModified.toLocaleString()}
				</Menu.Item>
				<Dropdown item text='Profile options'>
					<Dropdown.Menu>
						<Dropdown.Item onClick={() => requestShowForm(true)}>Update now...</Dropdown.Item>
						{!showShare && (<Dropdown.Item onClick={() => setShowShare(true)}>Share profile...</Dropdown.Item>)}
						<Dropdown.Item onClick={() => requestClearData()}>Clear player data</Dropdown.Item>
					</Dropdown.Menu>
				</Dropdown>
			  <Dropdown item text='Export'>
				<Dropdown.Menu>
					<Popup basic content='Download crew data as traditional comma delimited CSV file' trigger={
						<Dropdown.Item onClick={() => exportCrewTool()} content='Download CSV...' />
					} />
					<Popup basic content='Copy crew data to clipboard in Google Sheets format' trigger={
						<Dropdown.Item onClick={() => exportCrewToClipboard()} content='Copy to clipboard' />
					} />
				</Dropdown.Menu>
			</Dropdown>
			</Menu>

			<React.Fragment>
				<ShareMessage />
				<Header as='h3'>{tools[activeTool].title}</Header>
				{tools[activeTool].render(props)}
			</React.Fragment>
		</Layout>
	);

	function variableReady(keyName: string) {
		setVarsReady(true);
	}

	function shareProfile() {
		setProfileUploading(true);

		let jsonBody = JSON.stringify({
			dbid: playerData.player.dbid,
			player_data: strippedPlayerData
		});

		fetch(`${process.env.GATSBY_DATACORE_URL}api/post_profile`, {
			method: 'post',
			headers: {
				'Content-Type': 'application/json'
			},
			body: jsonBody
		}).then(() => {
			if (!profileAutoUpdate) window.open(`${process.env.GATSBY_DATACORE_URL}profile/?dbid=${playerData.player.dbid}`, '_blank');
			setProfileUploading(false);
			setProfileUploaded(true);
			setProfileShared(true);
		});
	}

	function exportCrewTool() {
		let text = exportCrew(playerData.player.character.crew.concat(playerData.player.character.unOwnedCrew));
		downloadData(`data:text/csv;charset=utf-8,${encodeURIComponent(text)}`, 'crew.csv');
	}

	function exportCrewToClipboard() {
		let text = exportCrew(playerData.player.character.crew.concat(playerData.player.character.unOwnedCrew), '\t');
		navigator.clipboard.writeText(text);
	}
}
Example #20
Source File: voyagecalculator.tsx    From website with MIT License 4 votes vote down vote up
InputCrewExcluder = (props: InputCrewExcluderProps) => {
	const { updateExclusions } = props;

	const [eventData, setEventData] = useStateWithStorage('tools/eventData', undefined);
	const [activeEvent, setActiveEvent] = React.useState(undefined);
	const [options, setOptions] = React.useState(undefined);

	React.useEffect(() => {
		if (props.excludedCrew)
			if (options && !options.initialized) populatePlaceholders();
	}, [props.excludedCrew]);

	React.useEffect(() => {
		if (!options) return;
		if (!options.initialized)
			populatePlaceholders();
		else
			populateOptions();
	}, [props.showFrozen]);

	React.useEffect(() => {
		if (activeEvent && activeEvent.seconds_to_end > 0 && activeEvent.seconds_to_start < 86400) {
			if (activeEvent.content_types.includes('shuttles') || activeEvent.content_types.includes('gather')) {
				const crewIds = props.myCrew.filter(c => activeEvent.bonus.includes(c.symbol)).map(c => c.id);
				updateExclusions([...crewIds]);
			}
		}
	}, [activeEvent]);

	if (!activeEvent) {
		identifyActiveEvent();
		return (<></>);
	}

	if (!options) {
		populatePlaceholders();
		return (<></>);
	}

	const label = (
		<React.Fragment>
			<label>Crew to exclude from voyage</label>
			{activeEvent && activeEvent.bonus.length > 0 &&
				(<div style={{ margin: '-.5em 0 .5em' }}>Preselected crew give bonuses for the event <b>{activeEvent.name}</b></div>)
			}
		</React.Fragment>
	);

	return (
		<React.Fragment>
			<Form.Field
				label={label}
				placeholder='Search for crew to exclude'
				control={Dropdown}
				clearable
				fluid
				multiple
				search
				selection
				options={options.list}
				value={props.excludedCrew}
				onFocus={() => { if (!options.initialized) populateOptions(); }}
				onChange={(e, { value }) => updateExclusions(value) }
			/>
		</React.Fragment>
	);

	function identifyActiveEvent(): void {
		// Get event data from recently uploaded playerData
		if (eventData && eventData.length > 0) {
			const currentEvent = getEventData(eventData.sort((a, b) => (a.seconds_to_start - b.seconds_to_start))[0]);
			setActiveEvent({...currentEvent});
		}
		// Otherwise guess event from autosynced events
		else {
			guessCurrentEvent().then(currentEvent => {
				setActiveEvent({...currentEvent});
			});
		}
	}

	function populatePlaceholders(): void {
		const options = { initialized: false, list: [] };
		if (props.excludedCrew.length > 0) {
			let crewList = [...props.myCrew];
			if (!props.showFrozen) crewList = crewList.filter(c => c.immortal == 0);
			options.list = crewList.filter(c => props.excludedCrew.includes(c.id)).map(c => {
				return { key: c.id, value: c.id, text: c.name, image: { avatar: true, src: `${process.env.GATSBY_ASSETS_URL}${c.imageUrlPortrait}` }};
			});
		}
		else {
			options.list = [{ key: 0, value: 0, text: 'Loading...' }];
		}
		setOptions({...options});
	}

	function populateOptions(): void {
		let crewList = [...props.myCrew];
		if (!props.showFrozen) crewList = crewList.filter(c => c.immortal == 0);
		options.list = crewList.sort((a, b) => a.name.localeCompare(b.name)).map(c => {
			return { key: c.id, value: c.id, text: c.name, image: { avatar: true, src: `${process.env.GATSBY_ASSETS_URL}${c.imageUrlPortrait}` }};
		});
		options.initialized = true;
		setOptions({...options});
	}
}
Example #21
Source File: voyagecalculator.tsx    From website with MIT License 4 votes vote down vote up
VoyageInput = (props: VoyageInputProps) => {
	const { voyageConfig, myCrew, allShips, useInVoyage } = props;

	const [bestShip, setBestShip] = React.useState(undefined);
	const [consideredCrew, setConsideredCrew] = React.useState([]);
	const [calculator, setCalculator] = React.useState(isMobile ? 'ussjohnjay' : 'iampicard');
	const [calcOptions, setCalcOptions] = React.useState({});
	const [telemetryOptOut, setTelemetryOptOut] = useStateWithStorage('telemetryOptOut', false, { rememberForever: true });
	const [requests, setRequests] = React.useState([]);
	const [results, setResults] = React.useState([]);

	React.useEffect(() => {
		// Note that allShips is missing the default ship for some reason (1* Constellation Class)
		//	This WILL break voyagecalculator if that's the only ship a player owns
		const consideredShips = [];
		allShips.filter(ship => ship.owned).forEach(ship => {
			const traited = ship.traits.find(trait => trait === voyageConfig.ship_trait);
			let entry = {
				ship: ship,
				score: ship.antimatter + (traited ? 150 : 0),
				traited: traited,
				bestIndex: Math.min(ship.index.left, ship.index.right)
			};
			consideredShips.push(entry);
		});
		consideredShips.sort((a, b) => {
			if (a.score === b.score) return a.bestIndex - b.bestIndex;
			return b.score - a.score;
		});
		setBestShip(consideredShips[0]);
		setRequests([]);
		setResults([]);
	}, [voyageConfig]);

	React.useEffect(() => {
		return function cleanup() {
			// Cancel active calculations when leaving page
			requests.forEach(request => {
				if (request.calcState == CalculatorState.InProgress)
					request.abort();
			});
		}
	}, []);

	// Scroll here when calculator started, finished
	const topAnchor = React.useRef(null);

	const calculators = CALCULATORS.helpers.map(helper => {
		return { key: helper.id, value: helper.id, text: helper.name };
	});
	calculators.push({ key: 'all', value: 'all', text: 'All calculators (slower)' });

	return (
		<React.Fragment>
			<div ref={topAnchor} />
			{renderBestShip()}
			{renderResults()}
			{requests.length > 0 && <Header as='h3'>Options</Header>}
			<Form>
				<InputCrewOptions myCrew={myCrew} updateConsideredCrew={setConsideredCrew} />
				<Form.Group inline>
					<Form.Field
						control={Select}
						label='Calculator'
						options={calculators}
						value={calculator}
						onChange={(e, { value }) => setCalculator(value)}
						placeholder='Select calculator'
					/>
					{CALCULATORS.fields.filter(field => field.calculators.includes(calculator) || calculator == 'all').map(field => (
						<Form.Field
							key={field.id}
							control={Select}	/* Only control allowed at the moment */
							label={field.name}
							options={field.options}
							value={calcOptions[field.id] ?? field.default}
							placeholder={field.description}
							onChange={(e, { value }) => setCalcOptions(prevOptions =>
								{
									const newValue = { [field.id]: value };
									return {...prevOptions, ...newValue};
								}
							)}
						/>
					))}
				</Form.Group>

				<Form.Group>
					<Form.Button primary onClick={() => startCalculation()}>
						Calculate best crew selection
					</Form.Button>
					{voyageConfig.state &&
						<Form.Button onClick={()=> useInVoyage()}>
							Return to in voyage calculator
						</Form.Button>
					}
				</Form.Group>
			</Form>
			<Message style={{ marginTop: '2em' }}>
				<Message.Content>
					<Message.Header>Privacy Notice</Message.Header>
					<p>We use anonymous statistics aggregated from voyage calculations to improve DataCore and power our <b><Link to='/hall_of_fame'>Voyage Hall of Fame</Link></b>.</p>
					<Form>
						<Form.Field
							control={Checkbox}
							label={<label>Permit DataCore to collect anonymous voyage stats</label>}
							checked={!telemetryOptOut}
							onChange={(e, { checked }) => setTelemetryOptOut(!checked) }
						/>
					</Form>
				</Message.Content>
			</Message>
		</React.Fragment>
	);

	function renderBestShip(): JSX.Element {
		if (!bestShip) return (<></>);

		const direction = bestShip.ship.index.right < bestShip.ship.index.left ? 'right' : 'left';
		const index = bestShip.ship.index[direction] ?? 0;

		return (
			<Card fluid>
				<Card.Content>
					<Image floated='left' src={`${process.env.GATSBY_ASSETS_URL}${bestShip.ship.icon.file.substr(1).replace('/', '_')}.png`} style={{ height: '4em' }} />
					<Card.Header>{bestShip.ship.name}</Card.Header>
					<p>best ship{bestShip.traited && (<span style={{ marginLeft: '1em' }}>{` +`}{allTraits.ship_trait_names[voyageConfig.ship_trait]}</span>)}</p>
					<p style={{ marginTop: '.5em' }}>Tap <Icon name={`arrow ${direction}`} />{index} time{index != 1 ? 's' : ''} on your voyage ship selection screen to select {bestShip.ship.name}.</p>
				</Card.Content>
			</Card>
		);
	}

	function renderResults(): JSX.Element {
		if (results.length == 0)
			return (<></>);

		const showPopup = (result) => <Popup basic content={<p>{result.result.postscript}</p>} trigger={<p>{result.name}</p>} />
		const panes = results.map(result => ({
			menuItem: { key: result.id, content: result.result ? showPopup(result) : result.name },
			render: () => (
				<VoyageResultPane result={result.result}
					requests={requests} requestId={result.requestId}
					calcState={result.calcState} abortCalculation={abortCalculation}
				/>
			)
		}));

		return (
			<React.Fragment>
				<Header as='h3'>Recommended Lineups</Header>
				<Tab menu={{ pointing: true }}
					panes={panes}
				/>
			</React.Fragment>
		);
	}

	function scrollToAnchor(): void {
		if (!topAnchor.current) return;
		topAnchor.current.scrollIntoView({
			behavior: 'smooth'
		}, 500);
	}

	function startCalculation(): void {
		const helperConfig = {
			voyageConfig, bestShip, consideredCrew, calcOptions,
			resultsCallback: handleResults,
			isMobile
		};
		CALCULATORS.helpers.forEach(helper => {
			if (helper.id == calculator || calculator == 'all') {
				const request = helper.helper(helperConfig);
				requests.push(request);
				results.push({
					id: request.id,
					requestId: request.id,
					name: 'Calculating...',
					calcState: CalculatorState.InProgress
				});
				request.start();
			}
		});
		setRequests([...requests]);
		setResults([...results]);
		scrollToAnchor();
	}

	function handleResults(requestId: string, reqResults: any[], calcState: number): void {
		reqResults.forEach((reqResult, idx) => {
			// Update existing pane with results
			if (idx == 0) {
				setResults(prevResults => {
					const result = prevResults.find(r => r.id == requestId);
					if (calcState == CalculatorState.Done) {
						result.name = formatTime(reqResult.estimate.refills[0].result);
						result.calcState = CalculatorState.Done;
						sendTelemetry(requestId, reqResult);
					}
					result.result = reqResult;
					return [...prevResults];
				});
			}
			// Add new panes if multiple results generated by this request
			else {
				setResults(prevResults => [...prevResults, {
					id: requestId+'-'+idx,
					requestId,
					name: formatTime(reqResult.estimate.refills[0].result),
					calcState: CalculatorState.Done,
					result: reqResult
				}]);
			}
		});
		if (calcState == CalculatorState.Done) scrollToAnchor();
	}

	function abortCalculation(requestId: string): void {
		const request = requests.find(r => r.id == requestId);
		if (request) {
			request.abort();
			setResults(prevResults => {
				const result = prevResults.find(prev => prev.id == requestId);
				if (result.result) {
					result.name = formatTime(result.result.estimate.refills[0].result);
					result.calcState = CalculatorState.Done;
				}
				else {
					const index = prevResults.findIndex(prev => prev.id == requestId);
					prevResults.splice(index, 1);
				}
				return [...prevResults];
			});
		}
	}

	function sendTelemetry(requestId: string, result: any): void {
		if (telemetryOptOut) return;
		const request = requests.find(r => r.id == requestId);
		const estimatedDuration = result.estimate.refills[0].result*60*60;
		try {
			fetch(`${process.env.GATSBY_DATACORE_URL}api/telemetry`, {
				method: 'post',
				headers: {
					'Content-Type': 'application/json'
				},
				body: JSON.stringify({
					type: 'voyageCalc',
					data: {
						voyagers: result.entries.map((entry) => entry.choice.symbol),
						estimatedDuration,
						calculator: request ? request.calculator : ''
					}
				})
			});
		}
		catch (err) {
			console.log('An error occurred while sending telemetry', err);
		}
	}
}
Example #22
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 #23
Source File: collectionstool.tsx    From website with MIT License 4 votes vote down vote up
CrewTable = (props: CrewTableProps) => {
	const { playerCollections, collectionCrew, collectionsFilter, setCollectionsFilter } = props;

	const [showPortalOnly, setShowPortalOnly] = useStateWithStorage('collectionstool/showPortalOnly', false);

	const tableConfig: ITableConfigRow[] = [
		{ width: 2, column: 'name', title: 'Crew' },
		{ width: 1, column: 'max_rarity', title: 'Rarity', reverse: true, tiebreakers: ['highest_owned_rarity'] },
		{ width: 1, column: 'unmaxedIds.length', title: 'Collections', reverse: true },
		{ width: 3, column: 'immortalRewards.length', title: <span>Immortal Rewards <Popup trigger={<Icon name='help' />} content='Rewards you can claim if you immortalize this crew right now' /></span>, reverse: true }
	];

	const collectionsOptions = playerCollections.filter(collection => collection.milestone.goal > 0).sort((a, b) => a.name.localeCompare(b.name)).map(collection => {
		return {
			key: collection.id,
			value: collection.id,
			text: collection.name + ' (' + collection.progress + ' / ' + collection.milestone.goal + ')'
		};
	});

	return (
		<React.Fragment>
			<Header as='h4'>Collection Crew</Header>
			<p>Search for crew that will help you make progress on collections and see what rewards you could claim by immortalizing certain crew right now. Note: maxed collections and immortalized crew will not be shown in this table.</p>
			<div style={{ margin: '1em 0' }}>
				<Form.Field
					placeholder='Filter by collections'
					control={Dropdown}
					clearable
					multiple
					search
					selection
					options={collectionsOptions}
					value={collectionsFilter}
					onChange={(e, { value }) => setCollectionsFilter(value) }
					closeOnChange
				/>
			</div>
			<div style={{ margin: '1em 0' }}>
				<Form.Group inline>
					<Form.Field
						control={Checkbox}
						label='Only show crew in portal'
						checked={showPortalOnly}
						onChange={(e, { checked }) => setShowPortalOnly(checked)}
					/>
				</Form.Group>
			</div>
			<SearchableTable
				id='collections/crew'
				data={collectionCrew}
				config={tableConfig}
				renderTableRow={(crew, idx) => renderCrewRow(crew, idx)}
				filterRow={(crew, filters, filterType) => showThisCrew(crew, filters, filterType)}
			/>
		</React.Fragment>
	);

	function showThisCrew(crew: any, filters: [], filterType: string): boolean {
		if (crew.immortal) return false;
		if (showPortalOnly && !crew.in_portal) return false;
		if (collectionsFilter.length > 0) {
			let hasAllCollections = true;
			for (let i = 0; i < collectionsFilter.length; i++) {
				if (!crew.unmaxedIds.includes(collectionsFilter[i])) {
					hasAllCollections = false;
					break;
				}
			}
			if (!hasAllCollections) return false;
		}
		return crewMatchesSearchFilter(crew, filters, filterType);
	}

	function renderCrewRow(crew: any, idx: number): JSX.Element {
		const unmaxed = crew.unmaxedIds.map(id => { return playerCollections.find(pc => pc.id === id) });
		const tabledProgress = unmaxed.sort((a, b) => a.needed - b.needed).map(collection => {
			return (
				<tr key={collection.id}>
					<td style={{ whiteSpace: 'nowrap', fontSize: '.95em' }}>{collection.name}</td>
					<td style={{ textAlign: 'right', fontSize: '.95em' }}>{collection.progress} / {collection.milestone.goal}</td>
				</tr>
			);
		});

		return (
			<Table.Row key={crew.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}${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.highest_owned_rarity} maxRating={crew.max_rarity} size='large' disabled />
				</Table.Cell>
				<Table.Cell>
					{tabledProgress && (
						<table style={{ width: '100%' }}>
							<tbody>{tabledProgress}</tbody>
						</table>
					)}
				</Table.Cell>
				<Table.Cell textAlign='center'>
					<RewardsGrid rewards={crew.immortalRewards} />
				</Table.Cell>
			</Table.Row>
		);
	}

	function descriptionLabel(crew: any): JSX.Element {
		if (crew.immortal) {
			return (
				<div>
					<Icon name='snowflake' /> <span>{crew.immortal} frozen</span>
				</div>
			);
		} else {
			return (
				<div>
					{crew.highest_owned_rarity > 0 && (<span>Level {crew.highest_owned_level}</span>)}
				</div>
			);
		}
	}
}
Example #24
Source File: shuttlehelper.tsx    From website with MIT License 4 votes vote down vote up
ShuttleHelper = (props: ShuttleHelperProps) => {
	const [shuttlers, setShuttlers] = useStateWithStorage(props.dbid+'/shuttlers/setups', new Shuttlers(), { rememberForever: true, onInitialize: variableReady });
	const [assigned, setAssigned] = useStateWithStorage(props.dbid+'/shuttlers/assigned', [], { rememberForever: true, onInitialize: variableReady });
	const [activeShuttles, setActiveShuttles] = useStateWithStorage('tools/activeShuttles', undefined);

	const [considerActive, setConsiderActive] = useStateWithStorage(props.helperId+'/considerActive', true);
	const [considerVoyage, setConsiderVoyage] = useStateWithStorage(props.helperId+'/considerVoyage', false);
	const [considerFrozen, setConsiderFrozen] = useStateWithStorage(props.helperId+'/considerFrozen', false);

	const [loadState, setLoadState] = React.useState(0);
	const [calcState, setCalcState] = React.useState(0);
	const [crewScores, setCrewScores] = React.useState(new CrewScores());
	const [activeStep, setActiveStep] = React.useState('missions');

	React.useEffect(() => {
		setCrewScores(new CrewScores());
	}, [props.crew, considerActive, considerVoyage, considerFrozen]);

	// Prune old shuttles from stored values, import open shuttles from player data
	React.useEffect(() => {
		if (loadState === 2) initializeShuttlers();
	}, [loadState]);

	// Prune assignments from other events, dismissed shuttles
	//	recommendShuttlers will prune assignments from other events anyway
	React.useEffect(() => {
		if (loadState === 2) {
			const assignable = shuttlers.shuttles.filter(shuttle => shuttle.groupId === props.groupId && shuttle.priority > 0).map(shuttle => shuttle.id);
			const newAssigned = assigned.filter(seat => assignable.includes(seat.shuttleId));
			setAssigned([...newAssigned]);
		}
	}, [shuttlers]);

	if (loadState < 2) return (<><Icon loading name='spinner' /> Loading...</>);

	if (calcState === 1) updateAssignments();

	return (
		<React.Fragment>
			<Form>
				<Form.Group grouped>
					<Form.Field
						control={Checkbox}
						label='Consider crew on active shuttles'
						checked={considerActive}
						onChange={(e, { checked }) => setConsiderActive(checked)}
					/>
					<Form.Field
						control={Checkbox}
						label='Consider crew on active voyage'
						checked={considerVoyage}
						onChange={(e, { checked }) => setConsiderVoyage(checked)}
					/>
					<Form.Field
						control={Checkbox}
						label='Consider frozen (vaulted) crew'
						checked={considerFrozen}
						onChange={(e, { checked }) => setConsiderFrozen(checked)}
					/>
				</Form.Group>
			</Form>
			<Step.Group>
				<Step active={activeStep === 'missions'} onClick={() => setActiveStep('missions')}>
					<Icon name='list' />
					<Step.Content>
						<Step.Title>Mission List</Step.Title>
						<Step.Description>Select your preferred missions</Step.Description>
					</Step.Content>
				</Step>
				<Step active={activeStep === 'assignments'} onClick={() => setActiveStep('assignments')}>
					<Icon name='rocket' />
					<Step.Content>
						<Step.Title>Shuttle Assignments</Step.Title>
						<Step.Description>See the best seats for your crew</Step.Description>
					</Step.Content>
				</Step>
			</Step.Group>
			{activeStep === 'missions' && (
				<MissionsList groupId={props.groupId}
					setActiveStep={setActiveStep} recommendShuttlers={recommendShuttlers}
					shuttlers={shuttlers} setShuttlers={setShuttlers} activeShuttles={activeShuttles} />
			)}
			{activeStep === 'assignments' && (
				<AssignmentsList groupId={props.groupId} crew={props.crew}
					setActiveStep={setActiveStep} recommendShuttlers={recommendShuttlers}
					shuttlers={shuttlers} setShuttlers={setShuttlers} assigned={assigned} setAssigned={setAssigned}
					crewScores={crewScores} updateCrewScores={updateCrewScores} />
			)}
		</React.Fragment>
	);

	function variableReady(keyName: string): void {
		setLoadState(prevState => Math.min(prevState + 1, 2));
	}

	function initializeShuttlers(): void {
		// Prune old shuttles
		const DAYS_TO_EXPIRE = 14;
		const expireDate = new Date();
		expireDate.setDate(expireDate.getDate()-DAYS_TO_EXPIRE);

		const oldIds = [];
		shuttlers.shuttles.forEach(shuttle => {
			if (!shuttle.groupId || shuttle.created < expireDate.getTime())
				oldIds.push(shuttle.id);
		});
		oldIds.forEach(shuttleId => {
			const shuttleNum = shuttlers.shuttles.findIndex(shuttle => shuttle.id === shuttleId);
			shuttlers.shuttles.splice(shuttleNum, 1);
		});

		// Import missions from player data (during an active event, if specified)
		if (activeShuttles && (!props.eventData || props.eventData.seconds_to_start === 0)) {
			activeShuttles.forEach(adventure => {
				if (!shuttlers.shuttles.find(shuttle => shuttle.id === adventure.symbol)) {
					const shuttle = new Shuttle(props.groupId, adventure.symbol, true);
					shuttle.name = adventure.name;
					shuttle.faction = adventure.faction_id;
					adventure.shuttles[0].slots.forEach(slot => {
						const seat = new ShuttleSeat();
						if (slot.skills.length > 1) {
							seat.operand = 'OR';
							seat.skillA = slot.skills[0];
							seat.skillB = slot.skills[1];
						}
						else {
							const skills = slot.skills[0].split(',');
							seat.skillA = skills[0];
							if (skills.length > 1) seat.skillB = skills[1];
						}
						shuttle.seats.push(seat);
					});
					shuttlers.shuttles.push(shuttle);
				}
			});
		}
		setShuttlers({...shuttlers});
		if (shuttlers.shuttles.filter(shuttle => shuttle.groupId === props.groupId && shuttle.priority > 0).length > 0)
			setActiveStep('assignments');
	}

	function recommendShuttlers(): void {
		if (calcState > 0) return;

		const todo = [], todoIds = [];
		shuttlers.shuttles.filter(shuttle => shuttle.groupId === props.groupId && shuttle.priority > 0).forEach(shuttle => {
			for (let seatNum = 0; seatNum < shuttle.seats.length; seatNum++) {
				const seat = shuttle.seats[seatNum];
				if (seat.skillA === '' && seat.skillB === '') continue;
				const ssId = getSkillSetId(seat);
				if (!crewScores.skillsets[ssId] && !todoIds.includes(ssId)) {
					todo.push(seat);
					todoIds.push(ssId);
				}
			}
		});
		if (todo.length > 0) {
			setCalcState(1);
			updateCrewScores(todo);
			return;
		}

		updateAssignments();
	}

	function updateCrewScores(todo: ShuttleSeat[] = [], doneCallback?: () => void): void {
		const newSkills = {};
		const newScores = [];

		for (let i = 0; i < props.crew.length; i++) {
			if (!considerActive && props.crew[i].active_status === 2)
				continue;

			if (!considerVoyage && props.crew[i].active_status === 3)
				continue;

			if (!considerFrozen && props.crew[i].immortal > 0)
				continue;

			todo.forEach(seat => {
				const skillOperand = seat.operand;
				const primarySkill = seat.skillA;
				const secondarySkill = seat.skillB;
				const ssId = getSkillSetId(seat);

				let iHigherSkill = 0, iLowerSkill = 0;
				for (let skill in CONFIG.SKILLS) {
					if (skill !== primarySkill && skill !== secondarySkill) continue;
					if (props.crew[i][skill].core === 0) continue;

					let iMultiplier = 1;
					if (props.eventData?.featured.indexOf(props.crew[i].symbol) >= 0)
						iMultiplier = 3;
					else if (props.eventData?.bonus.indexOf(props.crew[i].symbol) >= 0)
						iMultiplier = 2;
					const iSkillScore = props.crew[i][skill].core*iMultiplier;

					if (iSkillScore > iHigherSkill) {
						iLowerSkill = iHigherSkill;
						iHigherSkill = iSkillScore;
					}
					else if (iSkillScore > iLowerSkill) {
						iLowerSkill = iSkillScore;
					}
				}

				let iSeatScore = 0;
				if (skillOperand === 'OR')
					iSeatScore = iHigherSkill;
				else
					iSeatScore = iHigherSkill+(iLowerSkill/4);

				const currentIndex = crewScores.ranked.findIndex(score => score.id === props.crew[i].id && score.ssId === ssId);
				if (currentIndex >= 0) crewScores.ranked.splice(currentIndex, 1);

				if (iSeatScore > 0) {
					const crewman = {
						id: props.crew[i].id,
						symbol: props.crew[i].symbol,
						name: props.crew[i].name,
						score: iSeatScore,
						ssId
					};
					if (!newSkills[ssId]) newSkills[ssId] = [];
					newSkills[ssId].push(crewman);
					newScores.push(crewman);
				}
			});
		}

		todo.forEach(seat => {
			const ssId = getSkillSetId(seat);
			crewScores.skillsets[ssId] = newSkills[ssId].sort((a, b) => b.score - a.score);
		});
		crewScores.ranked = crewScores.ranked.concat(newScores);
		crewScores.ranked.sort((a, b) => b.score - a.score);
		setCrewScores({...crewScores});
		if (doneCallback) doneCallback();
	}

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

		const seats = [];
		data.forEach(shuttle => {
			for (let seatNum = 0; seatNum < shuttle.seats.length; seatNum++) {
				const ssId = getSkillSetId(shuttle.seats[seatNum]);
				const newSeat = {
					shuttleId: shuttle.id,
					seatNum,
					ssId,
					assignedId: -1,
					assignedSymbol: '',
					seatScore: 0
				};
				const seated = assigned.find(seat => seat.shuttleId === shuttle.id && seat.seatNum === seatNum);
				if (seated?.locked) {
					newSeat.assignedId = seated.assignedId;
					newSeat.assignedSymbol = seated.assignedSymbol;
					newSeat.seatScore = seated.seatScore;
					newSeat.locked = true;
				}
				seats.push(newSeat);
			}
		});
		if (seats.length === 0) return;

		const scores = JSON.parse(JSON.stringify(crewScores.ranked));
		let iAssigned = 0;
		while (scores.length > 0 && iAssigned < seats.length) {
			const testScore = scores.shift();
			const alreadyAssigned = seats.find(seat => seat.assignedId === testScore.id);
			if (alreadyAssigned) continue;

			const openSeat = seats.find(seat => seat.ssId === testScore.ssId && seat.assignedId === -1);
			if (openSeat) {
				openSeat.assignedId = testScore.id;
				openSeat.assignedSymbol = testScore.symbol;
				openSeat.seatScore = testScore.score;
				iAssigned++;
			}
		}
		setAssigned([...seats]);
		setCalcState(0);
		setActiveStep('assignments');
	}
}
Example #25
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 #26
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 #27
Source File: eventplanner.tsx    From website with MIT License 4 votes vote down vote up
EventPicker = (props: EventPickerProps) => {
	const { playerData, events, crew, buffConfig, allCrew } = props;

	const [eventIndex, setEventIndex] = useStateWithStorage('eventplanner/eventIndex', 0);
	const [phaseIndex, setPhaseIndex] = useStateWithStorage('eventplanner/phaseIndex', 0);
	const [prospects, setProspects] = useStateWithStorage('eventplanner/prospects', []);

	const eventsList = [];
	events.forEach((activeEvent, eventId) => {
		eventsList.push(
			{
				key: activeEvent.symbol,
				value: eventId,
				text: activeEvent.name
			}
		);
	});

	const eventData = events[eventIndex];

	const EVENT_TYPES = {
		'shuttles': 'Faction',
		'gather': 'Galaxy',
		'skirmish': 'Skirmish'
	};

	const phaseList = [];
	eventData.content_types.forEach((contentType, phaseId) => {
		if (!phaseList.find((phase) => phase.key == contentType)) {
			phaseList.push(
				{
					key: contentType,
					value: phaseId,
					text: EVENT_TYPES[contentType]
				}
			);
		}
	});

	const allBonusCrew = allCrew.filter((c) => eventData.bonus.indexOf(c.symbol) >= 0);
	allBonusCrew.sort((a, b)=>a.name.localeCompare(b.name));

	const myCrew = JSON.parse(JSON.stringify(crew));
	const lockable = [];

	prospects.forEach((p) => {
		let prospect = allCrew.find((c) => c.symbol == p.symbol);
		if (prospect) {
			prospect = JSON.parse(JSON.stringify(prospect));
			prospect.id = myCrew.length+1;
			prospect.prospect = true;
			prospect.have = false;
			prospect.rarity = p.rarity;
			prospect.level = 100;
			prospect.immortal = 0;
			CONFIG.SKILLS_SHORT.forEach(skill => {
				let score = { core: 0, min: 0, max: 0 };
				if (prospect.base_skills[skill.name]) {
					if (prospect.rarity == prospect.max_rarity)
						score = applySkillBuff(buffConfig, skill.name, prospect.base_skills[skill.name]);
					else
						score = applySkillBuff(buffConfig, skill.name, prospect.skill_data[prospect.rarity-1].base_skills[skill.name]);
				}
				prospect[skill.name] = score;
			});
			myCrew.push(prospect);
			lockable.push({
				symbol: prospect.symbol,
				name: prospect.name,
				rarity: prospect.rarity,
				level: prospect.level,
				prospect: prospect.prospect
			});
		}
	});

	return (
		<React.Fragment>
			<Form>
				<Form.Field
					control={Dropdown}
					selection
					options={eventsList}
					value={eventIndex}
					onChange={(e, { value }) => setEventIndex(value) }
				/>
				<Image size='large' src={`${process.env.GATSBY_ASSETS_URL}${eventData.image}`} />
				<div>{eventData.description}</div>
				{phaseList.length > 1 && (<div style={{ margin: '1em 0' }}>Select a phase: <Dropdown selection options={phaseList} value={phaseIndex} onChange={(e, { value }) => setPhaseIndex(value) } /></div>)}
			</Form>
			<EventCrewTable crew={myCrew} eventData={eventData} phaseIndex={phaseIndex} buffConfig={buffConfig} lockable={lockable} />
			<EventProspects pool={allBonusCrew} prospects={prospects} setProspects={setProspects} />
			{eventData.content_types[phaseIndex] == 'shuttles' && (<EventShuttles playerData={playerData} crew={myCrew} eventData={eventData} />)}
		</React.Fragment>
	);
}
Example #28
Source File: crewretrieval.tsx    From website with MIT License 4 votes vote down vote up
RetrievalForm = (props: RetrievalFormProps) => {
	const { ownedPolestars, allCrew, allKeystones, myCrew } = props;

	const [disabledPolestars, setDisabledPolestars] = useStateWithStorage('crewretrieval/disabledPolestars', []);
	const [addedPolestars, setAddedPolestars] = useStateWithStorage('crewretrieval/addedPolestars', []);
	const [ownedFilter, setOwnedFilter] = useStateWithStorage('crewretrieval/ownedFilter', ownedFilterOptions[0].value);
	const [minRarity, setMinRarity] = useStateWithStorage('crewretrieval/minRarity', null);
	const [collection, setCollection] = useStateWithStorage('crewretrieval/collection', null);

	const [polestars, setPolestars] = React.useState([]);
	const [data, setData] = React.useState(null);

	// Update polestar list on filter, prospect change
	React.useEffect(() => {
		let filtered = JSON.parse(JSON.stringify(ownedPolestars));
		filtered = filtered.filter((p) => disabledPolestars.indexOf(p.id) === -1);
		addedPolestars.forEach((prospect) => {
			let existing = filtered.find(polestar => polestar.symbol === prospect);
			if (existing) {
				existing.loaned = existing.loaned ? existing.loaned + 1 : 1;
				existing.quantity++;
			}
			else {
				let keystone = allKeystones.find(k => k.symbol === prospect);
				filtered.push({...keystone, quantity: 1, loaned: 1});
			}
		});
		setPolestars([...filtered]);
	}, [disabledPolestars, addedPolestars]);

	// Update dataset on any form change
	React.useEffect(() => {
		let retrievable = allCrew.filter(
			(crew) => crew.unique_polestar_combos?.some(
				(upc) => upc.every(
					(trait) => polestars.some(op => filterTraits(op, trait))
				)
			)
		);

		retrievable = retrievable.filter(ownedFilters[ownedFilter](myCrew));
		if (ownedFilterOptions[2].value === ownedFilter) {
			retrievable = retrievable.filter(crew => crew.max_rarity > crew.highest_owned_rarity);
		}

		if (minRarity) {
			retrievable = retrievable.filter((crew) => crew.max_rarity >= minRarity);
		}

		if (collection) {
			retrievable = retrievable.filter((crew) => crew.collections.indexOf(collection) !== -1);
		}

		setData([...retrievable]);
	}, [polestars, ownedFilter, minRarity, collection]);

	return (
		<React.Fragment>
			<p>Here are all the crew who you can perform a 100% guaranteed crew retrieval for, using the polestars in your inventory:</p>
			<Form>
				<Form.Group inline>
					<PolestarFilterModal ownedPolestars={ownedPolestars} disabledPolestars={disabledPolestars} updateDisableds={setDisabledPolestars} />
					<PolestarProspectModal ownedPolestars={ownedPolestars} addedPolestars={addedPolestars} allKeystones={allKeystones} allCrew={allCrew} updateProspects={setAddedPolestars} />
					<Form.Field
						control={Dropdown}
						selection
						options={ownedFilterOptions}
						value={ownedFilter}
						onChange={(e, { value }) => setOwnedFilter(value)}
					/>
					<Form.Field
						control={Dropdown}
						placeholder="Minimum rarity"
						selection
						options={rarityOptions}
						value={minRarity}
						onChange={(e, { value }) => setMinRarity(value)}
					/>
					<Form.Field
						control={Dropdown}
						placeholder="Collections"
						selection
						options={collectionsOptions}
						value={collection}
						onChange={(e, { value }) => setCollection(value)}
					/>
				</Form.Group>
			</Form>
			<CrewTable data={data} polestars={polestars} />
		</React.Fragment>
	);
}
Example #29
Source File: collectionstool.tsx    From website with MIT License 4 votes vote down vote up
ProgressTable = (props: ProgressTableProps) => {
	const { playerCollections, filterCrewByCollection } = props;

	const [rewardFilter, setRewardFilter] = useStateWithStorage('collectionstool/rewardFilter', undefined);
	const [showMaxed, setShowMaxed] = useStateWithStorage('collectionstool/showMaxed', false);

	const tableConfig: ITableConfigRow[] = [
		{ width: 2, column: 'name', title: 'Collection' },
		{ width: 1, column: 'owned', title: 'Total Owned', reverse: true },
		{ width: 1, column: 'progressPct', title: 'Progress', reverse: true },
		{ width: 1, column: 'needed', title: 'Needed', tiebreakers: ['neededPct'] },
		{ width: 3, column: 'totalRewards', title: <span>Milestone Rewards <Popup trigger={<Icon name='help' />} content='Rewards you can claim after immortalizing the needed number of crew to reach the next milestone' /></span>, reverse: true }
	];

	// Rewards will test value against literal symbol string, except when prefixed by:
	//	= Regular expression against symbol, * Special test case
	const rewardOptions = [
		{ key: 'roAnyr', value: '*any', text: 'Any reward' },
		{ key: 'roBuff', value: '*buffs', text: 'Buffs' },
		{ key: 'roEner', value: 'energy', text: 'Chronitons' },
		{ key: 'roCred', value: 'nonpremium', text: 'Credits' },
		{ key: 'roCrew', value: '=_crew$', text: 'Crew' },
		{ key: 'roDili', value: 'premium_purchasable', text: 'Dilithium' },
		{ key: 'roHono', value: 'honor', text: 'Honor' },
		{ key: 'roMeri', value: 'premium_earnable', text: 'Merits' },
		{ key: 'roPort', value: '=premium_\\d+x_bundle', text: 'Portals' },
		{ key: 'roRepl', value: '=^replicator_fuel', text: 'Replicator Fuel' },
		{ key: 'roSche', value: '=_ship_schematic$', text: 'Ship schematics' },
		{ key: 'roBoos', value: '=minor_consumables_\\d+x_bundle', text: 'Shuttle boosts' },
		{ key: 'roTrai', value: '=_production_training$', text: 'Training' }
	];

	return (
		<React.Fragment>
			<p>Search for collections by name or description. You can also filter collections by milestone reward types. Click a row to view crew that will help you make progress on that collection.</p>
			<div style={{ margin: '.5em 0' }}>
				<Form>
					<Form.Group inline>
						<Form.Field
							control={Dropdown}
							placeholder='Filter by reward'
							selection
							clearable
							options={rewardOptions}
							value={rewardFilter}
							onChange={(e, { value }) => setRewardFilter(value)}
						/>
						<Form.Field
							control={Checkbox}
							label='Show maxed collections'
							checked={showMaxed}
							onChange={(e, { checked }) => setShowMaxed(checked)}
						/>
					</Form.Group>
				</Form>
			</div>
			<SearchableTable
				id='collections/progress'
				data={playerCollections}
				config={tableConfig}
				renderTableRow={(collection, idx) => renderCollectionRow(collection, idx)}
				filterRow={(collection, filter) => showCollectionRow(collection, filter)}
				explanation={
					<div>
						<p>Search for collections by name or trait.</p>
					</div>
				}
			/>
		</React.Fragment>
	);

	function showCollectionRow(collection: any, filters: []): boolean {
		if (!showMaxed && collection.milestone.goal == 0) return false;

		if (rewardFilter && rewardFilter != '*any') {
			let re;
			if (rewardFilter == '*buffs') {
				if (collection.milestone.buffs.length == 0) return false;
			}
			else if (rewardFilter.substr(0, 1) == '=') {
				re = new RegExp(rewardFilter.substr(1));
				if (!collection.milestone.rewards.find(reward => re.test(reward.symbol))) return false;
			}
			else if (!collection.milestone.rewards.find(reward => reward.symbol == rewardFilter)) {
				return false;
			}
		}

		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(collection.name, segment.text) ||
						matchesFilter(collection.simpleDescription, segment.text) ||
						collection.traits?.some(t => matchesFilter(t, segment.text));
					meetsAllConditions = meetsAllConditions && (segment.negated ? !segmentResult : segmentResult);
				}
			}
			if (meetsAllConditions) {
				meetsAnyCondition = true;
				break;
			}
		}

		return meetsAnyCondition;
	}

	function renderCollectionRow(collection: any, idx: number): JSX.Element {
		const rewards = collection.totalRewards > 0 ? collection.milestone.buffs.concat(collection.milestone.rewards) : [];

		return (
			<Table.Row key={collection.id} style={{ cursor: 'zoom-in' }} onClick={() => filterCrewByCollection(collection.id)}>
				<Table.Cell>
					<span style={{ fontWeight: 'bolder', fontSize: '1.25em' }}><Link to={`/collections/#${encodeURI(collection.name)}`}>{collection.name}</Link></span>
					<br/>{collection.simpleDescription}
				</Table.Cell>
				<Table.Cell textAlign='center'>{collection.owned} / {collection.crew.length}</Table.Cell>
				<Table.Cell textAlign='center'>{collection.milestone.goal > 0 ? `${collection.progress} / ${collection.milestone.goal}` : 'MAX'}</Table.Cell>
				<Table.Cell textAlign='center'>{collection.needed}</Table.Cell>
				<Table.Cell textAlign='center'>
					<RewardsGrid rewards={rewards} />
				</Table.Cell>
			</Table.Row>
		);
	}
}