react-icons/fa#FaRegNewspaper TypeScript Examples

The following examples show how to use react-icons/fa#FaRegNewspaper. 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: MainLayout.tsx    From personal-archive with MIT License 5 votes vote down vote up
MainLayout: FC<Props> = ({ side, title, children}) => {
  const [showSearchDrawer, setShowSearchDrawer] = useState(false)
  const [showNav, setShowNav] = useState(false)  // only works in mobile
  const history = useHistory()

  useEffect(() => {
    // 페이지 이동시 초기화
    history.listen(() => setShowNav(false))
  }, [history])

  useSubscribe(ShowSearchDrawer, (isOpened: boolean) => setShowSearchDrawer(isOpened))

  const customTokens = getTokens({})

  return (
    <ThemeProvider theme={{orbit: customTokens}}>
      <Parent>
        <Helmet>
          <title>{getTitle(title)}</title>
        </Helmet>
        <Header>
          <Logo>PA</Logo>
          <Menu onClick={() => history.push(`/tags/all`)}>
            <FaRegNewspaper size="21px" />
          </Menu>
          <Menu onClick={() => history.push(`/notes`)}>
            <FaRegStickyNote size="21px" />
          </Menu>
          <Menu onClick={() => setShowSearchDrawer(true)}>
            <FaSearch size="21px" />
          </Menu>
          <Menu onClick={() => history.push(`/settings`)}>
            <FaRegSun size="21px" />
          </Menu>
          <DarkModeSwitch />
        </Header>
        <Middle>
          <Mobile>
            <When condition={side != null}>
              <Menu onClick={() => setShowNav(true)}>
                <MenuHamburger/>
              </Menu>
            </When>
          </Mobile>
        </Middle>
        <Body>
          <Desktop>
            <When condition={side != null}>
              <Nav>
                {side}
              </Nav>
            </When>
          </Desktop>
          <Main>
            {children}
            <SearchDrawer
              show={showSearchDrawer}
              onClose={() => setShowSearchDrawer(false)}
            />
          </Main>
        </Body>
        <Drawer
          shown={showNav}
          onClose={() => setShowNav(false)}
        >
          <Stack>
            {side}
          </Stack>
        </Drawer>
      </Parent>
    </ThemeProvider>
  )
}
Example #2
Source File: Builder.tsx    From crosshare with GNU Affero General Public License v3.0 4 votes vote down vote up
GridMode = ({
  getMostConstrainedEntry,
  reRunAutofill,
  state,
  dispatch,
  setClueMode,
  ...props
}: GridModeProps) => {
  const [muted, setMuted] = usePersistedBoolean('muted', false);
  const [toggleKeyboard, setToggleKeyboard] = usePersistedBoolean(
    'keyboard',
    false
  );
  const { showSnackbar } = useSnackbar();

  const gridRef = useRef<HTMLDivElement | null>(null);

  const focusGrid = useCallback(() => {
    if (gridRef.current) {
      gridRef.current.focus();
    }
  }, []);

  const physicalKeyboardHandler = useCallback(
    (e: KeyboardEvent) => {
      const mkey = fromKeyboardEvent(e);
      if (isSome(mkey)) {
        e.preventDefault();
        if (mkey.value.k === KeyK.Enter && !state.isEnteringRebus) {
          reRunAutofill();
          return;
        }
        if (mkey.value.k === KeyK.Exclamation) {
          const entry = getMostConstrainedEntry();
          if (entry !== null) {
            const ca: ClickedEntryAction = {
              type: 'CLICKEDENTRY',
              entryIndex: entry,
            };
            dispatch(ca);
          }
          return;
        }
        if (mkey.value.k === KeyK.Octothorp) {
          const a: ToggleHiddenAction = { type: 'TOGGLEHIDDEN' };
          dispatch(a);
        }

        const kpa: KeypressAction = { type: 'KEYPRESS', key: mkey.value };
        dispatch(kpa);
      }
    },
    [dispatch, reRunAutofill, state.isEnteringRebus, getMostConstrainedEntry]
  );
  useEventListener(
    'keydown',
    physicalKeyboardHandler,
    gridRef.current || undefined
  );

  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);

  const fillLists = useMemo(() => {
    let left = <></>;
    let right = <></>;
    const [entry, cross] = entryAndCrossAtPosition(state.grid, state.active);
    let crossMatches = cross && potentialFill(cross, state.grid);
    let entryMatches = entry && potentialFill(entry, state.grid);

    if (
      crossMatches !== null &&
      entryMatches !== null &&
      entry !== null &&
      cross !== null
    ) {
      /* If we have both entry + cross we now filter for only matches that'd work for both. */
      const entryActiveIndex = activeIndex(state.grid, state.active, entry);
      const crossActiveIndex = activeIndex(state.grid, state.active, cross);
      const entryValidLetters = lettersAtIndex(entryMatches, entryActiveIndex);
      const crossValidLetters = lettersAtIndex(crossMatches, crossActiveIndex);
      const validLetters = (
        entryValidLetters.match(
          new RegExp('[' + crossValidLetters + ']', 'g')
        ) || []
      ).join('');
      entryMatches = entryMatches.filter(([word]) => {
        const l = word[entryActiveIndex];
        return l && validLetters.indexOf(l) !== -1;
      });
      crossMatches = crossMatches.filter(([word]) => {
        const l = word[crossActiveIndex];
        return l && validLetters.indexOf(l) !== -1;
      });
    }

    if (cross && crossMatches !== null) {
      if (cross.direction === Direction.Across) {
        left = (
          <PotentialFillList
            selected={false}
            gridRef={gridRef}
            header="Across"
            values={crossMatches}
            entryIndex={cross.index}
            dispatch={dispatch}
          />
        );
      } else {
        right = (
          <PotentialFillList
            selected={false}
            gridRef={gridRef}
            header="Down"
            values={crossMatches}
            entryIndex={cross.index}
            dispatch={dispatch}
          />
        );
      }
    }
    if (entry && entryMatches !== null) {
      if (entry.direction === Direction.Across) {
        left = (
          <PotentialFillList
            selected={true}
            gridRef={gridRef}
            header="Across"
            values={entryMatches}
            entryIndex={entry.index}
            dispatch={dispatch}
          />
        );
      } else {
        right = (
          <PotentialFillList
            selected={true}
            gridRef={gridRef}
            header="Down"
            values={entryMatches}
            entryIndex={entry.index}
            dispatch={dispatch}
          />
        );
      }
    }
    return { left, right };
  }, [state.grid, state.active, dispatch]);

  const { autofillEnabled, setAutofillEnabled } = props;
  const toggleAutofillEnabled = useCallback(() => {
    if (autofillEnabled) {
      showSnackbar('Autofill Disabled');
    }
    setAutofillEnabled(!autofillEnabled);
  }, [autofillEnabled, setAutofillEnabled, showSnackbar]);

  const stats = useMemo(() => {
    let totalLength = 0;
    const lengthHistogram: Array<number> = new Array(
      Math.max(state.grid.width, state.grid.height) - 1
    ).fill(0);
    const lengthHistogramNames = lengthHistogram.map((_, i) =>
      (i + 2).toString()
    );

    state.grid.entries.forEach((e) => {
      totalLength += e.cells.length;
      lengthHistogram[e.cells.length - 2] += 1;
    });
    const numEntries = state.grid.entries.length;
    const averageLength = totalLength / numEntries;
    const lettersHistogram: Array<number> = new Array(26).fill(0);
    const lettersHistogramNames = lettersHistogram.map((_, i) =>
      String.fromCharCode(i + 65)
    );
    let numBlocks = 0;
    const numTotal = state.grid.width * state.grid.height;
    state.grid.cells.forEach((s) => {
      if (s === '.') {
        numBlocks += 1;
      } else {
        const index = lettersHistogramNames.indexOf(s);
        if (index !== -1) {
          lettersHistogram[index] += 1;
        }
      }
    });
    return {
      numBlocks,
      numTotal,
      lengthHistogram,
      lengthHistogramNames,
      numEntries,
      averageLength,
      lettersHistogram,
      lettersHistogramNames,
    };
  }, [
    state.grid.entries,
    state.grid.height,
    state.grid.width,
    state.grid.cells,
  ]);

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

  const topBarChildren = useMemo(() => {
    let autofillIcon = <SpinnerDisabled />;
    let autofillReverseIcon = <SpinnerWorking />;
    let autofillReverseText = 'Enable Autofill';
    let autofillText = 'Autofill disabled';
    if (props.autofillEnabled) {
      autofillReverseIcon = <SpinnerDisabled />;
      autofillReverseText = 'Disable Autofill';
      if (props.autofillInProgress) {
        autofillIcon = <SpinnerWorking />;
        autofillText = 'Autofill in progress';
      } else if (props.autofilledGrid.length) {
        autofillIcon = <SpinnerFinished />;
        autofillText = 'Autofill complete';
      } else {
        autofillIcon = <SpinnerFailed />;
        autofillText = "Couldn't autofill this grid";
      }
    }
    return (
      <>
        <TopBarDropDown
          onClose={focusGrid}
          icon={autofillIcon}
          text="Autofill"
          hoverText={autofillText}
        >
          {() => (
            <>
              <TopBarDropDownLink
                icon={autofillReverseIcon}
                text={autofillReverseText}
                onClick={toggleAutofillEnabled}
              />
              <TopBarDropDownLink
                icon={<FaSignInAlt />}
                text="Jump to Most Constrained"
                shortcutHint={<ExclamationKey />}
                onClick={() => {
                  const entry = getMostConstrainedEntry();
                  if (entry !== null) {
                    const ca: ClickedEntryAction = {
                      type: 'CLICKEDENTRY',
                      entryIndex: entry,
                    };
                    dispatch(ca);
                  }
                }}
              />
              <TopBarDropDownLink
                icon={<MdRefresh />}
                text="Rerun Autofiller"
                shortcutHint={<EnterKey />}
                onClick={() => {
                  reRunAutofill();
                }}
              />
            </>
          )}
        </TopBarDropDown>
        <TopBarLink
          icon={<FaListOl />}
          text="Clues"
          onClick={() => setClueMode(true)}
        />
        <TopBarLink
          icon={<FaRegNewspaper />}
          text="Publish"
          onClick={() => {
            const a: PublishAction = {
              type: 'PUBLISH',
              publishTimestamp: TimestampClass.now(),
            };
            dispatch(a);
          }}
        />
        <TopBarDropDown onClose={focusGrid} icon={<FaEllipsisH />} text="More">
          {(closeDropdown) => (
            <>
              <NestedDropDown
                onClose={focusGrid}
                closeParent={closeDropdown}
                icon={<FaRegPlusSquare />}
                text="New Puzzle"
              >
                {() => <NewPuzzleForm dispatch={dispatch} />}
              </NestedDropDown>
              <NestedDropDown
                onClose={focusGrid}
                closeParent={closeDropdown}
                icon={<FaFileImport />}
                text="Import .puz File"
              >
                {() => <ImportPuzForm dispatch={dispatch} />}
              </NestedDropDown>
              <TopBarDropDownLink
                icon={<FaRegFile />}
                text="Export .puz File"
                onClick={() => {
                  const a: SetShowDownloadLink = {
                    type: 'SETSHOWDOWNLOAD',
                    value: true,
                  };
                  dispatch(a);
                }}
              />
              <NestedDropDown
                onClose={focusGrid}
                closeParent={closeDropdown}
                icon={<IoMdStats />}
                text="Stats"
              >
                {() => (
                  <>
                    <h2>Grid</h2>
                    <div>
                      {state.gridIsComplete ? (
                        <FaRegCheckCircle />
                      ) : (
                        <FaRegCircle />
                      )}{' '}
                      All cells should be filled
                    </div>
                    <div>
                      {state.hasNoShortWords ? (
                        <FaRegCheckCircle />
                      ) : (
                        <FaRegCircle />
                      )}{' '}
                      All words should be at least three letters
                    </div>
                    <div>
                      {state.repeats.size > 0 ? (
                        <>
                          <FaRegCircle /> (
                          {Array.from(state.repeats).sort().join(', ')})
                        </>
                      ) : (
                        <FaRegCheckCircle />
                      )}{' '}
                      No words should be repeated
                    </div>
                    <h2 css={{ marginTop: '1.5em' }}>Fill</h2>
                    <div>Number of words: {stats.numEntries}</div>
                    <div>
                      Mean word length: {stats.averageLength.toPrecision(3)}
                    </div>
                    <div>
                      Number of blocks: {stats.numBlocks} (
                      {((100 * stats.numBlocks) / stats.numTotal).toFixed(1)}%)
                    </div>
                    <div
                      css={{
                        marginTop: '1em',
                        textDecoration: 'underline',
                        textAlign: 'center',
                      }}
                    >
                      Word Lengths
                    </div>
                    <Histogram
                      data={stats.lengthHistogram}
                      names={stats.lengthHistogramNames}
                    />
                    <div
                      css={{
                        marginTop: '1em',
                        textDecoration: 'underline',
                        textAlign: 'center',
                      }}
                    >
                      Letter Counts
                    </div>
                    <Histogram
                      data={stats.lettersHistogram}
                      names={stats.lettersHistogramNames}
                    />
                  </>
                )}
              </NestedDropDown>
              <NestedDropDown
                onClose={focusGrid}
                closeParent={closeDropdown}
                icon={<SymmetryIcon type={state.symmetry} />}
                text="Change Symmetry"
              >
                {() => (
                  <>
                    <TopBarDropDownLink
                      icon={<SymmetryRotational />}
                      text="Use Rotational Symmetry"
                      onClick={() => {
                        const a: SymmetryAction = {
                          type: 'CHANGESYMMETRY',
                          symmetry: Symmetry.Rotational,
                        };
                        dispatch(a);
                      }}
                    />
                    <TopBarDropDownLink
                      icon={<SymmetryHorizontal />}
                      text="Use Horizontal Symmetry"
                      onClick={() => {
                        const a: SymmetryAction = {
                          type: 'CHANGESYMMETRY',
                          symmetry: Symmetry.Horizontal,
                        };
                        dispatch(a);
                      }}
                    />
                    <TopBarDropDownLink
                      icon={<SymmetryVertical />}
                      text="Use Vertical Symmetry"
                      onClick={() => {
                        const a: SymmetryAction = {
                          type: 'CHANGESYMMETRY',
                          symmetry: Symmetry.Vertical,
                        };
                        dispatch(a);
                      }}
                    />
                    <TopBarDropDownLink
                      icon={<SymmetryNone />}
                      text="Use No Symmetry"
                      onClick={() => {
                        const a: SymmetryAction = {
                          type: 'CHANGESYMMETRY',
                          symmetry: Symmetry.None,
                        };
                        dispatch(a);
                      }}
                    />
                    {state.grid.width === state.grid.height ? (
                      <>
                        <TopBarDropDownLink
                          icon={<SymmetryIcon type={Symmetry.DiagonalNESW} />}
                          text="Use NE/SW Diagonal Symmetry"
                          onClick={() => {
                            const a: SymmetryAction = {
                              type: 'CHANGESYMMETRY',
                              symmetry: Symmetry.DiagonalNESW,
                            };
                            dispatch(a);
                          }}
                        />
                        <TopBarDropDownLink
                          icon={<SymmetryIcon type={Symmetry.DiagonalNWSE} />}
                          text="Use NW/SE Diagonal Symmetry"
                          onClick={() => {
                            const a: SymmetryAction = {
                              type: 'CHANGESYMMETRY',
                              symmetry: Symmetry.DiagonalNWSE,
                            };
                            dispatch(a);
                          }}
                        />
                      </>
                    ) : (
                      ''
                    )}
                  </>
                )}
              </NestedDropDown>
              <TopBarDropDownLink
                icon={<FaSquare />}
                text="Toggle Block"
                shortcutHint={<PeriodKey />}
                onClick={() => {
                  const a: KeypressAction = {
                    type: 'KEYPRESS',
                    key: { k: KeyK.Dot },
                  };
                  dispatch(a);
                }}
              />
              <TopBarDropDownLink
                icon={<CgSidebarRight />}
                text="Toggle Bar"
                shortcutHint={<CommaKey />}
                onClick={() => {
                  const a: KeypressAction = {
                    type: 'KEYPRESS',
                    key: { k: KeyK.Comma },
                  };
                  dispatch(a);
                }}
              />
              <TopBarDropDownLink
                icon={<FaEyeSlash />}
                text="Toggle Cell Visibility"
                shortcutHint={<KeyIcon text="#" />}
                onClick={() => {
                  const a: ToggleHiddenAction = {
                    type: 'TOGGLEHIDDEN',
                  };
                  dispatch(a);
                }}
              />
              <TopBarDropDownLink
                icon={<Rebus />}
                text="Enter Rebus"
                shortcutHint={<EscapeKey />}
                onClick={() => {
                  const a: KeypressAction = {
                    type: 'KEYPRESS',
                    key: { k: KeyK.Escape },
                  };
                  dispatch(a);
                }}
              />
              <TopBarDropDownLink
                icon={
                  state.grid.highlight === 'circle' ? (
                    <FaRegCircle />
                  ) : (
                    <FaFillDrip />
                  )
                }
                text="Toggle Square Highlight"
                shortcutHint={<BacktickKey />}
                onClick={() => {
                  const a: KeypressAction = {
                    type: 'KEYPRESS',
                    key: { k: KeyK.Backtick },
                  };
                  dispatch(a);
                }}
              />
              <TopBarDropDownLink
                icon={
                  state.grid.highlight === 'circle' ? (
                    <FaFillDrip />
                  ) : (
                    <FaRegCircle />
                  )
                }
                text={
                  state.grid.highlight === 'circle'
                    ? 'Use Shade for Highlights'
                    : 'Use Circle for Highlights'
                }
                onClick={() => {
                  const a: SetHighlightAction = {
                    type: 'SETHIGHLIGHT',
                    highlight:
                      state.grid.highlight === 'circle' ? 'shade' : 'circle',
                  };
                  dispatch(a);
                }}
              />
              {muted ? (
                <TopBarDropDownLink
                  icon={<FaVolumeUp />}
                  text="Unmute"
                  onClick={() => setMuted(false)}
                />
              ) : (
                <TopBarDropDownLink
                  icon={<FaVolumeMute />}
                  text="Mute"
                  onClick={() => setMuted(true)}
                />
              )}
              <TopBarDropDownLink
                icon={<FaKeyboard />}
                text="Toggle Keyboard"
                onClick={() => setToggleKeyboard(!toggleKeyboard)}
              />
              {props.isAdmin ? (
                <>
                  <TopBarDropDownLinkA
                    href="/admin"
                    icon={<FaUserLock />}
                    text="Admin"
                  />
                </>
              ) : (
                ''
              )}
              <TopBarDropDownLinkA
                href="/dashboard"
                icon={<FaHammer />}
                text="Constructor Dashboard"
              />
              <TopBarDropDownLinkA
                href="/account"
                icon={<FaUser />}
                text="Account"
              />
            </>
          )}
        </TopBarDropDown>
      </>
    );
  }, [
    focusGrid,
    getMostConstrainedEntry,
    props.autofillEnabled,
    props.autofillInProgress,
    props.autofilledGrid.length,
    stats,
    props.isAdmin,
    setClueMode,
    setMuted,
    state.grid.highlight,
    state.grid.width,
    state.grid.height,
    state.gridIsComplete,
    state.hasNoShortWords,
    state.repeats,
    state.symmetry,
    toggleAutofillEnabled,
    reRunAutofill,
    dispatch,
    muted,
    toggleKeyboard,
    setToggleKeyboard,
  ]);

  return (
    <>
      <Global styles={FULLSCREEN_CSS} />
      <div
        css={{
          display: 'flex',
          flexDirection: 'column',
          height: '100%',
        }}
      >
        <div css={{ flex: 'none' }}>
          <TopBar>{topBarChildren}</TopBar>
        </div>
        {state.showDownloadLink ? (
          <PuzDownloadOverlay
            state={state}
            cancel={() => {
              const a: SetShowDownloadLink = {
                type: 'SETSHOWDOWNLOAD',
                value: false,
              };
              dispatch(a);
            }}
          />
        ) : (
          ''
        )}
        {state.toPublish ? (
          <PublishOverlay
            id={state.id}
            toPublish={state.toPublish}
            warnings={state.publishWarnings}
            user={props.user}
            cancelPublish={() => dispatch({ type: 'CANCELPUBLISH' })}
          />
        ) : (
          ''
        )}
        {state.publishErrors.length ? (
          <Overlay
            closeCallback={() => dispatch({ type: 'CLEARPUBLISHERRORS' })}
          >
            <>
              <div>
                Please fix the following errors and try publishing again:
              </div>
              <ul>
                {state.publishErrors.map((s, i) => (
                  <li key={i}>{s}</li>
                ))}
              </ul>
              {state.publishWarnings.length ? (
                <>
                  <div>Warnings:</div>
                  <ul>
                    {state.publishWarnings.map((s, i) => (
                      <li key={i}>{s}</li>
                    ))}
                  </ul>
                </>
              ) : (
                ''
              )}
            </>
          </Overlay>
        ) : (
          ''
        )}
        <div
          css={{ flex: '1 1 auto', overflow: 'scroll', position: 'relative' }}
        >
          <SquareAndCols
            leftIsActive={state.active.dir === Direction.Across}
            ref={gridRef}
            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}
                  dispatch={dispatch}
                  allowBlockEditing={true}
                  autofill={props.autofillEnabled ? props.autofilledGrid : []}
                />
              );
            }}
            left={fillLists.left}
            right={fillLists.right}
            dispatch={dispatch}
          />
        </div>
        <div css={{ flex: 'none', width: '100%' }}>
          <Keyboard
            toggleKeyboard={toggleKeyboard}
            keyboardHandler={keyboardHandler}
            muted={muted}
            showExtraKeyLayout={state.showExtraKeyLayout}
            includeBlockKey={true}
          />
        </div>
      </div>
    </>
  );
}
Example #3
Source File: ClueMode.tsx    From crosshare with GNU Affero General Public License v3.0 4 votes vote down vote up
ClueMode = ({ state, ...props }: ClueModeProps) => {
  const [settingCoverPic, setSettingCoverPic] = useState(false);
  const [contestAnswerInProg, setContestAnswerInProg] = useState('');
  const [addingAlternate, setAddingAlternate] = useState(false);
  const [editingTags, setEditingTags] = useState(false);
  const privateUntil = state.isPrivateUntil?.toDate();
  const autoTags = autoTag(state);

  const contestAnswerError =
    state.contestAnswers &&
    isMetaSolution(contestAnswerInProg, state.contestAnswers)
      ? 'Duplicate solution!'
      : '';

  const count: Record<string, number> = {};

  const clueRows = props.completedEntries
    .sort((e1, e2) =>
      e1.direction === e2.direction
        ? e1.labelNumber - e2.labelNumber
        : e1.direction - e2.direction
    )
    .map((e) => {
      const clueIdx = count[e.completedWord || ''] || 0;
      count[e.completedWord || ''] = clueIdx + 1;
      return (
        <ClueRow
          idx={clueIdx}
          key={(e.completedWord || '') + clueIdx}
          dispatch={props.dispatch}
          entry={e}
          clues={props.clues}
        />
      );
    });

  if (addingAlternate) {
    return (
      <>
        <AlternateSolutionEditor
          grid={state.grid.cells}
          save={async (alt) => {
            const act: AddAlternateAction = {
              type: 'ADDALT',
              alternate: alt,
            };
            props.dispatch(act);
          }}
          cancel={() => setAddingAlternate(false)}
          width={state.grid.width}
          height={state.grid.height}
          highlight={state.grid.highlight}
          highlighted={state.grid.highlighted}
          hidden={state.grid.hidden}
          vBars={state.grid.vBars}
          hBars={state.grid.hBars}
        />
      </>
    );
  }
  return (
    <>
      <TopBar>
        <TopBarLink
          icon={<FaRegNewspaper />}
          text="Publish"
          onClick={() => {
            const a: PublishAction = {
              type: 'PUBLISH',
              publishTimestamp: TimestampClass.now(),
            };
            props.dispatch(a);
          }}
        />
        <TopBarLink
          icon={<SpinnerFinished />}
          text="Back to Grid"
          onClick={props.exitClueMode}
        />
      </TopBar>
      {state.toPublish ? (
        <PublishOverlay
          id={state.id}
          toPublish={state.toPublish}
          warnings={state.publishWarnings}
          user={props.user}
          cancelPublish={() => props.dispatch({ type: 'CANCELPUBLISH' })}
        />
      ) : (
        ''
      )}
      {state.publishErrors.length ? (
        <Overlay
          closeCallback={() => props.dispatch({ type: 'CLEARPUBLISHERRORS' })}
        >
          <>
            <div>Please fix the following errors and try publishing again:</div>
            <ul>
              {state.publishErrors.map((s, i) => (
                <li key={i}>{s}</li>
              ))}
            </ul>
            {state.publishWarnings.length ? (
              <>
                <div>Warnings:</div>
                <ul>
                  {state.publishWarnings.map((s, i) => (
                    <li key={i}>{s}</li>
                  ))}
                </ul>
              </>
            ) : (
              ''
            )}
          </>
        </Overlay>
      ) : (
        ''
      )}

      <div css={{ padding: '1em' }}>
        {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
        <label css={{ width: '100%' }}>
          <h2>Title</h2>
          <LengthLimitedInput
            type="text"
            css={{ width: '100%' }}
            placeholder="Give your puzzle a title"
            value={props.title || ''}
            maxLength={MAX_STRING_LENGTH}
            updateValue={(s: string) => {
              const sta: SetTitleAction = {
                type: 'SETTITLE',
                value: s,
              };
              props.dispatch(sta);
            }}
          />
          <div css={{ textAlign: 'right' }}>
            <LengthView
              maxLength={MAX_STRING_LENGTH}
              value={props.title || ''}
              hideUntilWithin={30}
            />
          </div>
        </label>
        <h2 css={{ marginTop: '1em' }}>Metadata</h2>
        <div>
          <ButtonAsLink
            onClick={() => setSettingCoverPic(true)}
            text="Add/edit cover pic"
          />
        </div>
        {settingCoverPic ? (
          <ImageCropper
            targetSize={COVER_PIC}
            isCircle={false}
            storageKey={`/users/${props.authorId}/${props.puzzleId}/cover.jpg`}
            cancelCrop={() => setSettingCoverPic(false)}
          />
        ) : (
          ''
        )}
        {props.notes !== null ? (
          <>
            <h3>Note:</h3>
            <LengthLimitedInput
              type="text"
              css={{ width: '100%' }}
              placeholder="Add a note"
              value={props.notes}
              maxLength={MAX_STRING_LENGTH}
              updateValue={(s: string) => {
                const sta: SetNotesAction = {
                  type: 'SETNOTES',
                  value: s,
                };
                props.dispatch(sta);
              }}
            />
            <div css={{ textAlign: 'right' }}>
              <LengthView
                maxLength={MAX_STRING_LENGTH}
                value={props.notes}
                hideUntilWithin={30}
              />
            </div>
            <p>
              <ButtonAsLink
                text="Remove note"
                onClick={() => {
                  const sna: SetNotesAction = { type: 'SETNOTES', value: null };
                  props.dispatch(sna);
                }}
              />
            </p>
          </>
        ) : (
          <div>
            <ButtonAsLink
              text="Add a note"
              onClick={() => {
                const sna: SetNotesAction = { type: 'SETNOTES', value: '' };
                props.dispatch(sna);
              }}
            />
            <ToolTipText
              css={{ marginLeft: '0.5em' }}
              text={<FaInfoCircle />}
              tooltip="Notes are shown before a puzzle is started and should be used if you need a short explainer of the theme or how the puzzle works"
            />
          </div>
        )}
        {props.blogPost !== null ? (
          <>
            <h3>Blog Post:</h3>
            <p>
              Blog posts are shown before solvers are finished with your puzzle.
              If you include spoilers you can hide them{' '}
              <code>||like this||</code>.
            </p>
            <LengthLimitedTextarea
              css={{ width: '100%', display: 'block' }}
              placeholder="Your post text (markdown format)"
              value={props.blogPost}
              maxLength={MAX_BLOG_LENGTH}
              updateValue={(s: string) => {
                const sta: SetBlogPostAction = {
                  type: 'SETBLOGPOST',
                  value: s,
                };
                props.dispatch(sta);
              }}
            />
            <div css={{ textAlign: 'right' }}>
              <LengthView
                maxLength={MAX_BLOG_LENGTH}
                value={props.blogPost || ''}
                hideUntilWithin={30}
              />
            </div>
            <p>
              <MarkdownPreview markdown={props.blogPost} />
              <ButtonAsLink
                text="Remove blog post"
                onClick={() => {
                  const sna: SetBlogPostAction = {
                    type: 'SETBLOGPOST',
                    value: null,
                  };
                  props.dispatch(sna);
                }}
              />
            </p>
          </>
        ) : (
          <div>
            <ButtonAsLink
              text="Add a blog post"
              onClick={() => {
                const sna: SetBlogPostAction = {
                  type: 'SETBLOGPOST',
                  value: '',
                };
                props.dispatch(sna);
              }}
            />
            <ToolTipText
              css={{ marginLeft: '0.5em' }}
              text={<FaInfoCircle />}
              tooltip="Blog posts are shown before and after the puzzle is solved - describe how you came up with the puzzle, talk about your day, whatever you want!"
            />
          </div>
        )}
        <div css={{ marginTop: '1em' }}>
          <label>
            <input
              css={{ marginRight: '1em' }}
              type="checkbox"
              checked={props.guestConstructor !== null}
              onChange={(e) => {
                const spa: SetGuestConstructorAction = {
                  type: 'SETGC',
                  value: e.target.checked ? '' : null,
                };
                props.dispatch(spa);
              }}
            />{' '}
            This puzzle is by a guest constructor
          </label>
        </div>
        {props.guestConstructor !== null ? (
          <div css={{ marginLeft: '1.5em', marginBottom: '1em' }}>
            <LengthLimitedInput
              type="text"
              css={{ width: '100%' }}
              placeholder="Guest constructor's name"
              value={props.guestConstructor}
              maxLength={MAX_STRING_LENGTH}
              updateValue={(s: string) => {
                const sta: SetGuestConstructorAction = {
                  type: 'SETGC',
                  value: s,
                };
                props.dispatch(sta);
              }}
            />
            <div css={{ textAlign: 'right' }}>
              <LengthView
                maxLength={MAX_STRING_LENGTH}
                value={props.guestConstructor}
                hideUntilWithin={30}
              />
            </div>
          </div>
        ) : (
          ''
        )}
        <div>
          <label>
            <input
              css={{ marginRight: '1em' }}
              type="checkbox"
              checked={state.isContestPuzzle}
              onChange={(e) => {
                const spa: UpdateContestAction = {
                  type: 'CONTEST',
                  enabled: e.target.checked,
                };
                props.dispatch(spa);
              }}
            />{' '}
            This is a meta/contest puzzle{' '}
            <ToolTipText
              css={{ marginLeft: '0.5em' }}
              text={<FaInfoCircle />}
              tooltip="A meta puzzle has an extra puzzle embedded in the grid for after solvers have finished solving. Solvers can submit their solution, find out if they were right or wrong, and view a leaderboard of those who've solved the contest correctly."
            />
          </label>
        </div>
        {state.isContestPuzzle ? (
          <div css={{ marginLeft: '1.5em', marginBottom: '1em' }}>
            <h4>Contest prompt (required):</h4>
            <p>
              Use the notes field above to give solvers a prompt for the
              contest.
            </p>
            <form
              onSubmit={(e) => {
                e.preventDefault();
                const spa: UpdateContestAction = {
                  type: 'CONTEST',
                  addAnswer: contestAnswerInProg,
                };
                props.dispatch(spa);
                setContestAnswerInProg('');
              }}
            >
              <h4>
                Contest solution(s) - must specify at least one valid solution:
              </h4>
              <p>
                Submissions will match regardless of case, whitespace, and
                punctuation.
              </p>
              {state.contestAnswers?.length ? (
                <ul>
                  {state.contestAnswers.map((a) => (
                    <li key={a}>
                      {a} (
                      <ButtonAsLink
                        onClick={() => {
                          const spa: UpdateContestAction = {
                            type: 'CONTEST',
                            removeAnswer: a,
                          };
                          props.dispatch(spa);
                        }}
                        text="remove"
                      />
                      )
                    </li>
                  ))}
                </ul>
              ) : (
                ''
              )}
              <LengthLimitedInput
                type="text"
                placeholder="Solution"
                value={contestAnswerInProg}
                updateValue={setContestAnswerInProg}
                maxLength={MAX_META_SUBMISSION_LENGTH}
              />
              <LengthView
                maxLength={MAX_META_SUBMISSION_LENGTH}
                value={contestAnswerInProg}
                hideUntilWithin={30}
              />
              {contestAnswerError ? (
                <span css={{ color: 'var(--error)', margin: 'auto 0.5em' }}>
                  {contestAnswerError}
                </span>
              ) : (
                ''
              )}
              <Button
                type="submit"
                css={{ marginLeft: '0.5em' }}
                disabled={
                  contestAnswerError !== '' ||
                  contestAnswerInProg.trim().length === 0
                }
                text="Add Solution"
              />
            </form>
            <h4 css={{ marginTop: '1em' }}>Contest explanation</h4>
            <p>
              After publishing, you can use a comment to explain how the
              meta/contest works - comments are only visible to solvers who have
              submitted or revealed the correct solution.
            </p>
            <h4 css={{ marginTop: '1em' }}>Delay before allowing reveal</h4>
            <p>
              Solvers get unlimited submission attempts and can optionally
              reveal the answer if they aren&apos;t able to figure it out. You
              can set a delay so that the reveal function will not be available
              until 1 week after the publish date.
            </p>
            <div>
              <label>
                <input
                  css={{ marginRight: '1em' }}
                  type="checkbox"
                  checked={
                    state.contestRevealDelay
                      ? state.contestRevealDelay > 0
                      : false
                  }
                  onChange={(e) => {
                    const spa: UpdateContestAction = {
                      type: 'CONTEST',
                      revealDelay: e.target.checked
                        ? 1000 * 60 * 60 * 24 * 7
                        : null,
                    };
                    props.dispatch(spa);
                  }}
                />{' '}
                Delay one week from publish date before allowing reveals
              </label>
            </div>

            <h4 css={{ marginTop: '1em' }}>Contest prize</h4>
            <p>
              If the contest has a prize solvers can choose to include their
              email address in their submission to be eligible to win.
            </p>
            <div>
              <label>
                <input
                  css={{ marginRight: '1em' }}
                  type="checkbox"
                  checked={state.contestHasPrize}
                  onChange={(e) => {
                    const spa: UpdateContestAction = {
                      type: 'CONTEST',
                      hasPrize: e.target.checked,
                    };
                    props.dispatch(spa);
                  }}
                />{' '}
                This contest has a prize
              </label>
            </div>
          </div>
        ) : (
          ''
        )}
        <div>
          <label>
            <input
              css={{ marginRight: '1em' }}
              type="checkbox"
              checked={state.isPrivate}
              onChange={(e) => {
                const spa: SetPrivateAction = {
                  type: 'SETPRIVATE',
                  value: e.target.checked,
                };
                props.dispatch(spa);
              }}
            />{' '}
            This puzzle is private
            <ToolTipText
              css={{ marginLeft: '0.5em' }}
              text={<FaInfoCircle />}
              tooltip="Private puzzles are still visible to anybody you share the link with. They do not appear on your constructor blog, they aren't eligible to be featured on the Crosshare homepage, and your followers won't be notified when they are published."
            />
          </label>
        </div>
        <div>
          <label>
            <input
              css={{ marginRight: '1em' }}
              type="checkbox"
              checked={state.isPrivateUntil !== null}
              onChange={(e) => {
                const spa: SetPrivateAction = {
                  type: 'SETPRIVATE',
                  value: e.target.checked && TimestampClass.now(),
                };
                props.dispatch(spa);
              }}
            />{' '}
            This puzzle should be private until a specified date/time
            <ToolTipText
              css={{ marginLeft: '0.5em' }}
              text={<FaInfoCircle />}
              tooltip="The puzzle won't appear on your constructor blog and your followers won't be notified until after the specified time."
            />
          </label>
          {privateUntil ? (
            <p css={{ marginLeft: '1.5em' }}>
              Visible after {lightFormat(privateUntil, "M/d/y' at 'h:mma")}:
              <DateTimePicker
                picked={privateUntil}
                setPicked={(d) => {
                  const spa: SetPrivateAction = {
                    type: 'SETPRIVATE',
                    value: TimestampClass.fromDate(d),
                  };
                  props.dispatch(spa);
                }}
              />
            </p>
          ) : (
            ''
          )}
        </div>
        <h3>Tags</h3>
        <p>
          Tags are shown any time a puzzle is displayed on the site, and help
          solvers quickly find puzzles with a particular attribute or theme.
        </p>
        {editingTags ? (
          <div css={{ marginBottom: '1.5em' }}>
            <TagEditor
              userTags={state.userTags}
              autoTags={autoTags}
              cancel={() => setEditingTags(false)}
              save={async (newTags) => {
                const st: SetTagsAction = {
                  type: 'SETTAGS',
                  tags: newTags,
                };
                props.dispatch(st);
                setEditingTags(false);
              }}
            />
          </div>
        ) : (
          <>
            <h4>Current tags:</h4>
            <TagList tags={state.userTags.concat(autoTags)} />
            <p>
              <Button onClick={() => setEditingTags(true)} text="Edit Tags" />
            </p>
          </>
        )}

        <h2 css={{ marginTop: '1em' }}>Clues</h2>
        {props.completedEntries.length ? (
          <table css={{ width: '100%' }}>
            <tbody>{clueRows}</tbody>
          </table>
        ) : (
          <>
            <p>
              This where you come to set clues for your puzzle, but you
              don&apos;t have any completed fill words yet!
            </p>
            <p>
              Go back to{' '}
              <ButtonAsLink
                text="the grid"
                onClick={(e) => {
                  props.exitClueMode();
                  e.preventDefault();
                }}
              />{' '}
              and fill in one or more words completely. Then come back here and
              make some clues.
            </p>
          </>
        )}
        <h2 css={{ marginTop: '1em' }}>Advanced</h2>
        <div>
          {state.alternates.length ? (
            <>
              <h3>Alternate Solutions</h3>
              <ul>
                {state.alternates.map((a, i) => (
                  <li key={i}>
                    {Object.entries(a).map(([pos, str]) => (
                      <span
                        css={{ '& + &:before': { content: '", "' } }}
                        key={pos}
                      >
                        Cell {pos}: &quot;{str}&quot;
                      </span>
                    ))}{' '}
                    (
                    <ButtonAsLink
                      onClick={() => {
                        const delAlt: DelAlternateAction = {
                          type: 'DELALT',
                          alternate: a,
                        };
                        props.dispatch(delAlt);
                      }}
                      text="remove"
                    />
                    )
                  </li>
                ))}
              </ul>
            </>
          ) : (
            ''
          )}
          <ButtonAsLink
            disabled={!state.gridIsComplete}
            text="Add an alternate solution"
            onClick={() => {
              setAddingAlternate(true);
            }}
          />
          <ToolTipText
            css={{ marginLeft: '0.5em' }}
            text={<FaInfoCircle />}
            tooltip={
              <>
                <p>
                  Alternate solutions can be used if one or more entries in your
                  puzzle have multiple valid solutions (e.g. a
                  Schrödinger&apos;s puzzle or a puzzle with bi-directional
                  rebuses).
                </p>
                <p>
                  Alternates can only be added once the grid is completely
                  filled. Once an alternate has been added the grid cannot be
                  further edited unless all alternates are deleted.
                </p>
              </>
            }
          />
        </div>
      </div>
    </>
  );
}
Example #4
Source File: Preview.tsx    From crosshare with GNU Affero General Public License v3.0 4 votes vote down vote up
Preview = (props: PuzzleInProgressT & AuthProps): JSX.Element => {
  const [state, dispatch] = useReducer(builderReducer, props, initializeState);
  const [dismissedIntro, setDismissedIntro] = useState(false);

  const physicalKeyboardHandler = useCallback(
    (e: KeyboardEvent) => {
      const mkey = fromKeyboardEvent(e);
      if (isSome(mkey)) {
        const kpa: KeypressAction = { type: 'KEYPRESS', key: mkey.value };
        dispatch(kpa);
        e.preventDefault();
      }
    },
    [dispatch]
  );
  useEventListener('keydown', physicalKeyboardHandler);

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

  const { acrossEntries, downEntries } = useMemo(() => {
    return getCluedAcrossAndDown(
      state.clues,
      state.grid.entries,
      state.grid.sortedEntries
    );
  }, [state.grid.entries, state.grid.sortedEntries, state.clues]);
  const scrollToCross = useMatchMedia(SMALL_AND_UP_RULES);

  const [clueMode, setClueMode] = useState(false);
  if (clueMode) {
    return (
      <ClueMode
        state={state}
        puzzleId={state.id}
        authorId={state.authorId}
        dispatch={dispatch}
        blogPost={state.blogPost}
        guestConstructor={state.guestConstructor}
        title={state.title}
        notes={state.notes}
        clues={state.clues}
        completedEntries={state.grid.entries.filter((e) => e.completedWord)}
        exitClueMode={() => setClueMode(false)}
        user={props.user}
      />
    );
  }

  return (
    <>
      <div
        css={{
          display: 'flex',
          flexDirection: 'column',
          height: '100%',
        }}
      >
        <div css={{ flex: 'none' }}>
          <DefaultTopBar>
            <TopBarLink
              icon={<FaRegNewspaper />}
              text="Publish"
              onClick={() => {
                const a: PublishAction = {
                  type: 'PUBLISH',
                  publishTimestamp: TimestampClass.now(),
                };
                dispatch(a);
              }}
            />
            <TopBarLink
              icon={<FaListOl />}
              text="Edit"
              onClick={() => setClueMode(true)}
            />
          </DefaultTopBar>
        </div>
        {state.toPublish ? (
          <PublishOverlay
            id={state.id}
            toPublish={state.toPublish}
            warnings={state.publishWarnings}
            user={props.user}
            cancelPublish={() => dispatch({ type: 'CANCELPUBLISH' })}
          />
        ) : (
          ''
        )}
        {state.publishErrors.length ? (
          <Overlay
            closeCallback={() => dispatch({ type: 'CLEARPUBLISHERRORS' })}
          >
            <>
              <div>
                Please fix the following errors and try publishing again:
              </div>
              <ul>
                {state.publishErrors.map((s, i) => (
                  <li key={i}>{s}</li>
                ))}
              </ul>
            </>
            {state.publishWarnings.length ? (
              <>
                <div>Warnings:</div>
                <ul>
                  {state.publishWarnings.map((s, i) => (
                    <li key={i}>{s}</li>
                  ))}
                </ul>
              </>
            ) : (
              ''
            )}
          </Overlay>
        ) : (
          ''
        )}
        {!dismissedIntro ? (
          <Overlay closeCallback={() => setDismissedIntro(true)}>
            <h2>
              <Emoji symbol="?" /> Successfully Imported{' '}
              {props.title ? <>&lsquo;{props.title}&rsquo;</> : ''}
            </h2>
            {props.notes ? <ConstructorNotes notes={props.notes} /> : ''}
            <p>
              Please look over your grid and clues to make sure everything is
              correct. If something didn&apos;t import correctly, get in touch
              with us via <ContactLinks />.
            </p>
            <p>
              You can edit your title, clues, etc. by clicking
              &lsquo;Edit&rsquo; in the top bar.
            </p>
            <p>
              Once you&apos;ve looked it over, click &lsquo;Publish&rsquo; in
              the top bar to publish your puzzle!
            </p>
          </Overlay>
        ) : (
          ''
        )}
        <div
          css={{ flex: '1 1 auto', overflow: 'scroll', position: 'relative' }}
        >
          <SquareAndCols
            leftIsActive={state.active.dir === Direction.Across}
            dispatch={dispatch}
            aspectRatio={state.grid.width / state.grid.height}
            square={(width: number, _height: number) => {
              return (
                <GridView
                  squareWidth={width}
                  grid={state.grid}
                  active={state.active}
                  dispatch={dispatch}
                  allowBlockEditing={true}
                />
              );
            }}
            left={
              <ClueList
                wasEntryClick={state.wasEntryClick}
                dimCompleted={false}
                active={state.active}
                grid={state.grid}
                showEntries={false}
                conceal={false}
                header="Across"
                entries={acrossEntries}
                current={entry?.index}
                refed={new Set()}
                cross={cross?.index}
                scrollToCross={scrollToCross}
                dispatch={dispatch}
                downsOnly={false}
              />
            }
            right={
              <ClueList
                wasEntryClick={state.wasEntryClick}
                dimCompleted={false}
                active={state.active}
                grid={state.grid}
                showEntries={false}
                conceal={false}
                header="Down"
                entries={downEntries}
                current={entry?.index}
                refed={new Set()}
                cross={cross?.index}
                scrollToCross={scrollToCross}
                dispatch={dispatch}
                downsOnly={false}
              />
            }
          />
        </div>
      </div>
    </>
  );
}