react-icons/fa#FaEdit TypeScript Examples

The following examples show how to use react-icons/fa#FaEdit. 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: TopNavbar.tsx    From 3Speak-app with GNU General Public License v3.0 4 votes vote down vote up
export function TopNavbar() {
  const [inEdit, setInEdit] = useState(false)
  const urlForm = useRef<any>()
  const [urlSplit, setUrlSplit] = useState([])

  const startEdit = () => {
    setInEdit(true)
  }

  useEffect(() => {
    if (inEdit) {
      urlForm.current?.focus()
    }
  }, [inEdit])

  const exitEdit = () => {
    setInEdit(false)
  }

  const finishEdit = (e) => {
    if (e.keyCode === 13) {
      if (location.hash !== `#${e.target.value}`) {
        location.replace(`#${e.target.value}`)
        location.reload()
      }
      setInEdit(false)
    } else if (e.keyCode === 27) {
      exitEdit()
    }
  }

  const updateUrlSplit = () => {
    const hash = window.location.hash
    const theUrlSplit = hash.split('/')
    theUrlSplit.splice(0, 1)

    if (theUrlSplit[0] === 'watch') {
      const pagePerm = theUrlSplit[1]
      const pagePermSpliced = pagePerm.split(':')
      pagePermSpliced.splice(0, 1)
      theUrlSplit.pop()
      pagePermSpliced.forEach((onePagePerm) => {
        theUrlSplit.push(onePagePerm)
      })

      setUrlSplit(theUrlSplit)
    } else {
      setUrlSplit(theUrlSplit)
    }
  }

  useEffect(() => {
    updateUrlSplit()
  }, [])

  useEffect(() => {
    window.addEventListener('hashchange', function (event) {
      updateUrlSplit()
    })
  }, [])

  const userProfileUrl = useMemo(() => {
    const windowLocationHash = window.location.hash
    const windowLocationSearch = windowLocationHash.search('#')
    const windowLocationHref = windowLocationHash.slice(windowLocationSearch)
    const hrefSegments = windowLocationHref.split('/')
    hrefSegments.splice(0, 1)

    let userProfileUrl = '#/user/'

    if (hrefSegments[0] === 'watch') {
      const userProfileUrlInit = hrefSegments[1]
      const userProfileUrlSpliced = userProfileUrlInit.split(':')
      userProfileUrlSpliced.pop()

      userProfileUrlSpliced.forEach((one) => {
        if (one === userProfileUrlSpliced[0]) {
          userProfileUrl = userProfileUrl + one + ':'
        } else {
          userProfileUrl = userProfileUrl + one
        }
      })
    }

    return userProfileUrl
  }, [])

  return (
    <div>
      <Navbar bg="light" expand="lg">
        <Navbar.Collapse id="basic-navbar-nav">
          <Nav className="mr-auto">
            {!inEdit ? (
              <>
                <Breadcrumb>
                  <Breadcrumb.Item href="#/">Home</Breadcrumb.Item>
                  {urlSplit.map((el) =>
                    el === updateUrlSplit[1] && updateUrlSplit[0] === 'watch' ? (
                      <Breadcrumb.Item href={userProfileUrl} key={el} id={el}>
                        {el}
                      </Breadcrumb.Item>
                    ) : (
                      <Breadcrumb.Item href={'#'} key={el} id={el}>
                        {el}
                      </Breadcrumb.Item>
                    ),
                  )}
                </Breadcrumb>
                <Button
                  className="btn btn-light btn-sm"
                  style={{
                    marginLeft: '5px',
                    width: '40px',
                    height: '40px',
                    padding: '3.5%',
                    verticalAlign: 'baseline',
                  }}
                  onClick={startEdit}
                >
                  <FaEdit style={{ textAlign: 'center', verticalAlign: 'initial' }} />
                </Button>
              </>
            ) : (
              <FormControl
                ref={urlForm}
                defaultValue={(() => {
                  return location.hash.slice(1)
                })()}
                onKeyDown={finishEdit}
                onBlur={exitEdit}
              />
            )}
          </Nav>
          <Dropdown>
            <Dropdown.Toggle variant="secondary" size="lg">
              Options
            </Dropdown.Toggle>

            <Dropdown.Menu>
              <Dropdown.Item onClick={() => copyToClip(window.location.hash)}>
                Copy Current URL{' '}
                <FaCopy size={28} onClick={() => copyToClip(window.location.hash)} />
              </Dropdown.Item>
              <Dropdown.Item onClick={goToClip}>
                Go to Copied URL <FaArrowRight size={28} />
              </Dropdown.Item>
            </Dropdown.Menu>
          </Dropdown>
          <Nav>
            <Nav.Link>
              <FaAngleLeft size={28} onClick={goBack} />
            </Nav.Link>
            <Nav.Link>
              <FaAngleRight size={28} onClick={goForth} />
            </Nav.Link>
          </Nav>
        </Navbar.Collapse>
      </Navbar>
    </div>
  )
}
Example #2
Source File: MobileSettings.tsx    From hub with Apache License 2.0 4 votes vote down vote up
MobileSettings = (props: Props) => {
  const { ctx } = useContext(AppCtx);
  const [openSideBarStatus, setOpenSideBarStatus] = useState(false);

  const getSidebarIcon = (): JSX.Element => {
    if (ctx.user) {
      if (ctx.user.profileImageId) {
        return (
          <Image
            imageId={ctx.user.profileImageId}
            alt="User profile"
            className={`rounded-circle mw-100 mh-100 h-auto border border-2 ${styles.profileImage}`}
            placeholderIcon={<FaUserCircle />}
          />
        );
      } else {
        return <FaUserCircle />;
      }
    } else {
      return <GoThreeBars />;
    }
  };

  return (
    <div className={`btn-group navbar-toggler pe-0 ms-auto border-0 fs-6 bg-transparent ${styles.navbarToggler}`}>
      {isUndefined(ctx.user) ? (
        <div className="spinner-grow spinner-grow-sm textLight pt-2" role="status">
          <span className="visually-hidden">Loading...</span>
        </div>
      ) : (
        <Sidebar
          label="User settings"
          className="d-inline-block d-lg-none"
          buttonType="position-relative btn text-secondary pe-0 ps-3"
          buttonIcon={
            <div
              className={classnames(
                'rounded-circle d-flex align-items-center justify-content-center lh-1 fs-3 bg-white',
                styles.iconWrapper
              )}
            >
              {getSidebarIcon()}
            </div>
          }
          direction="right"
          header={
            <>
              {!isNull(ctx.user) && (
                <div className="h6 mb-0 text-dark flex-grow-1">
                  Signed in as <span className="fw-bold">{ctx.user.alias}</span>
                </div>
              )}
            </>
          }
          open={openSideBarStatus}
          onOpenStatusChange={(status: boolean) => setOpenSideBarStatus(status)}
        >
          <>
            {!isUndefined(ctx.user) && (
              <>
                {!isNull(ctx.user) ? (
                  <>
                    <ThemeMode device="mobile" onSelection={() => setOpenSideBarStatus(false)} />

                    <div className="dropdown-divider my-3" />

                    <Link
                      className="dropdown-item my-2"
                      onClick={() => {
                        setOpenSideBarStatus(false);
                      }}
                      to={{
                        pathname: '/stats',
                      }}
                    >
                      <div className="d-flex align-items-center">
                        <HiChartSquareBar className="me-2" />
                        <div>Stats</div>
                      </div>
                    </Link>

                    <Link
                      className="dropdown-item my-2"
                      to={{
                        pathname: '/packages/starred',
                      }}
                      onClick={() => setOpenSideBarStatus(false)}
                    >
                      <div className="d-flex align-items-center">
                        <FaStar className="me-2" />
                        <div>Starred packages</div>
                      </div>
                    </Link>

                    <Link
                      className="dropdown-item my-2"
                      to={{
                        pathname: '/control-panel',
                      }}
                      onClick={() => setOpenSideBarStatus(false)}
                    >
                      <div className="d-flex align-items-center">
                        <FaCog className="me-2" />
                        <div>Control Panel</div>
                      </div>
                    </Link>

                    <LogOut
                      className="my-2"
                      onSuccess={() => setOpenSideBarStatus(false)}
                      privateRoute={props.privateRoute}
                    />
                  </>
                ) : (
                  <>
                    <ThemeMode device="mobile" onSelection={() => setOpenSideBarStatus(false)} />

                    <div className="dropdown-divider my-3" />

                    <Link
                      className="dropdown-item my-2"
                      onClick={() => {
                        setOpenSideBarStatus(false);
                      }}
                      to={{
                        pathname: '/stats',
                      }}
                    >
                      <div className="d-flex align-items-center">
                        <HiChartSquareBar className="me-2" />
                        <div>Stats</div>
                      </div>
                    </Link>

                    <button
                      className="dropdown-item my-2"
                      onClick={() => {
                        setOpenSideBarStatus(false);
                        props.setOpenLogIn(true);
                      }}
                      aria-label="Open sign in modal"
                    >
                      <div className="d-flex align-items-center">
                        <FaSignInAlt className="me-2" />
                        <div>Sign in</div>
                      </div>
                    </button>

                    <button
                      className="dropdown-item my-2"
                      onClick={() => {
                        setOpenSideBarStatus(false);
                        props.setOpenSignUp(true);
                      }}
                      aria-label="Open sign up modal"
                    >
                      <div className="d-flex align-items-center">
                        <FaEdit className="me-2" />
                        <div>Sign up</div>
                      </div>
                    </button>
                  </>
                )}
              </>
            )}
          </>
        </Sidebar>
      )}
    </div>
  );
}
Example #3
Source File: ScansView.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
ScansView: React.FC = () => {
  const { apiGet, apiPost, apiDelete } = useAuthContext();
  const [showModal, setShowModal] = useState<Boolean>(false);
  const [selectedRow, setSelectedRow] = useState<number>(0);
  const [scans, setScans] = useState<Scan[]>([]);
  const [organizationOptions, setOrganizationOptions] = useState<
    OrganizationOption[]
  >([]);
  const [tags, setTags] = useState<OrganizationTag[]>([]);
  const [scanSchema, setScanSchema] = useState<ScanSchema>({});

  const columns: Column<Scan>[] = [
    {
      Header: 'Run',
      id: 'run',
      Cell: ({ row }: { row: { index: number } }) => (
        <div
          style={{ textAlign: 'center' }}
          onClick={() => {
            runScan(row.index);
          }}
        >
          <FaPlayCircle />
        </div>
      ),
      disableFilters: true
    },
    {
      Header: 'Name',
      accessor: 'name',
      width: 200,
      id: 'name',
      disableFilters: true
    },
    {
      Header: 'Tags',
      accessor: ({ tags }) => tags.map((tag) => tag.name).join(', '),
      width: 150,
      minWidth: 150,
      id: 'tags',
      disableFilters: true
    },
    {
      Header: 'Mode',
      accessor: ({ name }) =>
        scanSchema[name] && scanSchema[name].isPassive ? 'Passive' : 'Active',
      width: 150,
      minWidth: 150,
      id: 'mode',
      disableFilters: true
    },
    {
      Header: 'Frequency',
      accessor: ({ frequency, isSingleScan }) => {
        let val, unit;
        if (frequency < 60 * 60) {
          val = frequency / 60;
          unit = 'minute';
        } else if (frequency < 60 * 60 * 24) {
          val = frequency / (60 * 60);
          unit = 'hour';
        } else {
          val = frequency / (60 * 60 * 24);
          unit = 'day';
        }
        if (isSingleScan) {
          return 'Single Scan';
        }
        return `Every ${val} ${unit}${val === 1 ? '' : 's'}`;
      },
      width: 200,
      id: 'frequency',
      disableFilters: true
    },
    {
      Header: 'Last Run',
      accessor: (args: Scan) => {
        return !args.lastRun ||
          new Date(args.lastRun).getTime() === new Date(0).getTime()
          ? 'None'
          : `${formatDistanceToNow(parseISO(args.lastRun))} ago`;
      },
      width: 200,
      id: 'lastRun',
      disableFilters: true
    },
    {
      Header: 'Edit',
      id: 'edit',
      Cell: ({ row }: CellProps<Scan>) => (
        <Link to={`/scans/${row.original.id}`} style={{ color: 'black' }}>
          <FaEdit />
        </Link>
      ),
      disableFilters: true
    },
    {
      Header: 'Delete',
      id: 'delete',
      Cell: ({ row }: { row: { index: number } }) => (
        <span
          onClick={() => {
            setShowModal(true);
            setSelectedRow(row.index);
          }}
        >
          <FaTimes />
        </span>
      ),
      disableFilters: true
    },
    {
      Header: 'Description',
      accessor: ({ name }) => scanSchema[name]?.description,
      width: 200,
      maxWidth: 200,
      id: 'description',
      disableFilters: true
    }
  ];
  const [errors, setErrors] = useState<Errors>({});

  const [values] = useState<ScanFormValues>({
    name: 'censys',
    arguments: '{}',
    organizations: [],
    frequency: 1,
    frequencyUnit: 'minute',
    isGranular: false,
    isUserModifiable: false,
    isSingleScan: false,
    tags: []
  });

  React.useEffect(() => {
    document.addEventListener('keyup', (e) => {
      //Escape
      if (e.keyCode === 27) {
        setShowModal(false);
      }
    });
  }, [apiGet]);

  const fetchScans = useCallback(async () => {
    try {
      const { scans, organizations, schema } = await apiGet<{
        scans: Scan[];
        organizations: Organization[];
        schema: ScanSchema;
      }>('/scans/');
      const tags = await apiGet<OrganizationTag[]>(`/organizations/tags`);
      setScans(scans);
      setScanSchema(schema);
      setOrganizationOptions(
        organizations.map((e) => ({ label: e.name, value: e.id }))
      );
      setTags(tags);
    } catch (e) {
      console.error(e);
    }
  }, [apiGet]);

  const deleteRow = async (index: number) => {
    try {
      const row = scans[index];
      await apiDelete(`/scans/${row.id}`, { body: {} });
      setScans(scans.filter((scan) => scan.id !== row.id));
    } catch (e) {
      setErrors({
        global:
          e.status === 422 ? 'Unable to delete scan' : e.message ?? e.toString()
      });
      console.log(e);
    }
  };

  const onSubmit = async (body: ScanFormValues) => {
    try {
      // For now, parse the arguments as JSON. We'll want to add a GUI for this in the future
      body.arguments = JSON.parse(body.arguments);
      setFrequency(body);

      const scan = await apiPost('/scans/', {
        body: {
          ...body,
          organizations: body.organizations
            ? body.organizations.map((e) => e.value)
            : [],
          tags: body.tags ? body.tags.map((e) => ({ id: e.value })) : []
        }
      });
      setScans(scans.concat(scan));
    } catch (e) {
      setErrors({
        global: e.message ?? e.toString()
      });
      console.log(e);
    }
  };

  const invokeScheduler = async () => {
    setErrors({ ...errors, scheduler: '' });
    try {
      await apiPost('/scheduler/invoke', { body: {} });
    } catch (e) {
      console.error(e);
      setErrors({ ...errors, scheduler: 'Invocation failed.' });
    }
  };

  /**
   * Manually runs a single scan, then immediately invokes the
   * scheduler so the scan is run.
   * @param index Row index
   */
  const runScan = async (index: number) => {
    const row = scans[index];
    try {
      await apiPost(`/scans/${row.id}/run`, { body: {} });
    } catch (e) {
      console.error(e);
      setErrors({ ...errors, scheduler: 'Run failed.' });
    }
    await invokeScheduler();
  };

  return (
    <>
      <Table<Scan> columns={columns} data={scans} fetchData={fetchScans} />
      <br></br>
      <Button type="submit" outline onClick={invokeScheduler}>
        Manually run scheduler
      </Button>
      {errors.scheduler && <p className={classes.error}>{errors.scheduler}</p>}
      <h2>Add a scan</h2>
      {errors.global && <p className={classes.error}>{errors.global}</p>}
      <ScanForm
        organizationOption={organizationOptions}
        tags={tags}
        propValues={values}
        onSubmit={onSubmit}
        type="create"
        scanSchema={scanSchema}
      ></ScanForm>
      <ImportExport<Scan>
        name="scans"
        fieldsToExport={['name', 'arguments', 'frequency']}
        onImport={async (results) => {
          // TODO: use a batch call here instead.
          const createdScans = [];
          for (const result of results) {
            createdScans.push(
              await apiPost('/scans/', {
                body: {
                  ...result,
                  // These fields are initially parsed as strings, so they need
                  // to be converted to objects.
                  arguments: JSON.parse(
                    ((result.arguments as unknown) as string) || ''
                  )
                }
              })
            );
          }
          setScans(scans.concat(...createdScans));
        }}
        getDataToExport={() =>
          scans.map((scan) => ({
            ...scan,
            arguments: JSON.stringify(scan.arguments)
          }))
        }
      />

      {showModal && (
        <div>
          <Overlay />
          <ModalContainer>
            <Modal
              actions={
                <>
                  <Button
                    outline
                    type="button"
                    onClick={() => {
                      setShowModal(false);
                    }}
                  >
                    Cancel
                  </Button>
                  <Button
                    type="button"
                    onClick={() => {
                      deleteRow(selectedRow);
                      setShowModal(false);
                    }}
                  >
                    Delete
                  </Button>
                </>
              }
              title={<h2>Delete scan?</h2>}
            >
              <p>
                Are you sure you would like to delete the{' '}
                <code>{scans[selectedRow].name}</code> scan?
              </p>
            </Modal>
          </ModalContainer>
        </div>
      )}
    </>
  );
}
Example #4
Source File: Puzzle.tsx    From crosshare with GNU Affero General Public License v3.0 4 votes vote down vote up
Puzzle = ({
  loadingPlayState,
  puzzle,
  play,
  ...props
}: PuzzleProps & AuthPropsOptional) => {
  const [state, dispatch] = useReducer(
    puzzleReducer,
    {
      type: 'puzzle',
      wasEntryClick: false,
      active: { col: 0, row: 0, dir: Direction.Across },
      grid: addClues(
        fromCells({
          mapper: (e) => e,
          width: puzzle.size.cols,
          height: puzzle.size.rows,
          cells: play
            ? play.g
            : puzzle.grid.map((s) => (s === BLOCK ? BLOCK : ' ')),
          vBars: new Set(puzzle.vBars),
          hBars: new Set(puzzle.hBars),
          allowBlockEditing: false,
          highlighted: new Set(puzzle.highlighted),
          highlight: puzzle.highlight,
          hidden: new Set(puzzle.hidden),
        }),
        puzzle.clues
      ),
      showExtraKeyLayout: false,
      answers: puzzle.grid,
      alternateSolutions: puzzle.alternateSolutions,
      verifiedCells: new Set<number>(play ? play.vc : []),
      wrongCells: new Set<number>(play ? play.wc : []),
      revealedCells: new Set<number>(play ? play.rc : []),
      downsOnly: play?.do || false,
      isEnteringRebus: false,
      rebusValue: '',
      success: play ? play.f : false,
      ranSuccessEffects: play ? play.f : false,
      filled: false,
      autocheck: false,
      dismissedKeepTrying: false,
      dismissedSuccess: false,
      moderating: false,
      showingEmbedOverlay: false,
      displaySeconds: play ? play.t : 0,
      bankedSeconds: play ? play.t : 0,
      ranMetaSubmitEffects: false,
      ...(play &&
        play.ct_rv && {
          contestRevealed: true,
          contestSubmitTime: play.ct_t?.toMillis(),
        }),
      ...(play &&
        play.ct_sub && {
          ranMetaSubmitEffects: true,
          contestPriorSubmissions: play.ct_pr_subs,
          contestDisplayName: play.ct_n,
          contestSubmission: play.ct_sub,
          contestEmail: play.ct_em,
          contestSubmitTime: play.ct_t?.toMillis(),
        }),
      currentTimeWindowStart: 0,
      didCheat: play ? play.ch : false,
      clueView: false,
      cellsUpdatedAt: play ? play.ct : puzzle.grid.map(() => 0),
      cellsIterationCount: play ? play.uc : puzzle.grid.map(() => 0),
      cellsEverMarkedWrong: new Set<number>(play ? play.we : []),
      loadedPlayState: !loadingPlayState,
      waitToResize: true,
      isEditable(cellIndex) {
        return !this.verifiedCells.has(cellIndex) && !this.success;
      },
    },
    advanceActiveToNonBlock
  );

  const authContext = useContext(AuthContext);
  useEffect(() => {
    if (!authContext.notifications?.length) {
      return;
    }
    for (const notification of authContext.notifications) {
      if (notification.r) {
        // shouldn't be possible but be defensive
        continue;
      }
      if (!isNewPuzzleNotification(notification)) {
        continue;
      }
      if (notification.p === puzzle.id) {
        App.firestore()
          .collection('n')
          .doc(notification.id)
          .update({ r: true });
        return;
      }
    }
  }, [authContext.notifications, puzzle.id]);

  useEffect(() => {
    if (loadingPlayState === false) {
      const action: LoadPlayAction = {
        type: 'LOADPLAY',
        play: play,
        prefs: props.prefs,
        isAuthor: props.user ? props.user.uid === puzzle.authorId : false,
      };
      dispatch(action);
    }
  }, [loadingPlayState, play, props.user, props.prefs, puzzle.authorId]);

  // Every (unpaused) second dispatch a tick action which updates the display time
  useEffect(() => {
    function tick() {
      if (state.currentTimeWindowStart) {
        dispatch({ type: 'TICKACTION' });
      }
    }
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, [state.currentTimeWindowStart, dispatch]);

  // Pause when page goes out of focus
  function prodPause() {
    if (process.env.NODE_ENV !== 'development') {
      dispatch({ type: 'PAUSEACTION' });
      writePlayToDBIfNeeded();
    }
  }
  useEventListener('blur', prodPause);

  const [muted, setMuted] = usePersistedBoolean('muted', false);
  const [toggleKeyboard, setToggleKeyboard] = usePersistedBoolean(
    'keyboard',
    false
  );

  // Set up music player for success song
  const [audioContext, initAudioContext] = useContext(CrosshareAudioContext);
  const playSuccess = useRef<(() => void) | null>(null);
  useEffect(() => {
    if (!audioContext) {
      return initAudioContext();
    }
    if (!playSuccess.current && !muted && audioContext) {
      fetch('/success.mp3')
        .then((response) => response.arrayBuffer())
        .then((buffer) => {
          audioContext.decodeAudioData(buffer, (audioBuffer) => {
            playSuccess.current = () => {
              const source = audioContext.createBufferSource();
              source.buffer = audioBuffer;
              source.connect(audioContext.destination);
              source.start();
            };
          });
        });
    }
  }, [muted, audioContext, initAudioContext]);

  const writePlayToDBIfNeeded = useCallback(
    async (user?: firebase.User) => {
      console.log('doing write play');
      if (!state.loadedPlayState) {
        return;
      }
      if (puzzle.contestAnswers?.length) {
        // For a meta we need to have run both to skip
        if (state.ranSuccessEffects && state.ranMetaSubmitEffects) {
          return;
        }
      } else {
        // For a reg puzzle skip if success effects have run
        if (state.ranSuccessEffects) {
          return;
        }
      }
      const u = user || props.user;
      if (!u) {
        return;
      }
      if (!isDirty(u, puzzle.id)) {
        return;
      }
      writePlayToDB(u, puzzle.id)
        .then(() => {
          console.log('Finished writing play state to db');
        })
        .catch((reason) => {
          console.error('Failed to write play: ', reason);
        });
    },
    [
      puzzle.id,
      puzzle.contestAnswers,
      props.user,
      state.ranMetaSubmitEffects,
      state.ranSuccessEffects,
      state.loadedPlayState,
    ]
  );

  const cachePlayForUser = useCallback(
    (user: firebase.User | undefined) => {
      if (!state.loadedPlayState) {
        return;
      }
      const updatedAt = TimestampClass.now();
      const playTime =
        state.currentTimeWindowStart === 0
          ? state.bankedSeconds
          : state.bankedSeconds +
            (new Date().getTime() - state.currentTimeWindowStart) / 1000;

      const playForUser: PlayWithoutUserT = {
        c: puzzle.id,
        n: puzzle.title,
        ua: updatedAt,
        g: Array.from(state.grid.cells),
        ct: Array.from(state.cellsUpdatedAt),
        uc: Array.from(state.cellsIterationCount),
        vc: Array.from(state.verifiedCells),
        wc: Array.from(state.wrongCells),
        we: Array.from(state.cellsEverMarkedWrong),
        rc: Array.from(state.revealedCells),
        t: playTime,
        ch: state.didCheat,
        do: state.downsOnly,
        f: state.success,
        ...(state.contestRevealed && {
          ct_rv: state.contestRevealed,
          ct_t:
            state.contestSubmitTime !== undefined
              ? TimestampClass.fromMillis(state.contestSubmitTime)
              : undefined,
          ct_n: state.contestDisplayName,
        }),
        ...(state.contestSubmission && {
          ct_sub: state.contestSubmission,
          ct_pr_subs: state.contestPriorSubmissions || [],
          ct_t:
            state.contestSubmitTime !== undefined
              ? TimestampClass.fromMillis(state.contestSubmitTime)
              : undefined,
          ct_n: state.contestDisplayName,
          ...(state.contestEmail && {
            ct_em: state.contestEmail,
          }),
        }),
      };
      cachePlay(user, puzzle.id, playForUser);
    },
    [
      state.downsOnly,
      state.loadedPlayState,
      puzzle.id,
      state.cellsEverMarkedWrong,
      state.cellsIterationCount,
      state.cellsUpdatedAt,
      state.didCheat,
      state.grid.cells,
      state.revealedCells,
      state.success,
      state.verifiedCells,
      state.wrongCells,
      puzzle.title,
      state.bankedSeconds,
      state.currentTimeWindowStart,
      state.contestSubmission,
      state.contestSubmitTime,
      state.contestEmail,
      state.contestDisplayName,
      state.contestRevealed,
      state.contestPriorSubmissions,
    ]
  );

  useEffect(() => {
    cachePlayForUser(props.user);
  }, [props.user, cachePlayForUser]);

  const router = useRouter();
  useEffect(() => {
    const listener = () => {
      writePlayToDBIfNeeded();
    };
    window.addEventListener('beforeunload', listener);
    router.events.on('routeChangeStart', listener);

    return () => {
      window.removeEventListener('beforeunload', listener);
      router.events.off('routeChangeStart', listener);
    };
  }, [writePlayToDBIfNeeded, router]);

  const { addToast } = useSnackbar();

  useEffect(() => {
    if (
      (state.contestSubmission || state.contestRevealed) &&
      !state.ranMetaSubmitEffects
    ) {
      const action: RanMetaSubmitEffectsAction = { type: 'RANMETASUBMIT' };
      dispatch(action);
      if (props.user) {
        cachePlayForUser(props.user);
        writePlayToDBIfNeeded(props.user);
      } else {
        signInAnonymously().then((u) => {
          cachePlayForUser(u);
          writePlayToDBIfNeeded(u);
        });
      }
    }
  }, [
    cachePlayForUser,
    state.contestSubmission,
    state.contestRevealed,
    state.ranMetaSubmitEffects,
    props.user,
    writePlayToDBIfNeeded,
  ]);

  useEffect(() => {
    if (state.success && !state.ranSuccessEffects) {
      const action: RanSuccessEffectsAction = { type: 'RANSUCCESS' };
      dispatch(action);

      if (props.user) {
        cachePlayForUser(props.user);
        writePlayToDBIfNeeded(props.user);
      } else {
        signInAnonymously().then((u) => {
          cachePlayForUser(u);
          writePlayToDBIfNeeded(u);
        });
      }

      let delay = 0;
      if (state.bankedSeconds <= 60) {
        addToast('? Solved in under a minute!');
        delay += 500;
      }
      if (!state.didCheat && state.downsOnly) {
        addToast('? Solved downs-only!', delay);
      } else if (!state.didCheat) {
        addToast('? Solved without check/reveal!', delay);
      }
      if (!muted && playSuccess.current) {
        playSuccess.current();
      }
    }
  }, [
    addToast,
    cachePlayForUser,
    muted,
    props.user,
    state.bankedSeconds,
    state.didCheat,
    state.downsOnly,
    state.ranSuccessEffects,
    state.success,
    writePlayToDBIfNeeded,
  ]);

  const physicalKeyboardHandler = useCallback(
    (e: KeyboardEvent) => {
      // Disable keyboard when paused / loading play
      if (!(state.success && state.dismissedSuccess)) {
        if (loadingPlayState || !state.currentTimeWindowStart) {
          return;
        }
      }

      const mkey = fromKeyboardEvent(e);
      if (isSome(mkey)) {
        const kpa: KeypressAction = { type: 'KEYPRESS', key: mkey.value };
        dispatch(kpa);
        e.preventDefault();
      }
    },
    [
      dispatch,
      loadingPlayState,
      state.currentTimeWindowStart,
      state.success,
      state.dismissedSuccess,
    ]
  );
  useEventListener('keydown', physicalKeyboardHandler);

  const pasteHandler = useCallback(
    (e: ClipboardEvent) => {
      const tagName = (e.target as HTMLElement)?.tagName?.toLowerCase();
      if (tagName === 'textarea' || tagName === 'input') {
        return;
      }

      const pa: PasteAction = {
        type: 'PASTE',
        content: e.clipboardData?.getData('Text') || '',
      };
      dispatch(pa);
      e.preventDefault();
    },
    [dispatch]
  );
  useEventListener('paste', pasteHandler);

  let [entry, cross] = entryAndCrossAtPosition(state.grid, state.active);
  if (entry === null && cross !== null) {
    dispatch({ type: 'CHANGEDIRECTION' });
    [entry, cross] = [cross, entry];
  }

  const keyboardHandler = useCallback(
    (key: string) => {
      const mkey = fromKeyString(key);
      if (isSome(mkey)) {
        const kpa: KeypressAction = { type: 'KEYPRESS', key: mkey.value };
        dispatch(kpa);
      }
    },
    [dispatch]
  );

  const { acrossEntries, downEntries } = useMemo(() => {
    return {
      acrossEntries: state.grid.entries.filter(
        (e) => e.direction === Direction.Across
      ),
      downEntries: state.grid.entries.filter(
        (e) => e.direction === Direction.Down
      ),
    };
  }, [state.grid.entries]);

  const isEmbed = useContext(EmbedContext);

  /* `clueMap` is a map from ENTRYWORD => '5D: This is the clue' - we use this
   *    for comment clue tooltips. */
  const clueMap = useMemo(() => {
    return getEntryToClueMap(state.grid, state.answers);
  }, [state.grid, state.answers]);

  /* `refs` is a set of referenced entry indexes for each entry in the grid - we use this
   * for grid highlights when an entry is selected.
   *
   * `refPositions` is an array for each entry of [reffedEntry, clueTextStart, clueTextEnd] tuples
   */
  const [refs, refPositions] = useMemo(() => {
    return getRefs(state.grid);
  }, [state.grid]);

  const scrollToCross = useMatchMedia(SMALL_AND_UP_RULES);

  const overlayBaseProps: PuzzleOverlayBaseProps = {
    publishTime: puzzle.isPrivateUntil || puzzle.publishTime,
    coverImage: props.coverImage,
    profilePicture: props.profilePicture,
    downsOnly: state.downsOnly,
    clueMap: clueMap,
    user: props.user,
    nextPuzzle: props.nextPuzzle,
    puzzle: puzzle,
    isMuted: muted,
    solveTime: state.displaySeconds,
    didCheat: state.didCheat,
    dispatch: dispatch,
  };

  let puzzleView: ReactNode;

  const entryIdx = entryIndexAtPosition(state.grid, state.active);
  let refed: Set<number> = new Set();
  if (entryIdx !== null) {
    refed = refs[entryIdx] || new Set();
  }

  const shouldConceal =
    state.currentTimeWindowStart === 0 &&
    !(state.success && state.dismissedSuccess);
  if (state.clueView) {
    puzzleView = (
      <TwoCol
        left={
          <ClueList
            isEnteringRebus={state.isEnteringRebus}
            rebusValue={state.rebusValue}
            wasEntryClick={state.wasEntryClick}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={true}
            conceal={shouldConceal}
            header={t`Across`}
            entries={acrossEntries}
            current={entry?.index}
            cross={cross?.index}
            scrollToCross={scrollToCross}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
        right={
          <ClueList
            isEnteringRebus={state.isEnteringRebus}
            rebusValue={state.rebusValue}
            wasEntryClick={state.wasEntryClick}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={true}
            conceal={shouldConceal}
            header={t`Down`}
            entries={downEntries}
            current={entry?.index}
            cross={cross?.index}
            scrollToCross={scrollToCross}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
      />
    );
  } else {
    puzzleView = (
      <SquareAndCols
        leftIsActive={state.active.dir === Direction.Across}
        waitToResize={state.waitToResize}
        dispatch={dispatch}
        aspectRatio={state.grid.width / state.grid.height}
        square={(width: number, _height: number) => {
          return (
            <GridView
              isEnteringRebus={state.isEnteringRebus}
              rebusValue={state.rebusValue}
              squareWidth={width}
              grid={state.grid}
              active={state.active}
              entryRefs={refs}
              dispatch={dispatch}
              revealedCells={state.revealedCells}
              verifiedCells={state.verifiedCells}
              wrongCells={state.wrongCells}
              showAlternates={state.success ? state.alternateSolutions : null}
              answers={state.answers}
            />
          );
        }}
        header={
          <div
            css={{
              height: SQUARE_HEADER_HEIGHT,
              fontSize: 18,
              lineHeight: '24px',
              backgroundColor: 'var(--lighter)',
              overflowY: 'scroll',
              scrollbarWidth: 'none',
              display: 'flex',
            }}
          >
            {entry ? (
              <div css={{ margin: 'auto 1em' }}>
                <span
                  css={{
                    fontWeight: 'bold',
                    paddingRight: '0.5em',
                  }}
                >
                  {entry.labelNumber}
                  {entry.direction === Direction.Across ? 'A' : 'D'}
                </span>
                <span
                  css={{
                    color: shouldConceal ? 'transparent' : 'var(--text)',
                    textShadow: shouldConceal
                      ? '0 0 1em var(--conceal-text)'
                      : '',
                  }}
                >
                  <ClueText
                    refPositions={refPositions}
                    entryIndex={entry.index}
                    allEntries={state.grid.entries}
                    grid={state.grid}
                    downsOnly={state.downsOnly && !state.success}
                  />
                </span>
              </div>
            ) : (
              ''
            )}
          </div>
        }
        left={
          <ClueList
            wasEntryClick={state.wasEntryClick}
            scrollToCross={scrollToCross}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={false}
            conceal={shouldConceal}
            header={t`Across`}
            entries={acrossEntries}
            current={entry?.index}
            cross={cross?.index}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
        right={
          <ClueList
            wasEntryClick={state.wasEntryClick}
            scrollToCross={scrollToCross}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={false}
            conceal={shouldConceal}
            header={t`Down`}
            entries={downEntries}
            current={entry?.index}
            cross={cross?.index}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
      />
    );
  }

  const checkRevealMenus = useMemo(
    () => (
      <>
        <TopBarDropDown icon={<FaEye />} text={t`Reveal`}>
          {() => (
            <>
              <TopBarDropDownLink
                icon={<RevealSquare />}
                text={t`Reveal Square`}
                onClick={() => {
                  const ca: CheatAction = {
                    type: 'CHEAT',
                    unit: CheatUnit.Square,
                    isReveal: true,
                  };
                  dispatch(ca);
                }}
              />
              <TopBarDropDownLink
                icon={<RevealEntry />}
                text={t`Reveal Word`}
                onClick={() => {
                  const ca: CheatAction = {
                    type: 'CHEAT',
                    unit: CheatUnit.Entry,
                    isReveal: true,
                  };
                  dispatch(ca);
                }}
              />
              <TopBarDropDownLink
                icon={<RevealPuzzle />}
                text={t`Reveal Puzzle`}
                onClick={() => {
                  const ca: CheatAction = {
                    type: 'CHEAT',
                    unit: CheatUnit.Puzzle,
                    isReveal: true,
                  };
                  dispatch(ca);
                }}
              />
            </>
          )}
        </TopBarDropDown>
        {!state.autocheck ? (
          <TopBarDropDown icon={<FaCheck />} text={t`Check`}>
            {() => (
              <>
                <TopBarDropDownLink
                  icon={<FaCheckSquare />}
                  text={t`Autocheck`}
                  onClick={() => {
                    const action: ToggleAutocheckAction = {
                      type: 'TOGGLEAUTOCHECK',
                    };
                    dispatch(action);
                  }}
                />
                <TopBarDropDownLink
                  icon={<CheckSquare />}
                  text={t`Check Square`}
                  onClick={() => {
                    const ca: CheatAction = {
                      type: 'CHEAT',
                      unit: CheatUnit.Square,
                    };
                    dispatch(ca);
                  }}
                />
                <TopBarDropDownLink
                  icon={<CheckEntry />}
                  text={t`Check Word`}
                  onClick={() => {
                    const ca: CheatAction = {
                      type: 'CHEAT',
                      unit: CheatUnit.Entry,
                    };
                    dispatch(ca);
                  }}
                />
                <TopBarDropDownLink
                  icon={<CheckPuzzle />}
                  text={t`Check Puzzle`}
                  onClick={() => {
                    const ca: CheatAction = {
                      type: 'CHEAT',
                      unit: CheatUnit.Puzzle,
                    };
                    dispatch(ca);
                  }}
                />
              </>
            )}
          </TopBarDropDown>
        ) : (
          <TopBarLink
            icon={<FaCheckSquare />}
            text={t`Autochecking`}
            onClick={() => {
              const action: ToggleAutocheckAction = { type: 'TOGGLEAUTOCHECK' };
              dispatch(action);
            }}
          />
        )}
      </>
    ),
    [state.autocheck]
  );

  const moreMenu = useMemo(
    () => (
      <>
        <TopBarDropDown icon={<FaEllipsisH />} text={t`More`}>
          {() => (
            <>
              {!state.success ? (
                <TopBarDropDownLink
                  icon={<Rebus />}
                  text={t`Enter Rebus`}
                  shortcutHint={<EscapeKey />}
                  onClick={() => {
                    const kpa: KeypressAction = {
                      type: 'KEYPRESS',
                      key: { k: KeyK.Escape },
                    };
                    dispatch(kpa);
                  }}
                />
              ) : (
                ''
              )}
              {muted ? (
                <TopBarDropDownLink
                  icon={<FaVolumeUp />}
                  text={t`Unmute`}
                  onClick={() => setMuted(false)}
                />
              ) : (
                <TopBarDropDownLink
                  icon={<FaVolumeMute />}
                  text={t`Mute`}
                  onClick={() => setMuted(true)}
                />
              )}
              <TopBarDropDownLink
                icon={<FaKeyboard />}
                text={t`Toggle Keyboard`}
                onClick={() => setToggleKeyboard(!toggleKeyboard)}
              />
              {props.isAdmin ? (
                <>
                  <TopBarDropDownLink
                    icon={<FaGlasses />}
                    text="Moderate"
                    onClick={() => dispatch({ type: 'TOGGLEMODERATING' })}
                  />
                  <TopBarDropDownLinkA
                    href="/admin"
                    icon={<FaUserLock />}
                    text="Admin"
                  />
                </>
              ) : (
                ''
              )}
              {props.isAdmin || props.user?.uid === puzzle.authorId ? (
                <>
                  <TopBarDropDownLinkA
                    href={`/stats/${puzzle.id}`}
                    icon={<IoMdStats />}
                    text={t`Stats`}
                  />
                  <TopBarDropDownLinkA
                    href={`/edit/${puzzle.id}`}
                    icon={<FaEdit />}
                    text={t`Edit`}
                  />
                  {!isEmbed ? (
                    <TopBarDropDownLink
                      icon={<ImEmbed />}
                      text={t`Embed`}
                      onClick={() => dispatch({ type: 'TOGGLEEMBEDOVERLAY' })}
                    />
                  ) : (
                    ''
                  )}
                </>
              ) : (
                ''
              )}
              <TopBarDropDownLinkSimpleA
                href={'/api/pdf/' + puzzle.id}
                icon={<FaPrint />}
                text={t`Print Puzzle`}
              />
              {puzzle.hBars.length || puzzle.vBars.length ? (
                ''
              ) : (
                <TopBarDropDownLinkSimpleA
                  href={'/api/puz/' + puzzle.id}
                  icon={<FaRegFile />}
                  text={t`Download .puz File`}
                />
              )}
              <TopBarDropDownLinkA
                href="/account"
                icon={<FaUser />}
                text={t`Account / Settings`}
              />
              <TopBarDropDownLinkA
                href="/construct"
                icon={<FaHammer />}
                text={t`Construct a Puzzle`}
              />
            </>
          )}
        </TopBarDropDown>
      </>
    ),
    [
      muted,
      props.isAdmin,
      props.user?.uid,
      puzzle,
      setMuted,
      state.success,
      toggleKeyboard,
      setToggleKeyboard,
      isEmbed,
    ]
  );

  const description = puzzle.blogPost
    ? puzzle.blogPost.slice(0, 160) + '...'
    : puzzle.clues.map(getClueText).slice(0, 10).join('; ');

  const locale = router.locale || 'en';

  return (
    <>
      <Global
        styles={FULLSCREEN_CSS}
      />
      <Head>
        <title>{puzzle.title} | Crosshare crossword puzzle</title>
        <I18nTags
          locale={locale}
          canonicalPath={`/crosswords/${puzzle.id}/${slugify(puzzle.title)}`}
        />
        <meta key="og:title" property="og:title" content={puzzle.title} />
        <meta
          key="og:description"
          property="og:description"
          content={description}
        />
        <meta
          key="og:image"
          property="og:image"
          content={'https://crosshare.org/api/ogimage/' + puzzle.id}
        />
        <meta key="og:image:width" property="og:image:width" content="1200" />
        <meta key="og:image:height" property="og:image:height" content="630" />
        <meta
          key="og:image:alt"
          property="og:image:alt"
          content="An image of the puzzle grid"
        />
        <meta key="description" name="description" content={description} />
      </Head>
      <div
        css={{
          display: 'flex',
          flexDirection: 'column',
          height: '100%',
        }}
      >
        <div css={{ flex: 'none' }}>
          <TopBar title={puzzle.title}>
            {!loadingPlayState ? (
              !state.success ? (
                <>
                  <TopBarLink
                    icon={<FaPause />}
                    hoverText={t`Pause Game`}
                    text={timeString(state.displaySeconds, true)}
                    onClick={() => {
                      dispatch({ type: 'PAUSEACTION' });
                      writePlayToDBIfNeeded();
                    }}
                    keepText={true}
                  />
                  <TopBarLink
                    icon={state.clueView ? <SpinnerFinished /> : <FaListOl />}
                    text={state.clueView ? t`Grid` : t`Clues`}
                    onClick={() => {
                      const a: ToggleClueViewAction = {
                        type: 'TOGGLECLUEVIEW',
                      };
                      dispatch(a);
                    }}
                  />
                  {checkRevealMenus}
                  {moreMenu}
                </>
              ) : (
                <>
                  <TopBarLink
                    icon={<FaComment />}
                    text={
                      puzzle.contestAnswers?.length
                        ? !isMetaSolution(
                            state.contestSubmission,
                            puzzle.contestAnswers
                          ) && !state.contestRevealed
                          ? t`Contest Prompt / Submission`
                          : t`Comments / Leaderboard`
                        : t`Show Comments`
                    }
                    onClick={() => dispatch({ type: 'UNDISMISSSUCCESS' })}
                  />
                  {moreMenu}
                </>
              )
            ) : (
              moreMenu
            )}
          </TopBar>
        </div>
        {state.filled && !state.success && !state.dismissedKeepTrying ? (
          <KeepTryingOverlay dispatch={dispatch} />
        ) : (
          ''
        )}
        {state.success && !state.dismissedSuccess ? (
          <PuzzleOverlay
            {...overlayBaseProps}
            overlayType={OverlayType.Success}
            contestSubmission={state.contestSubmission}
            contestHasPrize={puzzle.contestHasPrize}
            contestRevealed={state.contestRevealed}
            contestRevealDelay={puzzle.contestRevealDelay}
          />
        ) : (
          ''
        )}
        {state.moderating ? (
          <ModeratingOverlay puzzle={puzzle} dispatch={dispatch} />
        ) : (
          ''
        )}
        {state.showingEmbedOverlay && props.user ? (
          <EmbedOverlay user={props.user} puzzle={puzzle} dispatch={dispatch} />
        ) : (
          ''
        )}
        {state.currentTimeWindowStart === 0 &&
        !state.success &&
        !(state.filled && !state.dismissedKeepTrying) ? (
          state.bankedSeconds === 0 ? (
            <PuzzleOverlay
              {...overlayBaseProps}
              overlayType={OverlayType.BeginPause}
              dismissMessage={t`Begin Puzzle`}
              message={t`Ready to get started?`}
              loadingPlayState={loadingPlayState || !state.loadedPlayState}
            />
          ) : (
            <PuzzleOverlay
              {...overlayBaseProps}
              overlayType={OverlayType.BeginPause}
              dismissMessage={t`Resume`}
              message={t`Your puzzle is paused`}
              loadingPlayState={loadingPlayState || !state.loadedPlayState}
            />
          )
        ) : (
          ''
        )}
        <div
          css={{
            flex: '1 1 auto',
            overflow: 'scroll',
            scrollbarWidth: 'none',
            position: 'relative',
          }}
        >
          {puzzleView}
        </div>
        <div css={{ flex: 'none', width: '100%' }}>
          <Keyboard
            toggleKeyboard={toggleKeyboard}
            keyboardHandler={keyboardHandler}
            muted={muted}
            showExtraKeyLayout={state.showExtraKeyLayout}
            includeBlockKey={false}
          />
        </div>
      </div>
    </>
  );
}