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 |
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 |
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 |
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'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'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}: "{str}"
</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'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 |
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 ? <>‘{props.title}’</> : ''}
</h2>
{props.notes ? <ConstructorNotes notes={props.notes} /> : ''}
<p>
Please look over your grid and clues to make sure everything is
correct. If something didn't import correctly, get in touch
with us via <ContactLinks />.
</p>
<p>
You can edit your title, clues, etc. by clicking
‘Edit’ in the top bar.
</p>
<p>
Once you've looked it over, click ‘Publish’ 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>
</>
);
}