semantic-ui-react#Popup TypeScript Examples

The following examples show how to use semantic-ui-react#Popup. 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: topmenu.tsx    From website with MIT License 6 votes vote down vote up
useRightItems = ({ onMessageClicked }) => {
	return (<>
		<Menu.Item onClick={() => (window as any).swapThemeCss()}>
			<Icon name='adjust' />
		</Menu.Item>
		<Menu.Item>
			<Popup position='bottom center' flowing hoverable trigger={<Icon name='dollar' />}>
				<p>We have enough reserve funds for now!</p>
				<p>
					Monthly cost <b>$15</b>, reserve fund <b>$205</b>
				</p>
				<p>
					You can join our <a href='https://www.patreon.com/Datacore'>Patreon</a> for future funding rounds.
				</p>
			</Popup>
		</Menu.Item>
		<Menu.Item>
			<Button size='tiny' color='green' onClick={onMessageClicked} content={'Developers needed!'} />
		</Menu.Item>
		<Menu.Item onClick={() => window.open('https://github.com/stt-datacore/website', '_blank')}>
			<Icon name='github' />
		</Menu.Item>
	</>);
}
Example #2
Source File: crewpopup.tsx    From website with MIT License 6 votes vote down vote up
render() {
		const { crew, useBase } = this.props;
		//console.log(crew);
		if (!crew || !crew.symbol) {
			return <span>ERROR!</span>;
		}

		return (
			<Popup trigger={<span style={{ cursor: 'help', fontWeight: 'bolder' }}>{crew.name}</span>}>
				<Popup.Header>{crew.name}</Popup.Header>
				<Popup.Content>
					<Image size='small' src={`${process.env.GATSBY_ASSETS_URL}${crew.imageUrlPortrait}`} />
					<Rating icon='star' defaultRating={crew.rarity} maxRating={crew.max_rarity} />
					<p>{formatCrewStats(crew, useBase ?? true)}</p>
				</Popup.Content>
			</Popup>
		);
	}
Example #3
Source File: TopBar.tsx    From watchparty with MIT License 6 votes vote down vote up
render() {
    return (
      <Popup
        content="Create a new room with a random URL that you can share with friends"
        trigger={
          <Button
            color="blue"
            size={this.props.size as any}
            icon
            labelPosition="left"
            onClick={this.createRoom}
            className="toolButton"
            fluid
          >
            <Icon name="certificate" />
            New Room
          </Button>
        }
      />
    );
  }
Example #4
Source File: PrereqTree.tsx    From peterportal-client with MIT License 6 votes vote down vote up
Node: FC<NodeProps> = (props) => {
  return (
    <div style={{ padding: '1px 0' }} className={`node-container ${props.node}`} key={props.index}>
      <Popup
        trigger={
          <a href={'/course/' + props.label.replace(/\s+/g, '')} role='button' style={{ padding: '0.5rem' }} className={'node ui button'}>
            {props.label}
          </a>
        }
        content={props.content} basic position='top center' wide='very' />
    </div>
  );
}
Example #5
Source File: SpeakerControl.tsx    From FLECT_Amazon_Chime_Meeting with Apache License 2.0 5 votes vote down vote up
render() {
        const props = this.props as any
        const gs = this.props as GlobalState
        const appState = props.appState as AppState
        const outputAudioDevicesOpts=gs.outputAudioDevices!.map(info => { return { key: info.label, text: info.label, value: info.deviceId } })

        const enableIcon=appState.currentSettings.speakerEnable ?
        (
            <Popup
            trigger={
                <Icon size="large" name="sound"  color="black" link onClick={() => { props.toggleSpeaker() }}/>
            }
            content="disable."
            />
        )
        :
        (
            <Popup
            trigger={
                <Icon.Group link onClick={() => { props.toggleSpeaker() }}>
                    <Icon size="large" color='black' name='sound' />
                    <Icon size="large" color='red' name='dont' />
                </Icon.Group>        
            }       
            content="enable."
        />
        )

        return (

            <Grid>
                <Grid.Row>
                    <Grid.Column >
                    {enableIcon}
                    <Dropdown
                        style={{paddingLeft:"10px"}}
                        pointing='top left'
                        options={outputAudioDevicesOpts}
                        trigger={trigger}
                        onChange={(e, { value }) => props.selectOutputAudioDevice(value as string)}
                    />
                        {/* <List style={{paddingLeft:"15px",paddingTop:"0px",paddingBottom:"0px"}} link>
                            <List.Item as='a' active onClick={() => { props.toggleSpeaker() }}><Icon name="ban" color={appState.currentSettings.speakerEnable ? "grey" : "red"}/>Disable Speaker</List.Item>
                        </List>  */}


                    </Grid.Column>
                </Grid.Row>
            </Grid>

        )
    }
Example #6
Source File: crewpage.tsx    From website with MIT License 5 votes vote down vote up
renderEquipmentDetails(crew) {
		if (!this.state.selectedEquipment) {
			return <span />;
		}

		let es = crew.equipment_slots.find(es => es.symbol === this.state.selectedEquipment);
		let equipment = this.state.items.find(item => item.symbol === es.symbol);
		if (!equipment) {
			console.error('Could not find equipment for slot', es);
			return <span />;
		}

		if (!equipment.recipe) {
			return (
				<div>
					<br />
					<p>This item is not craftable, you can find it in these sources:</p>
					<ItemSources item_sources={equipment.item_sources} />
				</div>
			);
		}

		return (
			<div>
				<Grid columns={4} centered padded>
					{equipment.recipe.list.map(entry => {
						let recipeEntry = this.state.items.find(item => item.symbol === entry.symbol);
						return (
							<Grid.Column key={recipeEntry.name + recipeEntry.rarity} textAlign='center'>
								<Popup
									trigger={
										<Label as='a' style={{ background: CONFIG.RARITIES[recipeEntry.rarity].color }} image size='big'>
											<img src={`${process.env.GATSBY_ASSETS_URL}${recipeEntry.imageUrl}`} />x{entry.count}
										</Label>
									}
									header={CONFIG.RARITIES[recipeEntry.rarity].name + ' ' + recipeEntry.name}
									content={<ItemSources item_sources={recipeEntry.item_sources} />}
									wide
								/>
							</Grid.Column>
						);
					})}
				</Grid>
			</div>
		);
	}
Example #7
Source File: bigbook2.tsx    From website with MIT License 5 votes vote down vote up
renderCrew(entry): JSX.Element {
		let markdownRemark = {
			frontmatter: {
				bigbook_tier: entry.bcrew.bigbook_tier,
				events: entry.bcrew.events,
				in_portal: entry.bcrew.in_portal
			}
		};

		return (
			<Grid.Column key={entry.crew.symbol}>
				<div style={{ textAlign: 'center', fontSize: '0.75em' }}>
					<Popup
						trigger={
							<Image
								src={`${process.env.GATSBY_ASSETS_URL}${entry.crew.imageUrlPortrait}`}
								size='small'
								style={{
									borderColor: CONFIG.RARITIES[entry.crew.max_rarity].color,
									borderWidth: '1px',
									borderRadius: '4px',
									borderStyle: 'solid'
								}}
							/>
						}
						wide='very'
						on='click'>
						<Header>
							<Link to={`/crew/${entry.crew.symbol}/`}>
								{entry.crew.name}{' '}
								<Rating rating={entry.crew.max_rarity} maxRating={entry.crew.max_rarity} icon='star' size='large' disabled />
							</Link>
						</Header>
						<CommonCrewData crew={entry.crew} markdownRemark={markdownRemark} />

						<div dangerouslySetInnerHTML={{ __html: marked(entry.bcrew.markdownContent) }} />
					</Popup>
					<Link to={`/crew/${entry.crew.symbol}/`}>{entry.crew.name}</Link>
				</div>
			</Grid.Column>
		);
	}
Example #8
Source File: voyagestats.tsx    From website with MIT License 5 votes vote down vote up
_renderCrew() {
		const {voyageData} = this.props;
		const  ship  = this.ship;

		return (
			<div>
			  {ship && (<span>Ship : <b>{ship.name}</b></span>)}
				<Grid columns={isMobile ? 1 : 2}>
					<Grid.Column>
						<ul>
							{Object.values(CONFIG.VOYAGE_CREW_SLOTS).map((entry, idx) => {
								let { crew, name }  = Object.values(voyageData.crew_slots).find(slot => slot.symbol == entry);

								if (!crew.imageUrlPortrait)
									crew.imageUrlPortrait =
										`${crew.portrait.file.substring(1).replaceAll('/', '_')}.png`;

								return (
									<li key={idx}>
										{name}
										{'  :  '}
										<CrewPopup crew={crew} useBase={false} />
										</li>
									);
								})}
							</ul>
						</Grid.Column>
						<Grid.Column verticalAlign="middle">
							<ul>
								<li>
									Antimatter
									{' : '}
									<b>{voyageData.max_hp}</b>
								</li>
							</ul>
							<ul>
								{Object.keys(CONFIG.SKILLS).map((entry, idx) => {
									const agg = voyageData.skill_aggregates[entry];

									if (typeof(agg) === 'number') {
										return (<li key={idx}>{`${CONFIG.SKILLS[entry]} : ${Math.round(agg)}`}</li>);
									} else {
										const score = Math.floor(agg.core + (agg.range_min + agg.range_max)/2);

										return (
											<li key={idx}>
												{CONFIG.SKILLS[entry]}
												{' : '}
												<Popup wide trigger={<span style={{ cursor: 'help', fontWeight: 'bolder' }}>{score}</span>}>
													<Popup.Content>
														{agg.core + ' +(' + agg.range_min + '-' + agg.range_max + ')'}
													</Popup.Content>
												</Popup>
											</li>
										);
									}
								})}
							</ul>
						</Grid.Column>
					</Grid>
				</div>
			);
	}
