semantic-ui-react#Input TypeScript Examples

The following examples show how to use semantic-ui-react#Input. 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: PasswordModal.tsx    From watchparty with MIT License 6 votes vote down vote up
PasswordModal = ({
  savedPasswords,
  roomId,
}: {
  savedPasswords: StringDict;
  roomId: string;
}) => {
  const setPassword = useCallback(() => {
    window.localStorage.setItem(
      'watchparty-passwords',
      JSON.stringify({
        ...savedPasswords,
        [roomId]: (document.getElementById('roomPassword') as HTMLInputElement)
          ?.value,
      })
    );
    window.location.reload();
  }, [savedPasswords, roomId]);
  return (
    <Modal inverted basic open>
      <Header as="h1" style={{ textAlign: 'center' }}>
        This room requires a password.
      </Header>
      <div style={{ display: 'flex', justifyContent: 'center' }}>
        <Input
          id="roomPassword"
          type="password"
          size="large"
          onKeyPress={(e: any) => e.key === 'Enter' && setPassword()}
          icon={
            <Icon onClick={setPassword} name="key" inverted circular link />
          }
        />
      </div>
    </Modal>
  );
}
Example #2
Source File: ExampleList.tsx    From plugin-vscode with Apache License 2.0 5 votes vote down vote up
public render() {
        return (
            <Grid className="welcome-page" divided>
                <Grid.Row className="welcome-navbar" columns={1}>
                    <Grid.Column>
                        <Header as="h3" dividing>
                            Ballerina Examples
                        </Header>
                        {this.state && this.state.noSearchReults ?
                            (<>No search results found!</>) : null
                        }
                        {this.state && this.state.samples && this.state.samples.length > 0 ?
                            (
                                <Form>
                                    <Form.Field inline>
                                        <Input
                                            ref={(ref) => {
                                                this.searchInput = ref as Input;
                                            }}
                                            loading={!this.state || !this.state.samples}
                                            placeholder="Search"
                                            onChange={(event: React.SyntheticEvent<HTMLInputElement>) => {
                                                this.setState({
                                                    searchQuery: event.currentTarget.value,
                                                });
                                                this.onSearchQueryEdit();
                                            }}
                                            className="search-control"
                                        />
                                    </Form.Field>
                                </Form>
                            ) : (
                                <>No Samples found in [BALLERINA_HOME]/examples folder.</>
                            )
                        }
                    </Grid.Column>
                </Grid.Row>
                <Grid.Row className="welcome-content-wrapper">
                    <Grid.Column mobile={16} tablet={16} computer={16} className="rightContainer">
                        <Grid>
                            {this.state && this.state.samples &&
                                <Grid.Row columns={4} className="sample-wrapper">
                                    {
                                        this.getColumnContents().map((column, index) => {
                                            return (
                                                <Grid.Column key={index} mobile={16} tablet={8} computer={4}>
                                                    {column.map((columnItem) => this.renderColumnItem(columnItem))}
                                                </Grid.Column>
                                            );
                                        })
                                    }
                                </Grid.Row>
                            }
                        </Grid>
                    </Grid.Column>
                </Grid.Row>
            </Grid>);
    }