Example #9
Source File: voyagecalculator.tsx    From website with MIT License 5 votes vote down vote up
VoyageExisting = (props: VoyageExistingProps) => {
	const { voyageConfig, allShips, useCalc } = props;
	const [CIVASExportFailed, setCIVASExportFailed] = React.useState(false);
	const [doingCIVASExport, setDoingCIVASExport] = React.useState(false);

	const hoursToTime = hours => {
		let wholeHours = Math.floor(hours);
		return `${wholeHours}:${Math.floor((hours-wholeHours)*60).toString().padStart(2, '0')}`
	}

	const exportData = () => new Promise((resolve, reject) => {
		setDoingCIVASExport(true);

		let estimate = getEstimate({
			startAm: voyageConfig.max_hp,
			ps: voyageConfig.skill_aggregates[voyageConfig.skills['primary_skill']],
			ss: voyageConfig.skill_aggregates[voyageConfig.skills['secondary_skill']],
			others: Object.values(voyageConfig.skill_aggregates).filter(s => !Object.values(voyageConfig.skills).includes(s.skill)),
		}, () => true).refills[0].result;

		let values = [
			new Date(voyageConfig.created_at).toLocaleDateString(),
			hoursToTime(estimate),
			hoursToTime(voyageConfig.log_index/180),
			voyageConfig.hp
		];

		values = values.concat(voyageConfig
			.crew_slots
			.sort((s1, s2) => CONFIG.VOYAGE_CREW_SLOTS.indexOf(s1.symbol) - CONFIG.VOYAGE_CREW_SLOTS.indexOf(s2.symbol))
			.map(s => s.crew.name)
		);

		navigator.clipboard.writeText(values.join('\n')).then(resolve, reject);
	});

	return (
		<div style={{ marginTop: '1em' }}>
			<VoyageStats
				voyageData={voyageConfig}
				ships={allShips}
				showPanels={voyageConfig.state == 'started' ? ['estimate'] : ['rewards']}
			/>
			<Button onClick={() => useCalc()}>Return to crew calculator</Button>
			{(voyageConfig.state == 'recalled' || voyageConfig.state == 'failed') && navigator.clipboard &&
				<React.Fragment>
					<Button loading={doingCIVASExport} onClick={() => exportData().then(
						() => setDoingCIVASExport(false),
						() => {
							setDoingCIVASExport(false);
							setCIVASExportFailed(true);

							let timeout = setTimeout(() => {
								setCIVASExportFailed(false);
								clearTimeout(timeout);
							}, 5000);
						})}>
						Export to CIVAS
					</Button>
					<Popup
						trigger={<Icon name='help' />}
						content={
							<>
								Copies details of the voyage to the clipboard so that it can be pasted into <a href='https://docs.google.com/spreadsheets/d/1nbnD2WvDXAT9cxEWep0f78bv6_hOaP51tmRjmY0oT1k' target='_blank'>Captain Idol's Voyage Analysis Sheet</a>
							</>
						}
						mouseLeaveDelay={1000}
					/>
					{CIVASExportFailed &&
						<Message negative>Export to clipboard failed</Message>
					}
				</React.Fragment>
			}
		</div>
	)
}
Example #10
Source File: crewfullequiptree.tsx    From website with MIT License 5 votes vote down vote up
render() {
		const { crew, items } = this.props;

		if (!crew || !this.props.visible) {
			return <span />;
		}

		let { craftCost, demands, factionOnlyTotal, totalChronCost } = calculateCrewDemands(crew, items);

		return (
			<Modal open={this.props.visible} onClose={() => this.props.onClosed()}>
				<Modal.Header>{crew.name}'s expanded equipment recipe trees</Modal.Header>
				<Modal.Content scrolling>
					<p>
						Faction-only items required <b>{factionOnlyTotal}</b>
					</p>
					<p>
						Estimated chroniton cost{' '}
						<span style={{ display: 'inline-block' }}>
							<img src={`${process.env.GATSBY_ASSETS_URL}atlas/energy_icon.png`} height={14} />
						</span>{' '}
						<b>{totalChronCost}</b>
						<Popup
							wide
							trigger={<Icon fitted name='help' />}
							header={'How is this calculated?'}
							content={
								<div>
									<p>This sums the estimated chroniton cost of each equipment and component in the tree.</p>
									<p>It estimates an item's cost by running the formula below for each mission and choosing the cheapest:</p>
									<p>
										<code>
											(6 - PIPS) * 1.8 * <i>mission cost</i>
										</code>
									</p>
									<p>See code for details. Feedback is welcome!</p>
								</div>
							}
						/>
					</p>
					<p>
						Build cost{' '}
						<span style={{ display: 'inline-block' }}>
							<img src={`${process.env.GATSBY_ASSETS_URL}currency_sc_currency_0.png`} height={16} />
						</span>{' '}
						<b>{craftCost}</b>
					</p>
					<Grid columns={3} centered padded>
						{demands.map((entry, idx) => (
							<Grid.Column key={idx}>
								<Popup
									trigger={
										<Header
											style={{ display: 'flex', cursor: 'zoom-in' }}
											icon={
												<ItemDisplay
													src={`${process.env.GATSBY_ASSETS_URL}${entry.equipment.imageUrl}`}
													size={48}
													maxRarity={entry.equipment.rarity}
													rarity={entry.equipment.rarity}
												/>
											}
											content={entry.equipment.name}
											subheader={`Need ${entry.count} ${entry.factionOnly ? ' (FACTION)' : ''}`}
										/>
									}
									header={CONFIG.RARITIES[entry.equipment.rarity].name + ' ' + entry.equipment.name}
									content={<ItemSources item_sources={entry.equipment.item_sources} />}
									on='click'
									wide
								/>
							</Grid.Column>
						))}
					</Grid>
				</Modal.Content>
			</Modal>
		);
	}
Example #11
Source File: cabexplanation.tsx    From website with MIT License 5 votes vote down vote up
CABExplanation = () => (
  <Popup wide trigger={<Icon name="help" />} header={'CAB STT Power Ratings'} content={text}/>
)
Example #12
Source File: MicControl.tsx    From FLECT_Amazon_Chime_Meeting with Apache License 2.0 5 votes vote down vote up
render() {
        const props = this.props as any
        const gs = this.props as GlobalState
        const appState = props.appState as AppState
        const inputAudioDevicesOpts=gs.inputAudioDevices!.map(info => { return { key: info.label, text: info.label, value: info.deviceId } })

        const muteIcon=appState.currentSettings.mute ?
        (
            <Popup
            trigger={
                <Icon.Group link onClick={() => { props.toggleMute() }}>
                    <Icon size="large" color='black' name='microphone' />
                    <Icon size="large" color='red' name='dont' />
                </Icon.Group>        
            }       
            content="unmute."
            />
        )
        :
        (
            <Popup
            trigger={
                <Icon size="large" name="microphone"  color="black" link onClick={() => { props.toggleMute() }}/>
            }
            content="mute."
            />
        )

        return (
            <Grid>
                <Grid.Row>
                    <Grid.Column >
                    {muteIcon}
                    <Dropdown
                        style={{paddingLeft:"10px"}}
                        pointing='top left'
                        options={inputAudioDevicesOpts}
                        trigger={trigger}
                        onChange={(e, { value }) => props.selectInputAudioDevice(value as string)}
                    />

                        {/* <List style={{paddingLeft:"15px",paddingTop:"0px",paddingBottom:"0px"}} link>
                            <List.Item as='a' active onClick={() => { props.toggleMute() }}><Icon name="ban" color={appState.currentSettings.mute ? "red" : "grey"} />Mute</List.Item>
                        </List>  */}



                    </Grid.Column>
                </Grid.Row>
            </Grid>            


        )
    }
Example #13
Source File: TopBar.tsx    From watchparty with MIT License 5 votes vote down vote up
render() {
    if (this.props.user) {
      return (
        <div
          style={{
            margin: '4px',
            width: '100px',
            alignItems: 'center',
            cursor: 'pointer',
          }}
        >
          <Image
            avatar
            src={this.state.userImage}
            onClick={() => this.setState({ isProfileOpen: true })}
          />
          {this.state.isProfileOpen && this.props.user && (
            <ProfileModal
              user={this.props.user}
              userImage={this.state.userImage}
              close={() => this.setState({ isProfileOpen: false })}
            />
          )}
        </div>
      );
    }
    return (
      <React.Fragment>
        {this.state.isLoginOpen && (
          <LoginModal
            closeLogin={() => this.setState({ isLoginOpen: false })}
          />
        )}
        <Popup
          basic
          content="Sign in to set your name and picture, subscribe, or launch VBrowsers"
          trigger={
            <Dropdown
              style={{ height: '36px' }}
              icon="sign in"
              labeled
              className="icon"
              button
              text="Sign in"
              fluid={this.props.fluid}
            >
              <Dropdown.Menu>
                <Dropdown.Item onClick={this.facebookSignIn}>
                  <Icon name="facebook" />
                  Facebook
                </Dropdown.Item>
                <Dropdown.Item onClick={this.googleSignIn}>
                  <Icon name="google" />
                  Google
                </Dropdown.Item>
                <Dropdown.Item
                  onClick={() => this.setState({ isLoginOpen: true })}
                >
                  <Icon name="mail" />
                  Email
                </Dropdown.Item>
              </Dropdown.Menu>
            </Dropdown>
          }
        />
      </React.Fragment>
    );
  }
Example #14
Source File: SubscribeButton.tsx    From watchparty with MIT License 5 votes vote down vote up
SubscribeButton = ({
  user,
  isSubscriber,
  isCustomer,
}: {
  user: firebase.User | undefined;
  isSubscriber: boolean;
  isCustomer: boolean;
}) => {
  const [isSubscribeModalOpen, setIsSubscribeModalOpen] = useState(false);
  const onManage = useCallback(async () => {
    const resp = await window.fetch(serverPath + '/manageSub', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        uid: user?.uid,
        token: await user?.getIdToken(),
        return_url: window.location.href,
      }),
    });
    const session = await resp.json();
    console.log(session);
    window.location.assign(session.url);
  }, [user]);
  return (
    <>
      {isSubscribeModalOpen && (
        <SubscribeModal
          user={user}
          isSubscriber={isSubscriber}
          closeSubscribe={() => setIsSubscribeModalOpen(false)}
        />
      )}
      {!isSubscriber && (
        <Popup
          content="Subscribe to help support us and enable additional features!"
          trigger={
            <Button
              fluid
              color="orange"
              className="toolButton"
              icon
              labelPosition="left"
              onClick={() => setIsSubscribeModalOpen(true)}
            >
              <Icon name="plus" />
              Subscribe
            </Button>
          }
        />
      )}
      {isSubscriber && (
        <Popup
          content="Manage your subscription"
          trigger={
            <Button
              fluid
              color="orange"
              className="toolButton"
              icon
              labelPosition="left"
              onClick={onManage}
            >
              <Icon name="wrench" />
              Manage
            </Button>
          }
        />
      )}
    </>
  );
}
Example #15
Source File: VideoControl.tsx    From FLECT_Amazon_Chime_Meeting with Apache License 2.0 5 votes vote down vote up
render() {
        const props = this.props as any
        const gs = this.props as GlobalState
        const appState = props.appState as AppState
        const inputVideoDevicesOpts=gs.inputVideoDevices!.map(info => { return { key: info.label, text: info.label, value: info.deviceId } })

        const enableIcon=appState.currentSettings.videoEnable ?
        (
            <Popup
            trigger={
                <Icon size="large" name="video camera"  color="black" link onClick={() => { props.toggleVideo() }}/>
            }
            content="disable."
            />
        )
        :
        (
            <Popup
            trigger={
                <Icon.Group link onClick={() => { props.toggleVideo() }}>
                    <Icon size="large" color='black' name='video camera' />
                    <Icon size="large" color='red' name='dont' />
                </Icon.Group>        
            }       
            content="enable."
        />
        )
        
        return (
            <Grid>
                <Grid.Row>
                    <Grid.Column>

                    {enableIcon}
                    
                    <Dropdown
                        style={{paddingLeft:"10px"}}
                        pointing='top left'
                        options={inputVideoDevicesOpts}
                        trigger={trigger}
                        onChange={(e, { value }) => props.selectInputVideoDevice(value as string)}
                    />
                        {/* <List style={{paddingLeft:"15px",paddingTop:"0px",paddingBottom:"0px"}} link>
                            <List.Item as='a' active onClick={() => { props.toggleVideo() }}><Icon name="ban" color={appState.currentSettings.videoEnable ? "grey" : "red"}/>Disable Camera</List.Item>
                        </List>  */}
                    </Grid.Column>
                </Grid.Row>
                <Grid.Row>
                    <Grid.Column>
                        <List link>
                            <List.Item as='a' active onClick={() => this.fileInputRef.current!.click()}>
                                <Icon name="folder"  active />dummy mov.
                            </List.Item>
                        </List>
                        <input
                            ref={this.fileInputRef}
                            type="file"
                            hidden
                            onChange={(e) => props.setSelectedVideo(e)}
                        />

                    </Grid.Column>
                </Grid.Row>


            </Grid>

        )
    }