Example #3
Source File: ExampleList.tsx    From plugin-vscode with Apache License 2.0 5 votes vote down vote up
private searchInput: Input | undefined;
Example #4
Source File: Members.tsx    From Riakuto-StartingReact-ja3.1 with Apache License 2.0 5 votes vote down vote up
Members: VFC<Props> = ({ orgCodeList, prefetch = () => undefined }) => {
  const [orgCode, setOrgCode] = useState('');
  const [input, setInput] = useState('');
  const [isPending, startTransition] = useTransition();
  const ebKey = useRef(0);

  const menuItems = orgCodeList.map((code) => ({
    key: code,
    name: capitalize(code),
    onClick: () => {
      setInput('');

      if (orgCode) {
        startTransition(() => setOrgCode(code));
      } else {
        setOrgCode(code);
      }
    },
    onMouseOver: () => prefetch(code),
    active: code === orgCode,
  }));

  const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
    event.preventDefault();
    setOrgCode(input.toLowerCase().trim());
  };

  return (
    <>
      <header className="app-header">
        <h1>組織メンバーリスト</h1>
      </header>
      <form className="search-form" onSubmit={handleSubmit}>
        <Input
          placeholder="組織コードを入力..."
          type="text"
          value={input}
          onChange={(_, data) => setInput(data.value)}
        />
        <Button type="submit" disabled={isPending} primary>
          検索
        </Button>
      </form>
      <Menu items={menuItems} text />
      <Divider />
      <div className={isPending ? 'loading' : ''}>
        <ErrorBoundary
          statusMessages={{
            404: `‘${orgCode}’ というコードの組織は見つかりません`,
          }}
          onError={() => {
            ebKey.current += 1;
          }}
          key={ebKey.current}
        >
          <SuspenseList revealOrder="forwards">
            <Suspense fallback={<Spinner size="small" />}>
              <OrgInfo orgCode={orgCode} />
            </Suspense>
            <Suspense fallback={<Spinner size="large" />}>
              <MemberList orgCode={orgCode} />
            </Suspense>
          </SuspenseList>
        </ErrorBoundary>
      </div>
    </>
  );
}
Example #5
Source File: Members2.tsx    From Riakuto-StartingReact-ja3.1 with Apache License 2.0 5 votes vote down vote up
Members: VFC<Props> = ({ orgCodeList, prefetch = () => undefined }) => {
  const [orgCode, setOrgCode] = useState('');
  const [input, setInput] = useState('');
  const [isPending, startTransition] = useTransition();
  const { reset } = useQueryErrorResetBoundary();

  const menuItems = orgCodeList.map((code) => ({
    key: code,
    name: capitalize(code),
    onClick: () => {
      setInput('');

      if (orgCode) {
        startTransition(() => setOrgCode(code));
      } else {
        setOrgCode(code);
      }
    },
    onMouseOver: () => prefetch(code),
    active: code === orgCode,
  }));

  const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
    event.preventDefault();
    setOrgCode(input.toLowerCase().trim());
  };

  return (
    <>
      <header className="app-header">
        <h1>組織メンバーリスト</h1>
      </header>
      <form className="search-form" onSubmit={handleSubmit}>
        <Input
          placeholder="組織コードを入力..."
          type="text"
          value={input}
          onChange={(_, data) => setInput(data.value)}
        />
        <Button type="submit" disabled={isPending} primary>
          検索
        </Button>
      </form>
      <Menu items={menuItems} text />
      <Divider />
      <div className={isPending ? 'loading' : ''}>
        <ErrorBoundary
          fallbackRender={({ resetErrorBoundary }) => (
            <>
              <Message warning>
                {orgCode} というコードの組織は見つかりません
              </Message>
              <Button color="olive" onClick={() => resetErrorBoundary()}>
                エラーをリセット
              </Button>
            </>
          )}
          onReset={() => reset()}
        >
          <SuspenseList revealOrder="forwards">
            <Suspense fallback={<Spinner size="small" />}>
              <OrgInfo orgCode={orgCode} />
            </Suspense>
            <Suspense fallback={<Spinner size="large" />}>
              <MemberList orgCode={orgCode} />
            </Suspense>
          </SuspenseList>
        </ErrorBoundary>
      </div>
    </>
  );
}
Example #6
Source File: index.tsx    From chartsy with GNU General Public License v3.0 4 votes vote down vote up
Search: React.FC<Props> = ({ searchType, setSearchType }) => {
  const { dispatch } = useImageGrid();

  const [activeButton, setActiveButton] = useState("music");
  const [results, setResults] = useState<Image[]>([]);
  const [tmp, setVal] = useState("");
  const [search, setSearch] = useState("");

  useEffect(() => {
    const download = async () => {
      if (search === "") {
        return;
      }

      switch (searchType) {
        case SearchType.Games:
          const albums = await getGame(search);
          setResults(albums);
          break;

        case SearchType.Movies:
          const movies = await getMovie(search);
          setResults(movies);
          break;

        case SearchType.Series:
          const series = await getSeries(search);
          setResults(series);
          break;

        default:
          const games = await getAlbum(search);
          setResults(games);
          break;
      }
    };

    download();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [search]);

  return (
    <div id="search">
      <Grid.Row>
        <Button.Group fluid>
          <Button
            basic
            active={activeButton === "music"}
            icon="music"
            onClick={(e) => {
              dispatch({ type: "reset" });
              setSearchType(SearchType.Music);
              setActiveButton("music");
              e.preventDefault();
            }}
          />
          <Button
            basic
            active={activeButton === "game"}
            icon="game"
            onClick={(e) => {
              dispatch({ type: "reset" });
              setSearchType(SearchType.Games);
              setActiveButton("game");
              e.preventDefault();
            }}
          />
          {/* <Button */}
          {/*   basic */}
          {/*   active={activeButton === "film"} */}
          {/*   icon="film" */}
          {/*   onClick={(e) => { */}
          {/*     setSearchType(SearchType.Movies); */}
          {/*     setActiveButton("film"); */}
          {/*     e.preventDefault(); */}
          {/*   }} */}
          {/* /> */}
          {/* <Button */}
          {/*   basic */}
          {/*   active={activeButton === "tv"} */}
          {/*   icon="tv" */}
          {/*   onClick={(e) => { */}
          {/*     setSearchType(SearchType.Series); */}
          {/*     setActiveButton("tv"); */}
          {/*     e.preventDefault(); */}
          {/*   }} */}
          {/* /> */}
        </Button.Group>
        <form
          className="search-bar"
          onSubmit={(e) => {
            setSearch(tmp);
            e.preventDefault();
          }}
        >
          <Input
            fluid
            placeholder={`Search...${searchType === SearchType.Games ? " powered by RAWG.io" : ""}`}
            value={tmp}
            onChange={(e) => {
              setVal(e.target.value);
              e.preventDefault();
            }}
          />
        </form>
      </Grid.Row>
      <Grid.Row>
        <Grid className="results" centered padded>
          {results.map((img) => (
            <Grid.Row>
              <ImageCard onGrid={false} key={img.url} img={img} showTitle={true} />
            </Grid.Row>
          ))}
        </Grid>
      </Grid.Row>
    </div>
  );
}
Example #7
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 #8
Source File: Chat.tsx    From watchparty with MIT License 4 votes vote down vote up
render() {
    return (
      <div
        className={this.props.className}
        style={{
          display: this.props.hide ? 'none' : 'flex',
          flexDirection: 'column',
          flexGrow: 1,
          minHeight: 0,
          marginTop: 0,
          marginBottom: 0,
          padding: '8px',
        }}
      >
        <div
          className="chatContainer"
          ref={this.messagesRef}
          style={{ position: 'relative', paddingTop: 13 }}
        >
          <Comment.Group>
            {this.props.chat.map((msg) => (
              <ChatMessage
                key={msg.timestamp + msg.id}
                className={
                  msg.id === this.state.reactionMenu.selectedMsgId &&
                  msg.timestamp === this.state.reactionMenu.selectedMsgTimestamp
                    ? classes.selected
                    : ''
                }
                message={msg}
                pictureMap={this.props.pictureMap}
                nameMap={this.props.nameMap}
                formatMessage={this.formatMessage}
                owner={this.props.owner}
                user={this.props.user}
                socket={this.props.socket}
                isChatDisabled={this.props.isChatDisabled}
                setReactionMenu={this.setReactionMenu}
                handleReactionClick={this.handleReactionClick}
              />
            ))}
            {/* <div ref={this.messagesEndRef} /> */}
          </Comment.Group>
          {!this.state.isNearBottom && (
            <Button
              size="tiny"
              onClick={this.scrollToBottom}
              style={{
                position: 'sticky',
                bottom: 0,
                display: 'block',
                margin: '0 auto',
              }}
            >
              Jump to bottom
            </Button>
          )}
        </div>
        <Separator />
        {this.state.isPickerOpen && (
          <PickerMenu
            addEmoji={this.addEmoji}
            closeMenu={() => this.setState({ isPickerOpen: false })}
          />
        )}
        <CSSTransition
          in={this.state.reactionMenu.isOpen}
          timeout={300}
          classNames={{
            enter: classes['reactionMenu-enter'],
            enterActive: classes['reactionMenu-enter-active'],
            exit: classes['reactionMenu-exit'],
            exitActive: classes['reactionMenu-exit-active'],
          }}
          unmountOnExit
        >
          <ReactionMenu
            handleReactionClick={this.handleReactionClick}
            closeMenu={() => this.setReactionMenu(false)}
            yPosition={this.state.reactionMenu.yPosition}
            xPosition={this.state.reactionMenu.xPosition}
          />
        </CSSTransition>
        <Input
          inverted
          fluid
          onKeyPress={(e: any) => e.key === 'Enter' && this.sendChatMsg()}
          onChange={this.updateChatMsg}
          value={this.state.chatMsg}
          error={this.chatTooLong()}
          icon
          disabled={this.props.isChatDisabled}
          placeholder={
            this.props.isChatDisabled
              ? 'The chat was disabled by the room owner.'
              : 'Enter a message...'
          }
        >
          <input />
          <Icon
            // style={{ right: '40px' }}
            onClick={() => this.setState({ isPickerOpen: true })}
            name={'' as any}
            inverted
            circular
            link
            disabled={this.props.isChatDisabled}
            style={{ opacity: 1 }}
          >
            <span role="img" aria-label="Emoji">
              ?
            </span>
          </Icon>
          {/* <Icon onClick={this.sendChatMsg} name="send" inverted circular link /> */}
        </Input>
      </div>
    );
  }
Example #9
Source File: ComboBox.tsx    From watchparty with MIT License 4 votes vote down vote up
render() {
    const { currentMedia, getMediaDisplayName } = this.props;
    const { results } = this.state;
    return (
      <div style={{ position: 'relative' }}>
        <div style={{ display: 'flex', gap: '4px' }}>
          <Input
            style={{ flexGrow: 1 }}
            inverted
            fluid
            focus
            disabled={this.props.disabled}
            onChange={this.doSearch}
            onFocus={async (e: any) => {
              e.persist();
              this.setState(
                {
                  inputMedia: getMediaDisplayName(currentMedia),
                },
                () => {
                  if (
                    !this.state.inputMedia ||
                    (this.state.inputMedia &&
                      this.state.inputMedia.startsWith('http'))
                  ) {
                    this.doSearch(e);
                  }
                }
              );
              setTimeout(() => e.target.select(), 100);
            }}
            onBlur={() => {
              setTimeout(
                () =>
                  this.setState({ inputMedia: undefined, results: undefined }),
                100
              );
            }}
            onKeyPress={(e: any) => {
              if (e.key === 'Enter') {
                this.setMediaAndClose(e, {
                  value: this.state.inputMedia,
                });
              }
            }}
            icon={
              <Icon
                onClick={(e: any) =>
                  this.setMediaAndClose(e, {
                    value: this.state.inputMedia,
                  })
                }
                name="arrow right"
                link
                circular
                //bordered
              />
            }
            loading={this.state.loading}
            label={'Now Watching:'}
            placeholder="Enter video file URL, YouTube link, or YouTube search term"
            value={
              this.state.inputMedia !== undefined
                ? this.state.inputMedia
                : getMediaDisplayName(currentMedia)
            }
          />
          <Dropdown
            icon="list"
            labeled
            className="icon"
            button
            text={`Playlist (${this.props.playlist.length})`}
            scrolling
          >
            <Dropdown.Menu direction="left">
              {this.props.playlist.length === 0 && (
                <Dropdown.Item disabled>
                  There are no items in the playlist.
                </Dropdown.Item>
              )}
              {this.props.playlist.map((item: PlaylistVideo, index: number) => {
                return (
                  <Dropdown.Item>
                    <div style={{ maxWidth: '500px' }}>
                      <ChatVideoCard
                        video={item}
                        index={index}
                        controls
                        onPlay={(index) => {
                          this.props.setMedia(null, {
                            value: this.props.playlist[index]?.url,
                          });
                          this.props.playlistDelete(index);
                        }}
                        onPlayNext={(index) => {
                          this.props.playlistMove(index, 0);
                        }}
                        onRemove={(index) => {
                          this.props.playlistDelete(index);
                        }}
                        disabled={this.props.disabled}
                        isYoutube={Boolean(item.img)}
                      />
                    </div>
                  </Dropdown.Item>
                );
              })}
            </Dropdown.Menu>
          </Dropdown>
        </div>
        {Boolean(results) && this.state.inputMedia !== undefined && (
          <Menu
            fluid
            vertical
            style={{
              position: 'absolute',
              top: '22px',
              maxHeight: '250px',
              overflow: 'scroll',
              zIndex: 1001,
            }}
          >
            {results}
          </Menu>
        )}
      </div>
    );
  }
Example #10
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>
  );
}
Example #11
Source File: profile_crew2.tsx    From website with MIT License 4 votes vote down vote up
render() {
		const { includeFrozen, excludeFF, onlyEvent, activeItem, searchFilter } = this.state;
		let { data, itemsReady } = this.state;

		const { isMobile } = this.props;

		if (!includeFrozen) {
			data = data.filter(crew => crew.immortal === 0);
		}

		if (excludeFF) {
			data = data.filter(crew => crew.rarity < crew.max_rarity);
		}

		if (searchFilter) {
			data = data.filter(
				crew =>
					crew.name.toLowerCase().indexOf(searchFilter) !== -1 ||
					crew.traits_named.some(t => t.toLowerCase().indexOf(searchFilter) !== -1) ||
					crew.traits_hidden.some(t => t.toLowerCase().indexOf(searchFilter) !== -1)
			);
		}

		const zoomFactor = isMobile ? 0.65 : 0.85;

		let opts = [];
		if (activeItem === '' || activeItem === this.state.defaultColumn) {
			opts = ['Default Sort', 'Crew Level', 'Crew Rarity', 'Alphabetical'];
		} else {
			opts = ['Base Skill', 'Proficiency Skill', 'Combined Skill'];
		}

		let settings = ['Include Frozen', 'Exclude FF'];
		let eventCrew = bonusCrewForCurrentEvent(this.props.playerData.player.character);
		if (eventCrew) {
			console.log(eventCrew);
			settings.push(`Only event bonus (${eventCrew.eventName})`);

			if (onlyEvent) {
				data = data.filter(crew => eventCrew.eventCrew[crew.symbol]);
			}
		}

		return (
			<div>
				<Menu attached={isMobile ? false : 'top'} fixed={isMobile ? 'top' : undefined}>
					<Menu.Item
						name="command_skill"
						active={activeItem === 'command_skill'}
						onClick={(e, { name }) => this._handleSortNew({activeItem: name})}
					>
						<img src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_command_skill.png`} />
					</Menu.Item>
					<Menu.Item
						name="diplomacy_skill"
						active={activeItem === 'diplomacy_skill'}
						onClick={(e, { name }) => this._handleSortNew({activeItem: name})}
					>
						<img src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_diplomacy_skill.png`} />
					</Menu.Item>
					<Menu.Item
						name="engineering_skill"
						active={activeItem === 'engineering_skill'}
						onClick={(e, { name }) => this._handleSortNew({activeItem: name})}
					>
						<img src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_engineering_skill.png`} />
					</Menu.Item>
					<Menu.Item
						name="security_skill"
						active={activeItem === 'security_skill'}
						onClick={(e, { name }) => this._handleSortNew({activeItem: name})}
					>
						<img src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_security_skill.png`} />
					</Menu.Item>
					<Menu.Item
						name="medicine_skill"
						active={activeItem === 'medicine_skill'}
						onClick={(e, { name }) => this._handleSortNew({activeItem: name})}
					>
						<img src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_medicine_skill.png`} />
					</Menu.Item>
					<Menu.Item
						name="science_skill"
						active={activeItem === 'science_skill'}
						onClick={(e, { name }) => this._handleSortNew({activeItem: name})}
					>
						<img src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_science_skill.png`} />
					</Menu.Item>

					<DropdownOpts
						opts={opts}
						settings={settings}
						onChange={text => this._onChange(text)}
						onSettingChange={(text, val) => this._onSettingChange(text, val)}
					/>

					<Menu.Menu position="right">
						<Menu.Item>
							<Input
								icon="search"
								placeholder="Search..."
								value={this.state.searchFilter}
								onChange={(e, { value }) => this._onChangeFilter(value)}
							/>
						</Menu.Item>
					</Menu.Menu>
				</Menu>

				<Segment
					attached={isMobile ? false : 'bottom'}
					style={isMobile ? { paddingTop: '6em', paddingBottom: '2em' } : {}}
				>
					<div
						style={{
							display: 'grid',
							gridTemplateColumns: `repeat(auto-fit, minmax(${(zoomFactor * 22).toFixed(2)}em, 1fr))`,
							gap: '1em'
						}}
					>
						{data.map((crew, idx) => (
							<VaultCrew key={idx} crew={crew} size={zoomFactor} itemsReady={itemsReady} />
						))}
					</div>
				</Segment>
			</div>
		);
	}