Example #16
Source File: searchabletable.tsx    From website with MIT License 4 votes vote down vote up
SearchableTable = (props: SearchableTableProps) => {
	let data = [...props.data];
	const tableId = props.id ?? '';

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

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

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

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

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

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

		const lastColumn = column, lastDirection = direction;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		console.log(item_data);

		let bonusText = [];
		if (item_data.item.bonuses) {
			for (let [key, value] of Object.entries(item_data.item.bonuses)) {
				let bonus = CONFIG.STATS_CONFIG[Number.parseInt(key)];
				if (bonus) {
					bonusText.push(`+${value} ${bonus.symbol}`);
				} else {
					// TODO: what kind of bonus is this?
				}
			}
		}

		// TODO: share this code with equipment.ts
		let demands = [];
		if (item_data.item.recipe) {
			for (let iter of item_data.item.recipe.list) {
				let recipeEquipment = items.find(item => item.symbol === iter.symbol);
				demands.push({
					count: iter.count,
					symbol: iter.symbol,
					equipment: recipeEquipment,
					factionOnly: iter.factionOnly
				});
			}
		}

		return (
			<Layout title={item_data.item.name}>
				<Message icon warning>
					<Icon name="exclamation triangle" />
					<Message.Content>
						<Message.Header>Work in progress!</Message.Header>
							This section is under development and not fully functional yet.
						</Message.Content>
				</Message>
				<Header as="h3">
					{item_data.item.name}{' '}
					<Rating icon='star' rating={item_data.item.rarity} maxRating={item_data.item.rarity} size="large" disabled />
				</Header>
				<Image size="small" src={`${process.env.GATSBY_ASSETS_URL}${item_data.item.imageUrl}`} />

				<br />

				{bonusText.length > 0 && (
					<div>
						<p>Bonuses: {bonusText.join(', ')}</p>
						<br />
					</div>
				)}

				{item_data.item.recipe && item_data.item.recipe.list && (
					<div>
						<Header as="h4">Craft it for {item_data.item.recipe.craftCost} chrons using this recipe:</Header>
						<Grid columns={3} padded>
							{demands.map((entry, idx) => (
								<Grid.Column key={idx}>
									<Popup
										trigger={
											<Header
												style={{ display: 'flex', cursor: 'zoom-in' }}
												icon={
													<ItemDisplay
														src={`${process.env.GATSBY_ASSETS_URL}${entry.equipment.imageUrl}`}
														size={48}
														maxRarity={entry.equipment.rarity}
														rarity={entry.equipment.rarity}
													/>
												}
												content={entry.equipment.name}
												subheader={`Need ${entry.count} ${entry.factionOnly ? ' (FACTION)' : ''}`}
											/>
										}
										header={
											<Link to={`/item_info?symbol=${entry.symbol}`}>
												{CONFIG.RARITIES[entry.equipment.rarity].name + ' ' + entry.equipment.name}
											</Link>
										}
										content={<ItemSources item_sources={entry.equipment.item_sources} />}
										on="click"
										wide
									/>
								</Grid.Column>
							))}
						</Grid>
					</div>
				)}

				{item_data.item.item_sources.length > 0 && (
					<div>
						<Header as="h4">Item sources</Header>
						<ItemSources item_sources={item_data.item.item_sources} />
						<br />
					</div>
				)}

				{item_data.crew_levels.length > 0 && (
					<div>
						<Header as="h4">Equippable by this crew:</Header>
						<Grid columns={3} padded>
							{item_data.crew_levels.map((entry, idx) => (
								<Grid.Column key={idx}>
									<Header
										style={{ display: 'flex' }}
										icon={
											<ItemDisplay
												src={`${process.env.GATSBY_ASSETS_URL}${entry.crew.imageUrlPortrait}`}
												size={60}
												maxRarity={entry.crew.max_rarity}
												rarity={entry.crew.max_rarity}
											/>
										}
										content={<Link to={`/crew/${entry.crew.symbol}/`}>{entry.crew.name}</Link>}
										subheader={`Level ${entry.level}`}
									/>
								</Grid.Column>
							))}
						</Grid>
					</div>
				)}

				{item_data.builds.length > 0 && (
					<div>
						<Header as="h4">Is used to build these</Header>
						<Grid columns={3} padded>
							{item_data.builds.map((entry, idx) => (
								<Grid.Column key={idx}>
									<Header
										style={{ display: 'flex', cursor: 'zoom-in' }}
										icon={
											<ItemDisplay
												src={`${process.env.GATSBY_ASSETS_URL}${entry.imageUrl}`}
												size={48}
												maxRarity={entry.rarity}
												rarity={entry.rarity}
											/>
										}
										content={
											<Link to={`/item_info?symbol=${entry.symbol}`}>
												{CONFIG.RARITIES[entry.rarity].name + ' ' + entry.name}
											</Link>
										}
									/>
								</Grid.Column>
							))}
						</Grid>
					</div>
				)}
			</Layout>
		);
	}