Example #12
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 #13
Source File: missionslist.tsx    From website with MIT License 4 votes vote down vote up
MissionsList = (props: MissionsListProps) => {
	const { groupId, shuttlers, setShuttlers, activeShuttles } = props;

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

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

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

		const checkOptions = [];

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	function updateShuttlers(): void {
		setShuttlers({...shuttlers});
	}
}
Example #14
Source File: crewchallenge.tsx    From website with MIT License 4 votes vote down vote up
CrewPicker = (props: CrewPickerProps) => {
	const { rules, guesses, handleSelect } = props;
	const portalCrew = React.useContext(PortalCrewContext);

	const [modalIsOpen, setModalIsOpen] = React.useState(false);
	const [searchFilter, setSearchFilter] = React.useState('');
	const [paginationPage, setPaginationPage] = React.useState(1);
	const [selectedCrew, setSelectedCrew] = React.useState(undefined);
	const [showHints, setShowHints] = React.useState(true);

	const guessesLeft = rules.guesses - guesses.length;

	const inputRef = React.createRef();

	React.useEffect(() => {
		if (modalIsOpen) inputRef.current.focus();
	}, [modalIsOpen]);

	return (
		<Modal
			open={modalIsOpen}
			onClose={() => setModalIsOpen(false)}
			onOpen={() => setModalIsOpen(true)}
			trigger={renderButton()}
			size='tiny'
			centered={false}
			closeIcon
		>
			<Modal.Header>
				<Input ref={inputRef}
					size='mini' fluid
					iconPosition='left'
					placeholder='Search for crew by name'
					value={searchFilter}
					onChange={(e, { value }) => { setSearchFilter(value); setPaginationPage(1); setSelectedCrew(undefined); }}>
						<input />
						<Icon name='search' />
						<Button icon onClick={() => { setSearchFilter(''); setPaginationPage(1); setSelectedCrew(undefined); inputRef.current.focus(); }} >
							<Icon name='delete' />
						</Button>
				</Input>
			</Modal.Header>
			<Modal.Content scrolling>
				{renderGrid()}
			</Modal.Content>
			<Modal.Actions>
				<Button content='Show hints' onClick={() => setShowHints(!showHints) } />
				{selectedCrew && (
					<Button color='blue'
						content={`Guess ${selectedCrew.name}`}
						onClick={() => confirmGuess(selectedCrew.symbol)} />
				)}
				{!selectedCrew && (
					<Button content='Close' onClick={() => setModalIsOpen(false)} />
				)}
			</Modal.Actions>
		</Modal>
	);

	function renderButton(): JSX.Element {
		return (
			<Button fluid size='big' color='blue'>
				<Icon name='zoom-in' />
				Guess Crew
				<span style={{ fontSize: '.95em', fontWeight: 'normal', paddingLeft: '1em' }}>
					({guessesLeft} guess{guessesLeft !== 1 ? 'es' : ''} remaining)
				</span>
			</Button>
		);
	}

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

		let data = portalCrew.slice();

		if (rules.excludedCrew.length > 0)
			data = data.filter(crew => !rules.excludedCrew.includes(crew.symbol));

		// Filtering
		if (searchFilter !== '') {
			const filter = (input: string) => input.toLowerCase().indexOf(searchFilter.toLowerCase()) >= 0;
			data = data.filter(crew => filter(crew.name));
		}
		if (data.length === 0) return (
			<Message>
				<p>No crew names match your current search.</p>
				<p>Only crew that are currently <b>available in the time portal</b> will be used as mystery crew and valid guesses.</p>
			</Message>
		);

		// Pagination
		const itemsPerPage = 24, itemsToShow = itemsPerPage*paginationPage;

		return (
			<div>
				<Grid doubling columns={3} textAlign='center'>
					{data.slice(0, itemsToShow).map(crew => (
						<Grid.Column key={crew.symbol} style={{ cursor: 'pointer' }}
							onClick={() => { if (!guesses.includes(crew.symbol)) setSelectedCrew(crew); }}
							onDoubleClick={() => { if (!guesses.includes(crew.symbol)) confirmGuess(crew.symbol); }}
							color={selectedCrew?.symbol === crew.symbol ? 'blue' : null}
						>
							<img width={48} height={48} src={`${process.env.GATSBY_ASSETS_URL}${crew.imageUrlPortrait}`} />
							<div>
								{guesses.includes(crew.symbol) && (<Icon name='x' color='red' />)}
								{crew.name}
							</div>
							{!showHints && (
								<div>({[crew.series.toUpperCase(), `${crew.max_rarity}*`].join(', ')})</div>
							)}
						</Grid.Column>
					))}
				</Grid>
				{itemsToShow < data.length && (
					<InView as='div' style={{ margin: '2em 0', textAlign: 'center' }}
						onChange={(inView, entry) => { if (inView) setPaginationPage(prevState => prevState + 1); }}
					>
						<Icon loading name='spinner' /> Loading...
					</InView>
				)}
				{itemsToShow >= data.length && (
					<Message>Tip: Double-tap a crew to make your guess more quickly.</Message>
				)}
			</div>
		);
	}

	function confirmGuess(symbol: string): void {
		handleSelect(symbol);
		setModalIsOpen(false);
		setSelectedCrew(undefined);
	}
}