Example #19
Source File: crewchallenge.tsx    From website with MIT License 4 votes vote down vote up
CrewChallengeGame = (props: CrewChallengeGame) => {
	const { rules, solution, guesses, setGuesses, solveState, setSolveState } = props;
	const portalCrew = React.useContext(PortalCrewContext);

	const [solvedCrew, setSolvedCrew] = React.useState(undefined);
	const [guessesEvaluated, setGuessesEvaluated] = React.useState([]);

	React.useEffect(() => {
		if (solution === '') return;
		setSolvedCrew(getCrew(solution));
		setGuessesEvaluated([]);
	}, [solution]);

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

	const newEvaluations = [];
	guesses.forEach(guess => {
		if (!guessesEvaluated.find(evaluation => evaluation.symbol === guess)) {
			const guessedCrew = getCrew(guess);
			guessedCrew.evaluation = evaluateGuess(guessedCrew);
			newEvaluations.push(guessedCrew);
		}
	});
	if (newEvaluations.length > 0)
		setGuessesEvaluated([...guessesEvaluated, ...newEvaluations]);

	return (
		<React.Fragment>
			<GuessTable solveState={solveState} solvedCrew={solvedCrew} guessesEvaluated={guessesEvaluated} />
			{renderInput()}
			{renderShare()}
		</React.Fragment>
	);

	function renderInput(): JSX.Element {
		if (solveState !== SolveState.Unsolved) return (<></>);
		return (
			<div style={{ margin: '1em 0' }}>
				<CrewPicker rules={rules} guesses={guesses} handleSelect={handleCrewSelect} />
			</div>
		);
	}

	function renderShare(): JSX.Element {
		if (solveState === SolveState.Unsolved) return (<></>);
		if (!props.gameTime) return (<></>);

		const formatEvaluation = (evaluation: number) => {
			if (evaluation === EvaluationState.Exact)
				return '?';
			else if (evaluation === EvaluationState.Adjacent)
				return '?';
			return '⬜';
		};

		const formatGrid = () => {
			const shortId = `${props.gameTime.getUTCMonth()+1}/${props.gameTime.getUTCDate()}`;
			let output = solveState === SolveState.Winner ? `I solved ${GAME_NAME} ${shortId} in ${guesses.length}!` : `${GAME_NAME} ${shortId} stumped me!`;
			output += `\n${GAME_URL}`;
			guessesEvaluated.forEach(guess => {
				output += '\n';
				['variant', 'series', 'rarity'].forEach(evaluation => {
					output += formatEvaluation(guess.evaluation[evaluation]);
				});
				[0, 1, 2].forEach(idx => {
					output += formatEvaluation(guess.evaluation.skills[idx]);
				});
			});
			navigator.clipboard.writeText(output);
		};

		return (
			<div style={{ marginTop: '2em' }}>
				<Popup
					content='Copied!'
					on='click'
					position='right center'
					size='tiny'
					trigger={
						<Button icon='clipboard check' content='Copy results to clipboard' onClick={() => formatGrid()} />
					}
				/>
			</div>
		);
	}

	function handleCrewSelect(symbol: string): void {
		if (symbol === '' || guesses.includes(symbol)) return;
		guesses.push(symbol);
		setGuesses([...guesses]);
		if (guesses.includes(solution))
			endGame(SolveState.Winner);
		else if (guesses.length >= rules.guesses)
			endGame(SolveState.Loser);
	}

	function endGame(solveState: number): void {
		setSolveState(solveState);
		if (props.onGameEnd) props.onGameEnd(solveState);
	}

	function getCrew(symbol: string): any {
		const getSkillOrder = (base_skills: any) => {
			const skills = Object.keys(base_skills).map(skill => {
				return {
					skill, core: base_skills[skill].core
				};
			}).sort((a, b) => b.core - a.core);
			for (let i = skills.length; i < 3; i++) {
				skills.push({ skill: '', core: 0 });
			}
			return skills;
		};

		const getVariantTraits = (traitsHidden: string[]) => {
			// Get variant names from traits_hidden
			const series = ['tos', 'tas', 'tng', 'ds9', 'voy', 'ent', 'dsc', 'pic', 'low', 'snw'];
			const ignore = [
				'female', 'male',
				'artificial_life', 'nonhuman', 'organic', 'species_8472',
				'admiral', 'captain', 'commander', 'lieutenant_commander', 'lieutenant', 'ensign', 'general', 'nagus', 'first_officer',
				'ageofsail', 'bridge_crew', 'evsuit', 'gauntlet_jackpot', 'mirror', 'niners', 'original', 'crewman', 'yeoman',
				'crew_max_rarity_5', 'crew_max_rarity_4', 'crew_max_rarity_3', 'crew_max_rarity_2', 'crew_max_rarity_1'
			];
			const variantTraits = [];
			traitsHidden.forEach(trait => {
				if (!series.includes(trait) && !ignore.includes(trait)) {
					// Also ignore multishow variant traits, e.g. spock_tos, spock_dsc
					if (!/_[a-z]{3}$/.test(trait) || !series.includes(trait.substr(-3)))
						variantTraits.push(trait);
				}
			});
			return variantTraits;
		};

		const getVariants = (variantTraits: string[], shortName: string) => {
			const variants = variantTraits.slice();
			// Dax hacks
			const daxIndex = variants.indexOf('dax');
			if (daxIndex >= 0) {
				variantTraits.unshift(shortName);
				variants[daxIndex] = shortName;
			}
			return variants;
		};

		const getUsableTraits = (crew: any, variantTraits: string[]) => {
			const traits = variantTraits.slice();
			['Female', 'Male'].forEach(usable => { if (crew.traits_hidden.includes(usable.toLowerCase())) traits.push(usable); });
			const usableCollections = [
				'A Little Stroll', 'Animated', 'Badda-Bing, Badda-Bang', 'Bride of Chaotica', 'Delphic Expanse',
				'Holodeck Enthusiasts', 'Our Man Bashir', 'Pet People', 'Play Ball!', 'Set Sail!', 'Sherwood Forest',
				'The Big Goodbye', 'The Wild West'
			];
			crew.collections.forEach(collection => {
				if (usableCollections.includes(collection))
					traits.push(collection);
			});
			return traits.concat(crew.traits_named);
		};

		const crew = portalCrew.find(crew => crew.symbol === symbol);
		let shortName = crew.short_name;
		// Dax hacks
		if (shortName === 'E. Dax') shortName = 'Ezri';
		if (shortName === 'J. Dax') shortName = 'Jadzia';
		const variantTraits = getVariantTraits(crew.traits_hidden);
		return {
			symbol: crew.symbol,
			name: crew.name,
			short_name: shortName,
			variants: getVariants(variantTraits, shortName),
			imageUrlPortrait: crew.imageUrlPortrait ?? `${crew.portrait.file.substring(1).replaceAll('/', '_')}.png`,
			flavor: crew.flavor,
			series: crew.series ?? 'original',
			rarity: crew.max_rarity,
			skills: getSkillOrder(crew.base_skills),
			traits: getUsableTraits(crew, variantTraits)
		};
	}

	function evaluateGuess(crew: any): any {
		const evaluateVariant = (symbol: string, variants: string[]) => {
			if (solvedCrew.symbol === symbol)
				return EvaluationState.Exact;
			else {
				let hasVariant = false;
				solvedCrew.variants.forEach(solvedVariant => {
					if (variants.includes(solvedVariant)) hasVariant = true;
				});
				if (hasVariant) return EvaluationState.Adjacent;
			}
			return EvaluationState.Wrong;
		};

		const evaluateSeries = (series: string) => {
			const getEra = (series: string) => {
				if (series === 'tos' || series === 'tas') return 1;
				if (series === 'tng' || series === 'ds9' || series === 'voy' || series === 'ent') return 2;
				if (series === 'original') return 0;
				return 3;
			};

			if (solvedCrew.series === series)
				return EvaluationState.Exact;
			else if (getEra(solvedCrew.series) === getEra(series))
				return EvaluationState.Adjacent;
			return EvaluationState.Wrong;
		};

		const evaluateRarity = (rarity: number) => {
			if (solvedCrew.rarity === rarity)
				return EvaluationState.Exact;
			else if (solvedCrew.rarity === rarity-1 || solvedCrew.rarity === rarity+1)
				return EvaluationState.Adjacent;
			return EvaluationState.Wrong;
		};

		const evaluateSkill = (skills: any[], index: number) => {
			if (skills[index].skill === '') {
				if (solvedCrew.skills[index].skill === '')
					return EvaluationState.Exact;
			}
			else {
				if (skills[index].skill === solvedCrew.skills[index].skill)
					return EvaluationState.Exact;
				else if (solvedCrew.skills.find(skill => skill.skill === skills[index].skill))
					return EvaluationState.Adjacent;
			}
			return EvaluationState.Wrong;
		};

		const evaluateTraits = (traits: any[]) => {
			const matches = [];
			traits.forEach(trait => {
				if (solvedCrew.traits.includes(trait) && !matches.includes(trait))
					matches.push(trait);
			});
			return matches;
		};

		return {
			crew: crew.symbol === solution ? EvaluationState.Exact : EvaluationState.Wrong,
			variant: evaluateVariant(crew.symbol, crew.variants),
			series: evaluateSeries(crew.series),
			rarity: evaluateRarity(crew.rarity),
			skills: [0, 1, 2].map(index => evaluateSkill(crew.skills, index)),
			traits: evaluateTraits(crew.traits)
		};
	}
}
Example #20
Source File: App.tsx    From watchparty with MIT License 4 votes vote down vote up
render() {
    const sharer = this.state.participants.find((p) => p.isScreenShare);
    const controls = (
      <Controls
        key={this.state.controlsTimestamp}
        togglePlay={this.togglePlay}
        onSeek={this.onSeek}
        fullScreen={this.fullScreen}
        toggleMute={this.toggleMute}
        toggleSubtitle={this.toggleSubtitle}
        setVolume={this.setVolume}
        jumpToLeader={this.jumpToLeader}
        paused={this.state.currentMediaPaused}
        muted={this.isMuted()}
        subtitled={this.isSubtitled()}
        currentTime={this.getCurrentTime()}
        duration={this.getDuration()}
        disabled={!this.haveLock()}
        leaderTime={this.isHttp() ? this.getLeaderTime() : undefined}
        isPauseDisabled={this.isPauseDisabled()}
      />
    );
    const subscribeButton = (
      <SubscribeButton
        user={this.props.user}
        isSubscriber={this.props.isSubscriber}
        isCustomer={this.props.isCustomer}
      />
    );
    const displayRightContent =
      this.state.showRightBar || this.state.fullScreen;
    const rightBar = (
      <Grid.Column
        width={displayRightContent ? 4 : 1}
        style={{ display: 'flex', flexDirection: 'column' }}
        className={`${
          this.state.fullScreen
            ? 'fullHeightColumnFullscreen'
            : 'fullHeightColumn'
        }`}
      >
        <Input
          inverted
          fluid
          label={'My name is:'}
          value={this.state.myName}
          onChange={this.updateName}
          style={{ visibility: displayRightContent ? '' : 'hidden' }}
          icon={
            <Icon
              onClick={() => this.updateName(null, { value: generateName() })}
              name="refresh"
              inverted
              circular
              link
            />
          }
        />
        {
          <Menu
            inverted
            widths={3}
            style={{
              marginTop: '4px',
              marginBottom: '4px',
              visibility: displayRightContent ? '' : 'hidden',
            }}
          >
            <Menu.Item
              name="chat"
              active={this.state.currentTab === 'chat'}
              onClick={() => {
                this.setState({ currentTab: 'chat', unreadCount: 0 });
              }}
              as="a"
            >
              Chat
              {this.state.unreadCount > 0 && (
                <Label circular color="red">
                  {this.state.unreadCount}
                </Label>
              )}
            </Menu.Item>
            <Menu.Item
              name="people"
              active={this.state.currentTab === 'people'}
              onClick={() => this.setState({ currentTab: 'people' })}
              as="a"
            >
              People
              <Label
                circular
                color={
                  getColorForString(
                    this.state.participants.length.toString()
                  ) as SemanticCOLORS
                }
              >
                {this.state.participants.length}
              </Label>
            </Menu.Item>
            <Menu.Item
              name="settings"
              active={this.state.currentTab === 'settings'}
              onClick={() => this.setState({ currentTab: 'settings' })}
              as="a"
            >
              {/* <Icon name="setting" /> */}
              Settings
            </Menu.Item>
          </Menu>
        }
        <Chat
          chat={this.state.chat}
          nameMap={this.state.nameMap}
          pictureMap={this.state.pictureMap}
          socket={this.socket}
          scrollTimestamp={this.state.scrollTimestamp}
          getMediaDisplayName={this.getMediaDisplayName}
          hide={this.state.currentTab !== 'chat' || !displayRightContent}
          isChatDisabled={this.state.isChatDisabled}
          owner={this.state.owner}
          user={this.props.user}
          ref={this.chatRef}
        />
        {this.state.state === 'connected' && (
          <VideoChat
            socket={this.socket}
            participants={this.state.participants}
            nameMap={this.state.nameMap}
            pictureMap={this.state.pictureMap}
            tsMap={this.state.tsMap}
            rosterUpdateTS={this.state.rosterUpdateTS}
            hide={this.state.currentTab !== 'people' || !displayRightContent}
            owner={this.state.owner}
            user={this.props.user}
          />
        )}
        <SettingsTab
          hide={this.state.currentTab !== 'settings' || !displayRightContent}
          user={this.props.user}
          roomLock={this.state.roomLock}
          setRoomLock={this.setRoomLock}
          socket={this.socket}
          isSubscriber={this.props.isSubscriber}
          roomId={this.state.roomId}
          isChatDisabled={this.state.isChatDisabled}
          setIsChatDisabled={this.setIsChatDisabled}
          owner={this.state.owner}
          setOwner={this.setOwner}
          vanity={this.state.vanity}
          setVanity={this.setVanity}
          roomLink={this.state.roomLink}
          password={this.state.password}
          setPassword={this.setPassword}
          clearChat={this.clearChat}
          roomTitle={this.state.roomTitle}
          setRoomTitle={this.setRoomTitle}
          roomDescription={this.state.roomDescription}
          setRoomDescription={this.setRoomDescription}
          roomTitleColor={this.state.roomTitleColor}
          setRoomTitleColor={this.setRoomTitleColor}
          mediaPath={this.state.mediaPath}
          setMediaPath={this.setMediaPath}
        />
      </Grid.Column>
    );
    return (
      <React.Fragment>
        {!this.state.isAutoPlayable && (
          <Modal inverted basic open>
            <div style={{ display: 'flex', justifyContent: 'center' }}>
              <Button
                primary
                size="large"
                onClick={() => {
                  this.setState({ isAutoPlayable: true });
                  this.setMute(false);
                  this.setVolume(1);
                }}
                icon
                labelPosition="left"
              >
                <Icon name="volume up" />
                Click to unmute
              </Button>
            </div>
          </Modal>
        )}
        {this.state.multiStreamSelection && (
          <MultiStreamModal
            streams={this.state.multiStreamSelection}
            setMedia={this.setMedia}
            resetMultiSelect={this.resetMultiSelect}
          />
        )}
        {this.state.isVBrowserModalOpen && (
          <VBrowserModal
            isSubscriber={this.props.isSubscriber}
            subscribeButton={subscribeButton}
            closeModal={() => this.setState({ isVBrowserModalOpen: false })}
            startVBrowser={this.startVBrowser}
            user={this.props.user}
            beta={this.props.beta}
          />
        )}
        {this.state.isScreenShareModalOpen && (
          <ScreenShareModal
            closeModal={() => this.setState({ isScreenShareModalOpen: false })}
            startScreenShare={this.setupScreenShare}
          />
        )}
        {this.state.isFileShareModalOpen && (
          <FileShareModal
            closeModal={() => this.setState({ isFileShareModalOpen: false })}
            startFileShare={this.setupFileShare}
          />
        )}
        {this.state.isSubtitleModalOpen && (
          <SubtitleModal
            closeModal={() => this.setState({ isSubtitleModalOpen: false })}
            socket={this.socket}
            currentSubtitle={this.state.currentSubtitle}
            src={this.state.currentMedia}
            haveLock={this.haveLock}
            getMediaDisplayName={this.getMediaDisplayName}
            beta={this.props.beta}
          />
        )}
        {this.state.error && <ErrorModal error={this.state.error} />}
        {this.state.isErrorAuth && (
          <PasswordModal
            savedPasswords={this.state.savedPasswords}
            roomId={this.state.roomId}
          />
        )}
        {this.state.errorMessage && (
          <Message
            negative
            header="Error"
            content={this.state.errorMessage}
            style={{
              position: 'fixed',
              bottom: '10px',
              right: '10px',
              zIndex: 1000,
            }}
          ></Message>
        )}
        {this.state.successMessage && (
          <Message
            positive
            header="Success"
            content={this.state.successMessage}
            style={{
              position: 'fixed',
              bottom: '10px',
              right: '10px',
              zIndex: 1000,
            }}
          ></Message>
        )}
        <TopBar
          user={this.props.user}
          isCustomer={this.props.isCustomer}
          isSubscriber={this.props.isSubscriber}
          roomTitle={this.state.roomTitle}
          roomDescription={this.state.roomDescription}
          roomTitleColor={this.state.roomTitleColor}
        />
        {
          <Grid stackable celled="internally">
            <Grid.Row id="theaterContainer">
              <Grid.Column
                width={this.state.showRightBar ? 12 : 15}
                className={
                  this.state.fullScreen
                    ? 'fullHeightColumnFullscreen'
                    : 'fullHeightColumn'
                }
              >
                <div
                  style={{
                    display: 'flex',
                    flexDirection: 'column',
                    height: '100%',
                  }}
                >
                  {!this.state.fullScreen && (
                    <React.Fragment>
                      <ComboBox
                        setMedia={this.setMedia}
                        playlistAdd={this.playlistAdd}
                        playlistDelete={this.playlistDelete}
                        playlistMove={this.playlistMove}
                        currentMedia={this.state.currentMedia}
                        getMediaDisplayName={this.getMediaDisplayName}
                        launchMultiSelect={this.launchMultiSelect}
                        streamPath={this.props.streamPath}
                        mediaPath={this.state.mediaPath}
                        disabled={!this.haveLock()}
                        playlist={this.state.playlist}
                      />
                      <Separator />
                      <div
                        className="mobileStack"
                        style={{ display: 'flex', gap: '4px' }}
                      >
                        {this.screenShareStream && (
                          <Button
                            fluid
                            className="toolButton"
                            icon
                            labelPosition="left"
                            color="red"
                            onClick={this.stopScreenShare}
                            disabled={sharer?.id !== this.socket?.id}
                          >
                            <Icon name="cancel" />
                            Stop Share
                          </Button>
                        )}
                        {!this.screenShareStream &&
                          !sharer &&
                          !this.isVBrowser() && (
                            <Popup
                              content={`Share a tab or an application. Make sure to check "Share audio" for best results.`}
                              trigger={
                                <Button
                                  fluid
                                  className="toolButton"
                                  disabled={!this.haveLock()}
                                  icon
                                  labelPosition="left"
                                  color={'instagram'}
                                  onClick={() => {
                                    this.setState({
                                      isScreenShareModalOpen: true,
                                    });
                                  }}
                                >
                                  <Icon name={'slideshare'} />
                                  Screenshare
                                </Button>
                              }
                            />
                          )}
                        {!this.screenShareStream &&
                          !sharer &&
                          !this.isVBrowser() && (
                            <Popup
                              content="Launch a shared virtual browser"
                              trigger={
                                <Button
                                  fluid
                                  className="toolButton"
                                  disabled={!this.haveLock()}
                                  icon
                                  labelPosition="left"
                                  color="green"
                                  onClick={() => {
                                    this.setState({
                                      isVBrowserModalOpen: true,
                                    });
                                  }}
                                >
                                  <Icon name="desktop" />
                                  VBrowser
                                </Button>
                              }
                            />
                          )}
                        {this.isVBrowser() && (
                          <Popup
                            content="Choose the person controlling the VBrowser"
                            trigger={
                              <Dropdown
                                icon="keyboard"
                                labeled
                                className="icon"
                                style={{ height: '36px' }}
                                button
                                value={this.state.controller}
                                placeholder="No controller"
                                clearable
                                onChange={this.changeController}
                                selection
                                disabled={!this.haveLock()}
                                options={this.state.participants.map((p) => ({
                                  text: this.state.nameMap[p.id] || p.id,
                                  value: p.id,
                                }))}
                              ></Dropdown>
                            }
                          />
                        )}
                        {this.isVBrowser() && (
                          <Dropdown
                            icon="desktop"
                            labeled
                            className="icon"
                            style={{ height: '36px' }}
                            button
                            disabled={!this.haveLock()}
                            value={this.state.vBrowserResolution}
                            onChange={(_e, data) =>
                              this.setState({
                                vBrowserResolution: data.value as string,
                              })
                            }
                            selection
                            options={[
                              {
                                text: '1080p (Plus only)',
                                value: '1920x1080@30',
                                disabled: !this.state.isVBrowserLarge,
                              },
                              {
                                text: '720p',
                                value: '1280x720@30',
                              },
                              {
                                text: '576p',
                                value: '1024x576@60',
                              },
                              {
                                text: '486p',
                                value: '864x486@60',
                              },
                              {
                                text: '360p',
                                value: '640x360@60',
                              },
                            ]}
                          ></Dropdown>
                        )}
                        {this.isVBrowser() && (
                          <Button
                            fluid
                            className="toolButton"
                            icon
                            labelPosition="left"
                            color="red"
                            disabled={!this.haveLock()}
                            onClick={this.stopVBrowser}
                          >
                            <Icon name="cancel" />
                            Stop VBrowser
                          </Button>
                        )}
                        {!this.screenShareStream &&
                          !sharer &&
                          !this.isVBrowser() && (
                            <Popup
                              content="Stream your own video file"
                              trigger={
                                <Button
                                  fluid
                                  className="toolButton"
                                  disabled={!this.haveLock()}
                                  icon
                                  labelPosition="left"
                                  onClick={() => {
                                    this.setState({
                                      isFileShareModalOpen: true,
                                    });
                                  }}
                                >
                                  <Icon name="file" />
                                  File
                                </Button>
                              }
                            />
                          )}
                        {false && (
                          <SearchComponent
                            setMedia={this.setMedia}
                            playlistAdd={this.playlistAdd}
                            type={'youtube'}
                            streamPath={this.props.streamPath}
                            disabled={!this.haveLock()}
                          />
                        )}
                        {Boolean(this.props.streamPath) && (
                          <SearchComponent
                            setMedia={this.setMedia}
                            playlistAdd={this.playlistAdd}
                            type={'stream'}
                            streamPath={this.props.streamPath}
                            launchMultiSelect={this.launchMultiSelect}
                            disabled={!this.haveLock()}
                          />
                        )}
                      </div>
                      <Separator />
                    </React.Fragment>
                  )}
                  <div style={{ flexGrow: 1 }}>
                    <div id="playerContainer">
                      {(this.state.loading ||
                        !this.state.currentMedia ||
                        this.state.nonPlayableMedia) && (
                        <div
                          id="loader"
                          className="videoContent"
                          style={{
                            display: 'flex',
                            alignItems: 'center',
                            justifyContent: 'center',
                          }}
                        >
                          {this.state.loading && (
                            <Dimmer active>
                              <Loader>
                                {this.isVBrowser()
                                  ? 'Launching virtual browser. This can take up to a minute.'
                                  : ''}
                              </Loader>
                            </Dimmer>
                          )}
                          {!this.state.loading && !this.state.currentMedia && (
                            <Message
                              color="yellow"
                              icon="hand point up"
                              header="You're not watching anything!"
                              content="Pick something to watch above."
                            />
                          )}
                          {!this.state.loading &&
                            this.state.nonPlayableMedia && (
                              <Message
                                color="red"
                                icon="frown"
                                header="It doesn't look like this is a media file!"
                                content="Maybe you meant to launch a VBrowser if you're trying to visit a web page?"
                              />
                            )}
                        </div>
                      )}
                      <iframe
                        style={{
                          display:
                            this.isYouTube() && !this.state.loading
                              ? 'block'
                              : 'none',
                        }}
                        title="YouTube"
                        id="leftYt"
                        className="videoContent"
                        allowFullScreen
                        frameBorder="0"
                        allow="autoplay"
                        src="https://www.youtube.com/embed/?enablejsapi=1&controls=0&rel=0"
                      />
                      {this.isVBrowser() &&
                      this.getVBrowserPass() &&
                      this.getVBrowserHost() ? (
                        <VBrowser
                          username={this.socket.id}
                          password={this.getVBrowserPass()}
                          hostname={this.getVBrowserHost()}
                          controlling={this.state.controller === this.socket.id}
                          setLoadingFalse={this.setLoadingFalse}
                          resolution={this.state.vBrowserResolution}
                          doPlay={this.doPlay}
                          setResolution={(data: string) =>
                            this.setState({ vBrowserResolution: data })
                          }
                        />
                      ) : (
                        <video
                          style={{
                            display:
                              (this.isVideo() && !this.state.loading) ||
                              this.state.fullScreen
                                ? 'block'
                                : 'none',
                            width: '100%',
                            maxHeight:
                              'calc(100vh - 62px - 36px - 36px - 8px - 41px - 16px)',
                          }}
                          id="leftVideo"
                          onEnded={this.onVideoEnded}
                          playsInline
                        ></video>
                      )}
                    </div>
                  </div>
                  {this.state.currentMedia && controls}
                  {Boolean(this.state.total) && (
                    <div>
                      <Progress
                        size="tiny"
                        color="green"
                        inverted
                        value={this.state.downloaded}
                        total={this.state.total}
                        // indicating
                        label={
                          Math.min(
                            (this.state.downloaded / this.state.total) * 100,
                            100
                          ).toFixed(2) +
                          '% - ' +
                          formatSpeed(this.state.speed) +
                          ' - ' +
                          this.state.connections +
                          ' connections'
                        }
                      ></Progress>
                    </div>
                  )}
                </div>
                <Button
                  style={{
                    position: 'absolute',
                    top: '50%',
                    right: 'calc(0% - 18px)',
                    zIndex: 900,
                  }}
                  circular
                  size="mini"
                  icon={this.state.showRightBar ? 'angle right' : 'angle left'}
                  onClick={() =>
                    this.setState({ showRightBar: !this.state.showRightBar })
                  }
                />
              </Grid.Column>
              {rightBar}
            </Grid.Row>
          </Grid>
        }
      </React.Fragment>
    );
  }
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: Chat.tsx    From watchparty with MIT License 4 votes vote down vote up
ChatMessage = ({
  message,
  nameMap,
  pictureMap,
  formatMessage,
  user,
  socket,
  owner,
  isChatDisabled,
  setReactionMenu,
  handleReactionClick,
  className,
}: {
  message: ChatMessage;
  nameMap: StringDict;
  pictureMap: StringDict;
  formatMessage: (cmd: string, msg: string) => React.ReactNode;
  user: firebase.User | undefined;
  socket: Socket;
  owner: string | undefined;
  isChatDisabled: boolean | undefined;
  setReactionMenu: Function;
  handleReactionClick: Function;
  className: string;
}) => {
  const { id, timestamp, cmd, msg, system, isSub, reactions } = message;
  const spellFull = 5; // the number of people whose names should be written out in full in the reaction popup
  return (
    <Comment className={`${classes.comment} ${className}`}>
      {id ? (
        <Popup
          content="WatchParty Plus subscriber"
          disabled={!isSub}
          trigger={
            <Comment.Avatar
              className={isSub ? classes.subscriber : ''}
              src={
                pictureMap[id] ||
                getDefaultPicture(nameMap[id], getColorForStringHex(id))
              }
            />
          }
        />
      ) : null}
      <Comment.Content>
        <UserMenu
          displayName={nameMap[id] || id}
          user={user}
          timestamp={timestamp}
          socket={socket}
          userToManage={id}
          isChatMessage
          disabled={!Boolean(owner && owner === user?.uid)}
          trigger={
            <Comment.Author as="a" className="light">
              {Boolean(system) && 'System'}
              {nameMap[id] || id}
            </Comment.Author>
          }
        />
        <Comment.Metadata className="dark">
          <div>{new Date(timestamp).toLocaleTimeString()}</div>
        </Comment.Metadata>
        <Comment.Text className="light system">
          {cmd && formatMessage(cmd, msg)}
        </Comment.Text>
        <Linkify
          componentDecorator={(
            decoratedHref: string,
            decoratedText: string,
            key: string
          ) => (
            <SecureLink href={decoratedHref} key={key}>
              {decoratedText}
            </SecureLink>
          )}
        >
          <Comment.Text className="light">{!cmd && msg}</Comment.Text>
        </Linkify>
        <div className={classes.commentMenu}>
          <Icon
            onClick={(e: MouseEvent) => {
              const viewportOffset = (e.target as any).getBoundingClientRect();
              setReactionMenu(
                true,
                id,
                timestamp,
                viewportOffset.top,
                viewportOffset.right
              );
            }}
            name={'' as any}
            inverted
            link
            disabled={isChatDisabled}
            style={{
              opacity: 1,
              display: 'flex',
              justifyContent: 'center',
              alignItems: 'center',
              padding: 10,
              margin: 0,
            }}
          >
            <span
              role="img"
              aria-label="React"
              style={{ margin: 0, fontSize: 18 }}
            >
              ?
            </span>
          </Icon>
        </div>
        <TransitionGroup>
          {Object.keys(reactions ?? []).map((key) =>
            reactions?.[key].length ? (
              <CSSTransition
                key={key}
                timeout={200}
                classNames={{
                  enter: classes['reaction-enter'],
                  enterActive: classes['reaction-enter-active'],
                  exit: classes['reaction-exit'],
                  exitActive: classes['reaction-exit-active'],
                }}
                unmountOnExit
              >
                <Popup
                  content={`${reactions[key]
                    .slice(0, spellFull)
                    .map((id) => nameMap[id] || 'Unknown')
                    .concat(
                      reactions[key].length > spellFull
                        ? [`${reactions[key].length - spellFull} more`]
                        : []
                    )
                    .reduce(
                      (text, value, i, array) =>
                        text + (i < array.length - 1 ? ', ' : ' and ') + value
                    )} reacted.`}
                  offset={[0, 6]}
                  trigger={
                    <div
                      className={`${classes.reactionContainer} ${
                        reactions[key].includes(socket.id)
                          ? classes.highlighted
                          : ''
                      }`}
                      onClick={() =>
                        handleReactionClick(key, message.id, message.timestamp)
                      }
                    >
                      <span
                        style={{
                          fontSize: 17,
                          position: 'relative',
                          bottom: 1,
                        }}
                      >
                        {key}
                      </span>
                      <SwitchTransition>
                        <CSSTransition
                          key={key + '-' + reactions[key].length}
                          classNames={{
                            enter: classes['reactionCounter-enter'],
                            enterActive:
                              classes['reactionCounter-enter-active'],
                            exit: classes['reactionCounter-exit'],
                            exitActive: classes['reactionCounter-exit-active'],
                          }}
                          addEndListener={(node, done) =>
                            node.addEventListener('transitionend', done, false)
                          }
                          unmountOnExit
                        >
                          <span
                            className={classes.reactionCounter}
                            style={{
                              color: 'rgba(255, 255, 255, 0.85)',
                              marginLeft: 3,
                            }}
                          >
                            {reactions[key].length}
                          </span>
                        </CSSTransition>
                      </SwitchTransition>
                    </div>
                  }
                />
              </CSSTransition>
            ) : null
          )}
        </TransitionGroup>
      </Comment.Content>
    </Comment>
  );
}
Example #23
Source File: vaultcrew.tsx    From website with MIT License 4 votes vote down vote up
render() {
		const { crew, itemsReady } = this.props;
		const SZ = (scale: number) => (this.props.size * scale).toFixed(2);
		let borderColor = new TinyColor(CONFIG.RARITIES[crew.max_rarity].color);

		let star_reward = `${process.env.GATSBY_ASSETS_URL}atlas/star_reward.png`;
		let star_reward_inactive = `${process.env.GATSBY_ASSETS_URL}atlas/star_reward_inactive.png`;

		let iconStyle: React.CSSProperties = {
			display: 'inline-block',
			height: SZ(2.4) + 'em',
			paddingTop: SZ(0.4) + 'em',
			paddingRight: SZ(0.4) + 'em'
		};

		let rarity = [];
		for (let i = 0; i < crew.rarity; i++) {
			rarity.push(<img key={i} src={star_reward} style={iconStyle} />);
		}
		for (let i = crew.rarity; i < crew.max_rarity; i++) {
			rarity.push(<img key={i} src={star_reward_inactive} style={iconStyle} />);
		}

		let skillicons = [];
		let skills_sorted = Object.entries(crew.base_skills)
			.sort((a, b) => { a[1].core - b[1].core });
		skills_sorted.forEach((s) => {
			let skillName = s[0];
			skillicons.push(<img key={skillName} src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${skillName}.png`} style={iconStyle} />);
		});

		let divStyle: React.CSSProperties = this.props.style || {};
		divStyle.display = 'grid';
		divStyle.width = SZ(22) + 'em';
		divStyle.height = SZ(20) + 'em';
		divStyle.gridTemplateColumns = `${SZ(16)}em ${SZ(6)}em`;
		divStyle.gridTemplateRows = `${SZ(16)}em ${SZ(4)}em`;
		divStyle.gridTemplateAreas = "'portrait equipment' 'footer footer'";
		divStyle.borderWidth = SZ(0.2) + 'em';
		divStyle.borderRadius = SZ(0.2) + 'em';
		divStyle.borderStyle = 'solid';
		divStyle.borderColor = borderColor.toHexString();
		divStyle.backgroundColor = borderColor
			.clone()
			.darken(50)
			.toHexString();

		let equipmentColumnStyle: React.CSSProperties = {
			gridArea: 'equipment',
			display: 'grid',
			textAlign: 'center',
			gridTemplateRows: 'repeat(4, 1fr)',
			margin: SZ(0.2) + 'em',
			gap: SZ(0.1) + 'em'
		};

		let equipmentCellImg: React.CSSProperties = {
			height: SZ(3) + 'em',
			borderWidth: SZ(0.2) + 'em',
			borderStyle: 'solid',
			borderColor: borderColor.toHexString(),
			borderRadius: SZ(0.4) + 'em'
		};

		let cardFooter: React.CSSProperties = {
			gridArea: 'footer',
			display: 'grid',
			gridTemplateColumns: '1fr 1fr',
			backgroundColor: borderColor
				.clone()
				.darken(40)
				.toHexString(),
			padding: SZ(0.4) + 'em',
			width: '98%',
			height: '90%'
		};

		let cardFooterSkills: React.CSSProperties = {
			justifySelf: 'start',
			backgroundColor: borderColor
				.clone()
				.darken(50)
				.toHexString(),
			padding: SZ(0.1) + 'em'
		};

		let cardFooterLevel: React.CSSProperties = {
			justifySelf: 'end',
			backgroundColor: borderColor
				.clone()
				.darken(50)
				.toHexString(),
			padding: SZ(0.1) + 'em',
			fontSize: SZ(2.2) + 'em',
			color: 'white',
			display: 'flex'
		};

		// Dec levels can be either end of one equip range or start of the next (e.g. lvl 20 is 10-20 or 20-30)
		//	Assume at the start of next range unless has multiple equips
		let startlevel = Math.floor(crew.level / 10) * 4;
		if (crew.level % 10 == 0 && crew.equipment.length > 1) startlevel = startlevel - 4;
		let eqimgs = [];
		if (!crew.equipment_slots[startlevel] || !itemsReady) {
			//console.error(`Missing equipment slots information for crew '${crew.name}'`);
			//console.log(crew);
			eqimgs = [
				'items_equipment_box02_icon.png',
				'items_equipment_box02_icon.png',
				'items_equipment_box02_icon.png',
				'items_equipment_box02_icon.png'
			];
		} else {
			eqimgs = [
				crew.equipment_slots[startlevel].imageUrl,
				crew.equipment_slots[startlevel + 1].imageUrl,
				crew.equipment_slots[startlevel + 2].imageUrl,
				crew.equipment_slots[startlevel + 3].imageUrl
			];
		}

		if (crew.equipment) {
			[0, 1, 2, 3].forEach(idx => {
				if (crew.equipment.indexOf(idx) < 0) {
					eqimgs[idx] = 'items_equipment_box02_icon.png';
				}
			});
		}

		let portraitDivStyle: React.CSSProperties = {
			gridArea: 'portrait',
			position: 'relative'
		};

		if (crew.immortal > 0 || (crew.rarity === crew.max_rarity && crew.level === 100 && crew.equipment.length === 4)) {
			// For immortalized crew only
			portraitDivStyle.backgroundSize = 'cover';
			portraitDivStyle.backgroundImage = `url(${process.env.GATSBY_ASSETS_URL}collection_vault_vault_item_bg_immortalized_256.png)`;
		}

		return (
			<div style={divStyle}>
				<div style={portraitDivStyle}>
					<Popup
						on="click"
						header={crew.name}
						content={formatCrewStats(crew)}
						trigger={<img src={`${process.env.GATSBY_ASSETS_URL}${crew.imageUrlPortrait}`} style={{ width: '100%' }} />}
					/>

					<div
						style={{
							position: 'absolute',
							bottom: '0px',
							width: '100%',
							textAlign: 'initial',
							backgroundColor: 'rgba(0, 0, 0, 0.5)'
						}}
					>
						{rarity}
					</div>
					{crew.immortal > 0 && (
						<div
							style={{
								position: 'absolute',
								top: '0px',
								width: '100%',
								textAlign: 'initial',
								backgroundColor: 'rgba(0, 0, 0, 0.5)',
								fontSize: SZ(1.2) + 'em'
							}}
						>
							Frozen: {crew.immortal} in vault
						</div>
					)}
				</div>

				<div style={equipmentColumnStyle}>
					<div style={{ display: 'inline-block' }}>
						<img style={equipmentCellImg} src={`${process.env.GATSBY_ASSETS_URL}${eqimgs[0]}`} />
					</div>
					<div style={{ display: 'inline-block' }}>
						<img style={equipmentCellImg} src={`${process.env.GATSBY_ASSETS_URL}${eqimgs[1]}`} />
					</div>
					<div style={{ display: 'inline-block' }}>
						<img style={equipmentCellImg} src={`${process.env.GATSBY_ASSETS_URL}${eqimgs[2]}`} />
					</div>
					<div style={{ display: 'inline-block' }}>
						<img style={equipmentCellImg} src={`${process.env.GATSBY_ASSETS_URL}${eqimgs[3]}`} />
					</div>
				</div>

				<div style={cardFooter}>
					<div style={cardFooterSkills}>
						<span>{skillicons}</span>
					</div>

					<div style={cardFooterLevel}>
						<span style={{ margin: 'auto' }}>{crew.level}</span>
					</div>
				</div>
			</div>
		);
	}
Example #24
Source File: Controls.tsx    From watchparty with MIT License 4 votes vote down vote up
render() {
    const {
      togglePlay,
      onSeek,
      fullScreen,
      toggleMute,
      toggleSubtitle,
      jumpToLeader,
      currentTime,
      duration,
      leaderTime,
      isPauseDisabled,
      disabled,
      subtitled,
      paused,
    } = this.props;
    const { muted, volume } = this.state;
    const isBehind = leaderTime && leaderTime - currentTime > 5;
    return (
      <div className="controls">
        <Icon
          size="large"
          onClick={() => {
            togglePlay();
          }}
          className="control action"
          disabled={disabled || isPauseDisabled}
          name={paused ? 'play' : 'pause'}
        />
        <Popup
          content={
            (isBehind ? "We've detected that your stream is behind. " : '') +
            'Click to sync to leader.'
          }
          trigger={
            <Icon
              size="large"
              onClick={jumpToLeader}
              className={`control action ${isBehind ? 'glowing' : ''}`}
              name={'angle double right'}
            />
          }
        />
        <div className="control">{formatTimestamp(currentTime)}</div>
        <Progress
          size="tiny"
          color="blue"
          onClick={
            duration < Infinity && !this.props.disabled ? onSeek : undefined
          }
          onMouseOver={this.onMouseOver}
          onMouseOut={this.onMouseOut}
          onMouseMove={this.onMouseMove}
          className="control action"
          inverted
          style={{
            flexGrow: 1,
            marginTop: 0,
            marginBottom: 0,
            position: 'relative',
            minWidth: '50px',
          }}
          value={currentTime}
          total={duration}
        >
          {duration < Infinity && this.state.showTimestamp && (
            <div
              style={{
                position: 'absolute',
                bottom: '0px',
                left: `calc(${this.state.posTimestamp * 100 + '% - 27px'})`,
                pointerEvents: 'none',
              }}
            >
              <Label basic color="blue" pointing="below">
                <div style={{ width: '34px' }}>
                  {formatTimestamp(this.state.currTimestamp)}
                </div>
              </Label>
            </div>
          )}
        </Progress>
        <div className="control">{formatTimestamp(duration)}</div>
        <Icon
          size="large"
          onClick={() => {
            toggleSubtitle();
          }}
          className="control action"
          name={subtitled ? 'closed captioning' : 'closed captioning outline'}
          title="Captions"
        />
        <Icon
          size="large"
          onClick={() => fullScreen(false)}
          className="control action"
          style={{ transform: 'rotate(90deg)' }}
          name="window maximize outline"
          title="Theater Mode"
        />
        <Icon
          size="large"
          onClick={() => fullScreen(true)}
          className="control action"
          name="expand"
          title="Fullscreen"
        />
        <Icon
          size="large"
          onClick={() => {
            toggleMute();
            this.setState({ muted: !this.state.muted });
          }}
          className="control action"
          name={muted ? 'volume off' : 'volume up'}
          title="Mute"
        />
        <div style={{ width: '100px', marginRight: '10px' }}>
          <Slider
            value={volume}
            color="blue"
            settings={{
              min: 0,
              max: 1,
              step: 0.01,
              onChange: (value: number) => {
                this.setState({ volume: value });
                this.props.setVolume(value);
              },
            }}
          />
        </div>
      </div>
    );
  }
Example #25
Source File: assignmentslist.tsx    From website with MIT License 4 votes vote down vote up
AssignmentsList = (props: AssignmentsList) => {
	const { shuttlers, setShuttlers, assigned, setAssigned, crewScores, updateCrewScores } = props;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	function updateShuttleScores(): void {
		const DIFFICULTY = 2000;
		const newScores = [];
		assigned.forEach(seated => {
			if (!newScores[seated.shuttleId]) {
				const seatCount = shuttlers.shuttles.find(shuttle => shuttle.id === seated.shuttleId).seats.length;
				newScores[seated.shuttleId] = { chance: 0, scores: Array(seatCount).fill(0) };
			}
			newScores[seated.shuttleId].scores[seated.seatNum] = seated.seatScore;
			const dAvgSkill = newScores[seated.shuttleId].scores.reduce((a, b) => (a + b), 0)/newScores[seated.shuttleId].scores.length;
			const dChance = 1/(1+Math.pow(Math.E, 3.5*(0.5-dAvgSkill/DIFFICULTY)));
			newScores[seated.shuttleId].chance = dAvgSkill > 0 ? dChance : 0;
		});
		setShuttleScores(newScores);
	}
}
Example #26
Source File: UserMenu.tsx    From watchparty with MIT License 4 votes vote down vote up
UserMenu = ({
  user,
  socket,
  userToManage,
  trigger,
  displayName,
  position,
  disabled,
  timestamp,
  isChatMessage,
}: {
  user?: firebase.User;
  socket: Socket;
  userToManage: string;
  trigger: any;
  icon?: string;
  displayName?: string;
  position?: any;
  disabled: boolean;
  timestamp?: string;
  isChatMessage?: boolean;
}) => {
  const [isOpen, setIsOpen] = useState(false);
  const handleOpen = () => setIsOpen(true);
  const handleClose = () => setIsOpen(false);
  return (
    <Popup
      className="userMenu"
      trigger={trigger}
      on="click"
      open={isOpen}
      onOpen={handleOpen}
      onClose={handleClose}
      position={position}
      disabled={disabled}
    >
      <div className="userMenuHeader">{displayName}</div>
      <div className="userMenuContent">
        <Button.Group vertical labeled icon>
          <Button
            content="Kick"
            negative
            icon="ban"
            onClick={async () => {
              const token = await user?.getIdToken();
              socket.emit('kickUser', {
                userToBeKicked: userToManage,
                uid: user?.uid,
                token,
              });
              setIsOpen(false);
            }}
          />
          {isChatMessage && (
            <Button
              content="Delete Message"
              icon="comment"
              onClick={async () => {
                const token = await user?.getIdToken();
                socket.emit('CMD:deleteChatMessages', {
                  author: userToManage,
                  timestamp: timestamp,
                  uid: user?.uid,
                  token,
                });
                setIsOpen(false);
              }}
            />
          )}
          <Button
            content="Delete User's Messages"
            icon="comments"
            onClick={async () => {
              const token = await user?.getIdToken();
              socket.emit('CMD:deleteChatMessages', {
                author: userToManage,
                uid: user?.uid,
                token,
              });
              setIsOpen(false);
            }}
          />
        </Button.Group>
      </div>
    </Popup>
  );
}
Example #27
Source File: profile_charts.tsx    From website with MIT License 4 votes vote down vote up
render() {
		const {
			data_ownership,
			skill_distribution,
			flat_skill_distribution,
			r4_stars,
			r5_stars,
			radar_skill_rarity,
			radar_skill_rarity_owned,
			honordebt,
			excludeFulfilled,
		} = this.state;

		let { demands } = this.state;

		let totalHonorDebt = 0;
		let readableHonorDebt = '';

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

			let totalHonorDebtDays = totalHonorDebt / 2000;

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

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

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

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

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

		totalChronCost = Math.floor(totalChronCost);

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

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

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

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

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

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

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

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

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

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

				<h3>Skill distribution for owned crew (number of characters per skill combos Primary > Secondary)</h3>
				<Checkbox label='Include tertiary skill' onChange={() => this._onIncludeTertiary()} checked={this.state.includeTertiary} />
				<div>
					<div style={{ height: '420px', width: '50%', display: 'inline-block' }}>
						<ResponsiveSunburst
							data={skill_distribution}
							theme={themes.dark}
							margin={{ top: 40, right: 20, bottom: 20, left: 20 }}
							identity='name'
							value='loc'
							cornerRadius={2}
							borderWidth={1}
							borderColor='white'
							colors={{ scheme: 'nivo' }}
							childColor={{ from: 'color' }}
							animate={true}
							motionStiffness={90}
							motionDamping={15}
							isInteractive={true}
						/>
					</div>
					<div style={{ height: '420px', width: '50%', display: 'inline-block' }}>
						<ResponsiveBar
							data={flat_skill_distribution}
							keys={['Count']}
							theme={themes.dark}
							indexBy='name'
							layout='horizontal'
							margin={{ top: 50, right: 130, bottom: 50, left: 100 }}
							padding={0.3}
							axisBottom={{
								legend: 'Number of crew',
								legendPosition: 'middle',
								legendOffset: 32,
							}}
							labelSkipWidth={12}
							labelSkipHeight={12}
							legends={[
								{
									dataFrom: 'keys',
									anchor: 'bottom-right',
									direction: 'column',
									justify: false,
									translateX: 120,
									translateY: 0,
									itemsSpacing: 2,
									itemWidth: 100,
									itemHeight: 20,
									itemDirection: 'left-to-right',
									symbolSize: 20,
									effects: [
										{
											on: 'hover',
											style: {
												itemOpacity: 1,
											},
										},
									],
								},
							]}
							animate={false}
						/>
					</div>
				</div>
			</ErrorBoundary>
		);
	}
Example #28
Source File: SubtitleModal.tsx    From watchparty with MIT License 4 votes vote down vote up
render() {
    const { closeModal } = this.props;
    return (
      <Modal open={true} onClose={closeModal as any}>
        <Modal.Header>
          Subtitles are {Boolean(this.props.currentSubtitle) ? 'on' : 'off'}
          <Button
            style={{ float: 'right' }}
            color="red"
            title="Remove Subtitles"
            disabled={
              !Boolean(this.props.currentSubtitle) || !this.props.haveLock()
            }
            icon
            onClick={() => {
              this.props.socket.emit('CMD:subtitle', null);
            }}
          >
            <Icon name="trash" />
          </Button>
        </Modal.Header>
        <Modal.Content image>
          <Modal.Description>
            {process.env.NODE_ENV === 'development' && (
              <div style={{ maxWidth: '600px' }}>
                {this.props.currentSubtitle}
              </div>
            )}
            <Grid columns={2}>
              <Grid.Column>
                <Popup
                  content="Upload a .srt subtitle file for this video"
                  trigger={
                    <Button
                      color="violet"
                      icon
                      labelPosition="left"
                      fluid
                      onClick={() => this.uploadSubtitle()}
                      disabled={!this.props.haveLock()}
                    >
                      <Icon name="closed captioning" />
                      Upload Subtitles
                    </Button>
                  }
                />
              </Grid.Column>
              {this.props.src.startsWith('http') && (
                <Grid.Column>
                  <div style={{ display: 'flex' }}>
                    <Button
                      loading={this.state.loading}
                      color="green"
                      disabled={!this.props.haveLock()}
                      icon
                      labelPosition="left"
                      fluid
                      onClick={async () => {
                        this.setState({ loading: true });
                        const resp = await window.fetch(
                          serverPath + '/searchSubtitles?url=' + this.props.src
                        );
                        const json = await resp.json();
                        this.setState({ searchResults: json });
                        this.setState({ loading: false });
                      }}
                    >
                      <Icon name="search" />
                      Search OpenSubtitles
                    </Button>
                    {this.props.beta && (
                      <Button
                        loading={this.state.loading}
                        color="green"
                        disabled={!this.props.haveLock()}
                        icon
                        labelPosition="left"
                        fluid
                        onClick={async () => {
                          this.setState({ loading: true });
                          const resp = await window.fetch(
                            serverPath +
                              '/searchSubtitles?title=' +
                              this.props.getMediaDisplayName(this.props.src)
                          );
                          const json = await resp.json();
                          this.setState({ searchResults: json });
                          this.setState({ loading: false });
                        }}
                      >
                        <Icon name="search" />
                        Search by Title
                      </Button>
                    )}
                  </div>
                </Grid.Column>
              )}
            </Grid>
            <div>
              {this.state.searchResults.map((result: any) => (
                <div>
                  <Radio
                    label={result.SubFileName}
                    name="radioGroup"
                    value={result.SubDownloadLink}
                    checked={this.props.currentSubtitle?.includes(
                      result.SubDownloadLink
                    )}
                    onChange={(e, { value }) => {
                      this.props.socket.emit(
                        'CMD:subtitle',
                        serverPath + '/downloadSubtitles?url=' + value
                      );
                    }}
                  />
                </div>
              ))}
            </div>
            {/* TODO add a spinner */}
          </Modal.Description>
        </Modal.Content>
      </Modal>
    );
  }
Example #29
Source File: SettingsTab.tsx    From watchparty with MIT License 4 votes vote down vote up
SettingsTab = ({
  hide,
  user,
  roomLock,
  setRoomLock,
  socket,
  isSubscriber,
  owner,
  vanity,
  setVanity,
  roomLink,
  password,
  setPassword,
  isChatDisabled,
  setIsChatDisabled,
  clearChat,
  roomTitle,
  roomDescription,
  roomTitleColor,
  mediaPath,
  setMediaPath,
}: SettingsTabProps) => {
  const [updateTS, setUpdateTS] = useState(0);
  const [permModalOpen, setPermModalOpen] = useState(false);
  const [validVanity, setValidVanity] = useState(true);
  const [validVanityLoading, setValidVanityLoading] = useState(false);
  const [adminSettingsChanged, setAdminSettingsChanged] = useState(false);
  const [roomTitleInput, setRoomTitleInput] = useState<string | undefined>(undefined);
  const [roomDescriptionInput, setRoomDescriptionInput] = useState<
    string | undefined
  >(undefined);
  const [roomTitleColorInput, setRoomTitleColorInput] = useState<
    string | undefined
  >('');

  const setRoomState = useCallback(
    async (data: any) => {
      const token = await user?.getIdToken();
      socket.emit('CMD:setRoomState', {
        uid: user?.uid,
        token,
        ...data,
      });
    },
    [socket, user]
  );
  const setRoomOwner = useCallback(
    async (data: any) => {
      const token = await user?.getIdToken();
      socket.emit('CMD:setRoomOwner', {
        uid: user?.uid,
        token,
        ...data,
      });
    },
    [socket, user]
  );
  const checkValidVanity = useCallback(
    async (input: string) => {
      if (!input) {
        setValidVanity(true);
        return;
      }
      setValidVanity(false);
      setValidVanityLoading(true);
      const response = await axios.get(serverPath + '/resolveRoom/' + input);
      const data = response.data;
      setValidVanityLoading(false);
      if (
        data &&
        data.vanity &&
        data.vanity !== roomLink.split('/').slice(-1)[0]
      ) {
        // Already exists and doesn't match current room
        setValidVanity(false);
      } else {
        setValidVanity(true);
      }
    },
    [setValidVanity, roomLink]
  );
  const disableLocking =
    !Boolean(user) || Boolean(roomLock && roomLock !== user?.uid);
  const disableOwning = !Boolean(user) || Boolean(owner && owner !== user?.uid);

  return (
    <div
      style={{
        display: hide ? 'none' : 'block',
        color: 'white',
        overflow: 'scroll',
        padding: '8px',
      }}
    >
      {permModalOpen && (
        <PermanentRoomModal
          closeModal={() => setPermModalOpen(false)}
        ></PermanentRoomModal>
      )}
      <div className="sectionHeader">Room Settings</div>
      {!user && (
        <Message color="yellow" size="tiny">
          You need to be signed in to change these settings.
        </Message>
      )}
      <SettingRow
        icon={roomLock ? 'lock' : 'lock open'}
        name={`Lock Room`}
        description="Only the person who locked the room can control the video."
        checked={Boolean(roomLock)}
        disabled={disableLocking && disableOwning}
        onChange={(_e, data) => setRoomLock(data.checked)}
      />
      {
        <SettingRow
          icon={'clock'}
          name={`Make Room Permanent`}
          description={
            'Prevent this room from expiring. This also unlocks additional room features.'
          }
          helpIcon={
            <Icon
              name="help circle"
              onClick={() => setPermModalOpen(true)}
              style={{ cursor: 'pointer' }}
            ></Icon>
          }
          checked={Boolean(owner)}
          disabled={disableOwning}
          onChange={(_e, data) => setRoomOwner({ undo: !data.checked })}
        />
      }
      {owner && owner === user?.uid && (
        <div className="sectionHeader">Admin Settings</div>
      )}
      {owner && owner === user?.uid && (
        <SettingRow
          icon={'key'}
          name={`Set Room Password`}
          description="Users must know this password in order to join the room."
          content={
            <Input
              value={password}
              size="mini"
              onChange={(e) => {
                setAdminSettingsChanged(true);
                setPassword(e.target.value);
              }}
              fluid
            />
          }
          disabled={false}
        />
      )}
      {owner && owner === user?.uid && (
        <SettingRow
          icon={'folder'}
          name={`Set Room Media Source`}
          description="Set a media source URL with files to replace the default examples. Supports S3 buckets and nginx file servers."
          content={
            <Input
              value={mediaPath}
              size="mini"
              onChange={(e) => {
                setAdminSettingsChanged(true);
                setMediaPath(e.target.value);
              }}
              fluid
            />
          }
          disabled={false}
        />
      )}
      {owner && owner === user?.uid && (
        <SettingRow
          icon={'i cursor'}
          name={`Disable Chat`}
          description="Prevent users from sending messages in chat."
          checked={Boolean(isChatDisabled)}
          disabled={false}
          onChange={(_e, data) => {
            setAdminSettingsChanged(true);
            setIsChatDisabled(Boolean(data.checked));
          }}
        />
      )}
      {owner && owner === user?.uid && (
        <SettingRow
          icon={'i delete'}
          name={`Clear Chat`}
          description="Delete all existing chat messages"
          disabled={false}
          content={
            <Button
              color="red"
              icon
              labelPosition="left"
              onClick={() => clearChat()}
            >
              <Icon name="delete" />
              Delete Messages
            </Button>
          }
        />
      )}
      {owner && owner === user?.uid && (
        <SettingRow
          icon={'linkify'}
          name={`Set Custom Room URL`}
          description="Set a custom URL for this room. Inappropriate names may be revoked."
          checked={Boolean(roomLock)}
          disabled={!isSubscriber}
          subOnly={true}
          content={
            <React.Fragment>
              <Input
                value={vanity}
                disabled={!isSubscriber}
                onChange={(e) => {
                  setAdminSettingsChanged(true);
                  checkValidVanity(e.target.value);
                  setVanity(e.target.value);
                }}
                label={<Label>{`${window.location.origin}/r/`}</Label>}
                loading={validVanityLoading}
                fluid
                size="mini"
                icon
                action={
                  validVanity ? (
                    <Icon name="checkmark" color="green" />
                  ) : (
                    <Icon name="close" color="red" />
                  )
                }
              ></Input>
            </React.Fragment>
          }
        />
      )}
      {owner && owner === user?.uid && (
        <SettingRow
          icon={'pencil'}
          name={`Set Room Title, Description & Color`}
          description="Set the room title, description and title color to be displayed in the top bar."
          disabled={!isSubscriber}
          subOnly={true}
          content={
            <React.Fragment>
              <div style={{ display: 'flex', marginBottom: 2 }}>
                <Input
                  style={{ marginRight: 3, flexGrow: 1 }}
                  value={roomTitleInput ?? roomTitle}
                  disabled={!isSubscriber}
                  maxLength={roomTitleMaxCharLength}
                  onChange={(e) => {
                    setAdminSettingsChanged(true);
                    setRoomTitleInput(e.target.value);
                  }}
                  placeholder={`Title (max. ${roomTitleMaxCharLength} characters)`}
                  fluid
                  size="mini"
                  icon
                ></Input>
                <Popup
                  content={
                    <React.Fragment>
                      <h5>Edit Title Color</h5>
                      <HexColorPicker
                        color={
                          roomTitleColorInput ||
                          roomTitleColor ||
                          defaultRoomTitleColor
                        }
                        onChange={(e) => {
                          setAdminSettingsChanged(true);
                          setRoomTitleColorInput(e);
                        }}
                      />
                      <div
                        style={{
                          marginTop: 8,
                          paddingLeft: 4,
                          borderLeft: `24px solid ${roomTitleColorInput}`,
                        }}
                      >
                        {roomTitleColorInput?.toUpperCase()}
                      </div>
                    </React.Fragment>
                  }
                  on="click"
                  trigger={
                    <Button
                      icon
                      color="teal"
                      size="tiny"
                      style={{ margin: 0 }}
                      disabled={!isSubscriber}
                    >
                      <Icon name="paint brush" />
                    </Button>
                  }
                />
              </div>
              <Input
                style={{ marginBottom: 2 }}
                value={roomDescriptionInput ?? roomDescription}
                disabled={!isSubscriber}
                maxLength={roomDescriptionMaxCharLength}
                onChange={(e) => {
                  setAdminSettingsChanged(true);
                  setRoomDescriptionInput(e.target.value);
                }}
                placeholder={`Description (max. ${roomDescriptionMaxCharLength} characters)`}
                fluid
                size="mini"
                icon
              ></Input>
            </React.Fragment>
          }
        />
      )}
      <div
        style={{
          borderTop: '3px dashed white',
          marginTop: 10,
          marginBottom: 10,
        }}
      />
      {owner && owner === user?.uid && (
        <Button
          primary
          disabled={!validVanity || !adminSettingsChanged}
          labelPosition="left"
          icon
          fluid
          onClick={() => {
            setRoomState({
              vanity: vanity,
              password: password,
              isChatDisabled: isChatDisabled,
              roomTitle: roomTitleInput ?? roomTitle,
              roomDescription: roomDescriptionInput ?? roomDescription,
              roomTitleColor:
                roomTitleColorInput || roomTitleColor || defaultRoomTitleColor,
              mediaPath: mediaPath,
            });
            setAdminSettingsChanged(false);
          }}
        >
          <Icon name="save" />
          Save Admin Settings
        </Button>
      )}
      <div className="sectionHeader">Local Settings</div>
      <SettingRow
        updateTS={updateTS}
        icon="bell"
        name="Disable chat notification sound"
        description="Don't play a sound when a chat message is sent while you're on another tab"
        checked={Boolean(getCurrentSettings().disableChatSound)}
        disabled={false}
        onChange={(_e, data) => {
          updateSettings(
            JSON.stringify({
              ...getCurrentSettings(),
              disableChatSound: data.checked,
            })
          );
          setUpdateTS(Number(new Date()));
        }}
      />
    </div>
  );
}