preact/hooks#useMemo JavaScript Examples
The following examples show how to use
preact/hooks#useMemo.
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: CodePanel.js From v8-deopt-viewer with MIT License | 6 votes |
PrismCode = forwardRef(function PrismCode(props, ref) {
const className = [`language-${props.lang}`, props.class].join(" ");
// TODO: File route changes will unmount and delete this cache. May be useful
// to cache across files so switching back and forth between files doesn't
// re-highlight the file each time
const __html = useMemo(
() => Prism.highlight(props.src, Prism.languages[props.lang], props.lang),
[props.src, props.lang]
);
return (
<pre class={className}>
<code ref={ref} class={className} dangerouslySetInnerHTML={{ __html }} />
{props.children}
</pre>
);
})
Example #2
Source File: CodePanel.js From v8-deopt-viewer with MIT License | 6 votes |
LineNumbers = memo(function LineNumbers({ selectedLine, contents }) {
// TODO: Do we want to cache these results beyond renders and for all
// combinations? memo will only remember the last combination.
const lines = useMemo(() => contents.split(NEW_LINE_EXP), [contents]);
return (
<span class="line-numbers-rows" aria-hidden="true">
{lines.map((_, i) => (
<span class={i == selectedLine - 1 ? "active" : null} />
))}
</span>
);
})
Example #3
Source File: Summary.js From v8-deopt-viewer with MIT License | 6 votes |
/**
* @typedef {{ deoptInfo: import('..').PerFileDeoptInfoWithSources; perFileStats: PerFileStats }} SummaryProps
* @param {import('..').AppProps} props
*/
export function Summary({ deoptInfo }) {
const perFileStats = useMemo(() => getPerFileStats(deoptInfo), [deoptInfo]);
return (
<Fragment>
{/* <SummaryList deoptInfo={deoptInfo} perFileStats={perFileStats} /> */}
<SummaryTable deoptInfo={deoptInfo} perFileStats={perFileStats} />
</Fragment>
);
}
Example #4
Source File: appState.js From v8-deopt-viewer with MIT License | 6 votes |
/**
* @typedef AppProviderProps
* @property {import('preact').JSX.Element | import('preact').JSX.Element[]} children
* @param {AppProviderProps} props
*/
export function AppProvider(props) {
const [state, dispatch] = useReducer(appContextReducer, props, initialState);
const dispatchers = useMemo(
() => ({
setSelectedPosition(newPosition) {
dispatch({ type: "SET_SELECTED_POSITION", newPosition });
},
setSelectedEntry(entry) {
dispatch({ type: "SET_SELECTED_ENTRY", entry });
},
}),
[dispatch]
);
return (
<AppDispatchContext.Provider value={dispatchers}>
<AppStateContext.Provider value={state}>
{props.children}
</AppStateContext.Provider>
</AppDispatchContext.Provider>
);
}
Example #5
Source File: pagination.js From rctf with BSD 3-Clause "New" or "Revised" License | 5 votes |
function Pagination ({ totalItems, pageSize, page, setPage, numVisiblePages }) {
numVisiblePages = numVisiblePages || 9
const totalPages = Math.ceil(totalItems / pageSize)
const { pages, startPage, endPage } = useMemo(() => {
// Follow the google pagination principle of always showing 10 items
let startPage, endPage
if (totalPages <= numVisiblePages) {
// Display all
startPage = 1
endPage = totalPages
} else {
// We need to hide some pages
startPage = page - Math.ceil((numVisiblePages - 1) / 2)
endPage = page + Math.floor((numVisiblePages - 1) / 2)
if (startPage < 1) {
startPage = 1
endPage = numVisiblePages
} else if (endPage > totalPages) {
endPage = totalPages
startPage = totalPages - numVisiblePages + 1
}
if (startPage > 1) {
startPage += 2
}
if (endPage < totalPages) {
endPage -= 2
}
}
const pages = [] // ...Array((endPage + 1) - startPage).keys()].map(i => startPage + i)
for (let i = startPage; i <= endPage; i++) {
pages.push(i)
}
return { pages, startPage, endPage }
}, [totalPages, page, numVisiblePages])
const boundSetPages = useMemo(() => {
const bsp = []
for (let i = 1; i <= totalPages; i++) {
bsp.push(() => setPage(i))
}
return bsp
}, [setPage, totalPages])
return (
<div class='pagination u-center'>
<PaginationItem disabled={page === 1} key='<' onClick={boundSetPages[page - 1 - 1]}><</PaginationItem>
{ startPage > 1 &&
<Fragment>
<PaginationItem key={1} onClick={boundSetPages[0]}>1</PaginationItem>
<PaginationEllipses key='.<' />
</Fragment>
}
{pages.map((p) => <PaginationItem selected={p === page} key={p} onClick={boundSetPages[p - 1]}>{p}</PaginationItem>)}
{ endPage < totalPages &&
<Fragment>
<PaginationEllipses key='.>' />
<PaginationItem key={totalPages} onClick={boundSetPages[totalPages - 1]}>{totalPages}</PaginationItem>
</Fragment>
}
<PaginationItem disabled={page === totalPages} key='>' onClick={boundSetPages[page + 1 - 1]}>></PaginationItem>
</div>
)
}
Example #6
Source File: challs.js From rctf with BSD 3-Clause "New" or "Revised" License | 5 votes |
Challenges = ({ classes }) => {
const [problems, setProblems] = useState([])
// newId is the id of the new problem. this allows us to reuse code for problem creation
// eslint-disable-next-line react-hooks/exhaustive-deps
const newId = useMemo(() => uuid(), [problems])
const completeProblems = problems.concat({
...SAMPLE_PROBLEM,
id: newId
})
useEffect(() => {
document.title = `Admin Challenges | ${config.ctfName}`
}, [])
useEffect(() => {
const action = async () => {
setProblems(await getChallenges())
}
action()
}, [])
const updateProblem = useCallback(({ problem }) => {
let nextProblems = completeProblems
// If we aren't creating new problem, remove sample problem first
if (problem.id !== newId) {
nextProblems = nextProblems.filter(p => p.id !== newId)
}
setProblems(nextProblems.map(p => {
// Perform partial update by merging properties
if (p.id === problem.id) {
return {
...p,
...problem
}
}
return p
}))
}, [newId, completeProblems])
return (
<div class={`row ${classes.row}`}>
<div class='col-9'>
{
completeProblems.map(problem => {
return (
<Problem update={updateProblem} key={problem.id} problem={problem} />
)
})
}
</div>
</div>
)
}
Example #7
Source File: FilterInput.js From duolingo-solution-viewer with MIT License | 4 votes |
FilterInput =
({
context = CONTEXT_CHALLENGE,
matchMode = STRING_MATCH_MODE_WORDS,
matchingData = {},
filters = [],
minQueryLength = 2,
onChange = noop,
onFocus = noop,
onBlur = noop,
}) => {
const {
words: suggestableWords = [],
locale = '',
matchingOptions = {},
} = matchingData;
const suggestions = useMemo(() => (
(suggestableWords || []).map((name, id) => ({
id,
name,
searchable: normalizeString(name, false, true),
}))
), [ suggestableWords ]);
// Extracts a word filter from a user query.
const parseWordFilter = useCallback(query => {
const [ , sign = '', start = '', base = '', end = '' ] = /^([-+]?)([*=]?)(.+?)(\*?)$/ug.exec(query) || [];
const word = getStringMatchableWords(base, locale, matchingOptions)[0] || '';
const matchType = MATCH_TYPE_MAP[matchMode][start]?.[end] || MATCH_TYPE_MAP[matchMode][''][end];
const isExcluded = sign === '-';
return { word, matchType, isExcluded };
}, [ matchMode, locale, matchingOptions ]);
// Filters the words that should be proposed to the user based on the current query.
const filterSuggestions = useCallback((query, suggestions) => {
if (0 === suggestions.length) {
return [];
}
const { word } = parseWordFilter(query);
if (word.length < minQueryLength) {
return [];
}
// The underlying library used to remove diacritics is based on a fixed list of characters,
// which lacks some cases such as "ạ" or "ả".
return {
options: matchSorter(
suggestions,
normalizeString(word, false, true),
{
keepDiacritics: true,
keys: [ 'searchable' ],
}
),
highlightedQuery: word,
};
}, [ minQueryLength, parseWordFilter ]);
const tagsInput = useRef();
const blurTagsInput = useCallback(
() => tagsInput.current && setTimeout(() => tagsInput.current.blur()),
[ tagsInput ]
);
const onAddFilter = useCallback(({ id = null, name }, query) => {
let filter;
if (null !== id) {
const { matchType, isExcluded } = parseWordFilter(query);
filter = { word: name, matchType, isExcluded };
} else {
filter = parseWordFilter(name);
}
onChange([ ...filters.filter(it.word !== filter.word), filter ]);
blurTagsInput();
}, [ filters, onChange, parseWordFilter, blurTagsInput ]);
const onUpdateFilter = useCallback((index, filter) => {
if (filters[index]) {
const updated = filters.slice();
updated.splice(index, 1, filter);
onChange(updated);
}
}, [ filters, onChange ]);
const onDeleteFilter = useCallback(index => {
if (filters[index]) {
const updated = filters.slice();
updated.splice(index, 1);
onChange(updated);
}
}, [ filters, onChange ]);
// Use a portal for the sizer, in case the input is rendered inside a hidden container.
const sizerContainer = usePortalContainer();
const onKeyDown = event => {
// Stop propagation of "keydown" events for the search input,
// to prevent Duolingo from handling them when the word bank is active (and calling preventDefault()).
event.stopPropagation();
('Escape' === event.key) && blurTagsInput();
}
useKey('f', event => {
if (
tagsInput.current
&& !event.ctrlKey
&& !event.metaKey
&& !isAnyInputFocused()
) {
discardEvent(event);
setTimeout(() => tagsInput.current.focus({ preventScroll: true }));
}
});
const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ context ]);
return (
<IntlProvider scope="word_filter">
<Localizer>
<ReactTags
ref={tagsInput}
id={`${EXTENSION_CODE}-word-filter-tag`}
tags={filters}
suggestions={suggestions}
suggestionsTransform={filterSuggestions}
allowNew={true}
delimiters={[ 'Enter', ' ', ',', ';' ]}
minQueryLength={minQueryLength}
onAddition={onAddFilter}
onUpdate={onUpdateFilter}
onDelete={onDeleteFilter}
onKeyDown={onKeyDown}
onFocus={onFocus}
onBlur={onBlur}
placeholderText={<Text id="add_filter">Add a filter</Text>}
removeButtonText={<Text id="click_to_remove_filter">Click to remove filter</Text>}
tagComponent={withForcedProps(Filter, { context, matchMode })}
suggestionsComponent={withContext(SuggestionsDropdown, context)}
autoresizePortal={sizerContainer}
classNames={{
root: getElementClassNames(WRAPPER),
rootFocused: getElementClassNames(WRAPPER__ACTIVE),
selected: getElementClassNames(FILTER_WRAPPER),
selectedTag: getElementClassNames(FILTER),
selectedTagName: getElementClassNames(FILTER_WORD),
search: getElementClassNames(SEARCH_WRAPPER),
searchInput: getElementClassNames(SEARCH_INPUT),
suggestions: getElementClassNames(SUGGESTIONS),
suggestion: getElementClassNames(SUGGESTION),
suggestionActive: getElementClassNames(SUGGESTION__ACTIVE),
}}
/>
</Localizer>
</IntlProvider>
);
}
Example #8
Source File: Pagination.js From duolingo-solution-viewer with MIT License | 4 votes |
Pagination =
({
context = CONTEXT_CHALLENGE,
activePage = 1,
totalItemCount = 0,
itemCountPerPage = 20,
displayedPageCount = 5,
onPageChange = noop,
}) => {
const paginator = useMemo(() => (
new Paginator(itemCountPerPage, displayedPageCount)
), [ itemCountPerPage, displayedPageCount ]);
const paginationData = paginator.build(totalItemCount, activePage);
const [ isControlPressed ] = useKeyPress('Control');
const onPreviousKey = useThrottledCallback((data, goToFirst, callback) => {
if (isAnyInputFocused()) {
return;
}
if (data.has_previous_page) {
if (goToFirst) {
callback(1);
} else {
callback(data.previous_page);
}
}
}, 50, [ paginationData, isControlPressed, onPageChange ]);
const onNextKey = useThrottledCallback((data, goToLast, callback) => {
if (isAnyInputFocused()) {
return;
}
if (data.has_next_page) {
if (goToLast) {
callback(data.total_pages);
} else {
callback(data.next_page);
}
}
}, 50, [ paginationData, isControlPressed, onPageChange ]);
useKey('ArrowLeft', onPreviousKey, {}, [ onPreviousKey ]);
useKey('ArrowRight', onNextKey, {}, [ onNextKey ]);
const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ context ]);
if (totalItemCount <= itemCountPerPage) {
return null;
}
const renderButton = ({ key, disabled, label, title, titleKey, titleFields = {}, onClick }) => {
let buttonClassNames = getElementClassNames(BUTTON);
if (isNumber(label)) {
buttonClassNames += ` ${getElementClassNames(INDEX_BUTTON)}`;
}
buttonClassNames += ` ${getElementClassNames(disabled ? DISABLED_BUTTON : ENABLED_BUTTON)}`;
return (
<div key={key} className={getElementClassNames(ITEM)}>
<Localizer>
<button
title={<Text id={titleKey} fields={titleFields}>{title}</Text>}
disabled={disabled}
onClick={onClick}
className={buttonClassNames}
>
<span className={getElementClassNames(BUTTON_LABEL)}>{label}</span>
</button>
</Localizer>
</div>
);
};
const pageButtons = [
renderButton({
key: 'first',
label: '«',
title: 'Go to first page',
titleKey: 'go_to_first',
disabled: !paginationData.has_previous_page,
onClick: () => onPageChange(1),
}),
renderButton({
key: 'previous',
label: '⟨',
title: 'Go to previous page',
titleKey: 'go_to_previous',
disabled: !paginationData.has_previous_page,
onClick: () => onPageChange(paginationData.previous_page),
}),
];
for (let page = paginationData.first_page; page <= paginationData.last_page; page++) {
pageButtons.push(
renderButton({
key: `page-${page}`,
label: page,
title: 'Go to page {{page}}',
titleKey: 'go_to_page',
titleFields: { page },
disabled: paginationData.current_page === page,
onClick: () => onPageChange(page),
}),
);
}
pageButtons.push(
renderButton({
key: 'next',
label: '⟩',
title: 'Go to next page',
titleKey: 'go_to_next',
disabled: !paginationData.has_next_page,
onClick: () => onPageChange(paginationData.next_page),
}),
renderButton({
key: 'last',
label: '»',
title: 'Go to last page',
titleKey: 'go_to_last',
disabled: paginationData.current_page === paginationData.total_pages,
onClick: () => onPageChange(paginationData.total_pages),
}),
);
return (
<IntlProvider scope="pagination">
<div className={getElementClassNames(WRAPPER)}>
{pageButtons}
</div>
</IntlProvider>
);
}
Example #9
Source File: SolutionList.js From duolingo-solution-viewer with MIT License | 4 votes |
SolutionList =
forwardRef(
(
{
context = CONTEXT_CHALLENGE,
solutions = [],
matchingData = {},
onPageChange = noop,
scrollOffsetGetter = (() => 0),
},
listRef
) => {
const isScoreAvailable = useMemo(() => {
return solutions.some('score' in it);
}, [ solutions ]);
const sortTypes = getAvailableSortTypes(isScoreAvailable);
const {
state: sortType,
nextState: nextSortType,
next: setNextSortType,
} = useLocalStorageList(
'sort-type',
sortTypes,
sortTypes[0]
);
const {
state: sortDirection,
nextState: nextSortDirection,
next: setNextSortDirection,
} = useLocalStorageList(
'sort-direction',
Object.keys(SORT_DIRECTIONS),
SORT_DIRECTION_DESC
);
const isFilterWordBased = !!matchingData.words;
// Sort the solutions.
const sortedSolutions = useMemo(() => (
solutions.slice()
.sort(
SORT_TYPE_SIMILARITY === sortType
? (SORT_DIRECTION_ASC === sortDirection ? invertComparison : identity)(Solution.compareByScore)
: (SORT_DIRECTION_ASC === sortDirection ? identity : invertComparison)(Solution.compareByReference)
)
), [ solutions, sortType, sortDirection ]);
// Filter the solutions.
const filterCache = useRef({}).current;
const [ filters, filtersRef, setFilters ] = useStateRef([]);
const filteredSolutions = useMemo(
() => (
isFilterWordBased
? filterSolutionsUsingWords
: filterSolutionsUsingSummaries
)(sortedSolutions, filters, filterCache),
[ sortedSolutions, filters, filterCache, isFilterWordBased ]
);
// Paginate and render the current solutions.
const [ rawPage, setRawPage ] = useState(1);
const shouldTriggerPageChange = useRef(false);
const [ pageSize, setRawPageSize ] = useLocalStorage('page_size', DEFAULT_PAGE_SIZE);
const page = (PAGE_SIZE_ALL === pageSize)
? 1
: Math.min(rawPage, Math.ceil(filteredSolutions.length / pageSize));
const setPage = useCallback(page => {
setRawPage(page);
shouldTriggerPageChange.current = true;
}, [ setRawPage ]);
const setPageSize = useCallback(size => {
setRawPageSize(size);
if (PAGE_SIZE_ALL === size) {
setRawPage(1);
} else {
// Update the current page to keep the same solution at the top of the list.
const sizeValue = Number(size);
if (PAGE_SIZES.indexOf(sizeValue) === -1) {
return;
}
const oldSize = (PAGE_SIZE_ALL === pageSize)
? filteredSolutions.length
: Math.min(pageSize, filteredSolutions.length);
setRawPage(Math.ceil(((page - 1) * oldSize + 1) / sizeValue));
}
shouldTriggerPageChange.current = true;
}, [ page, pageSize, filteredSolutions.length, setRawPageSize ]);
const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ context ]);
const solutionItems = useMemo(() => {
const renderSolutionItem = solution => (
<li className={getElementClassNames(SOLUTION)}>
{Solution.getReaderFriendlySummary(solution)}
</li>
);
const pageSolutions = (PAGE_SIZE_ALL === pageSize)
? filteredSolutions
: filteredSolutions.slice((page - 1) * pageSize, page * pageSize);
return pageSolutions.map(renderSolutionItem);
}, [ page, pageSize, filteredSolutions, getElementClassNames ]);
// Triggers the "page change" callback asynchronously,
// to make sure it is run only when the changes have been applied to the UI.
useEffect(() => {
if (shouldTriggerPageChange.current) {
setTimeout(onPageChange());
shouldTriggerPageChange.current = false;
}
}, [ solutionItems, onPageChange, shouldTriggerPageChange ]);
const filterWrapperRef = useRef();
// Scrolls the filter input into view when it is focused.
const onFilterFocus = useCallback(() => {
filterWrapperRef.current
&& scrollElementIntoParentView(filterWrapperRef.current, scrollOffsetGetter(), 'smooth');
}, [ scrollOffsetGetter, filterWrapperRef ]);
// Focuses the solution list when the filter input loses focus, to ensure that the list is scrollable again.
const onFilterBlur = useCallback(() => listRef.current?.closest('[tabindex]')?.focus(), [ listRef ]);
// Detects selected words, and proposes new filter options when relevant.
const [ selectedWord, setSelectedWord ] = useState(null);
useEffect(() => {
// Detect when the left button is released to only propose suggestions when a selection has been committed.
const onMouseUp = event => {
if (listRef.current && (event.button === 0)) {
const selection = document.getSelection();
if (
selection.anchorNode
&& (selection.anchorNode === selection.focusNode)
&& listRef.current.contains(selection.anchorNode)
&& (selection.anchorNode.parentNode.nodeName === 'LI')
) {
// We are only interested in single-word selections.
const words = Solution.getStringMatchableWords(
selection.toString().trim(),
matchingData.locale,
matchingData.matchingOptions
);
if (1 === words.length) {
const selectedText = !isFilterWordBased
? selection.toString()
: getWordAt(
selection.anchorNode.wholeText,
Math.floor((selection.anchorOffset + selection.focusOffset) / 2)
);
const [ word = '' ] = Solution.getStringMatchableWords(
selectedText,
matchingData.locale,
matchingData.matchingOptions
);
if (
(!isFilterWordBased || (word.length > 1))
&& !(filtersRef.current || []).some(it.word === word)
) {
const bbox = selection.getRangeAt(0).getBoundingClientRect();
const offsetParent = getFixedElementPositioningParent(listRef.current);
if (offsetParent) {
const parentBbox = offsetParent.getBoundingClientRect();
bbox.x -= parentBbox.x;
bbox.y -= parentBbox.y;
}
setSelectedWord({
word,
bbox: {
left: `${Math.floor(bbox.x)}px`,
top: `${Math.floor(bbox.y)}px`,
width: `${Math.ceil(bbox.width)}px`,
height: `${Math.ceil(bbox.height)}px`,
},
});
return;
}
}
}
}
// Delay hiding the actions dropdown to let "click" events be triggered normally.
setTimeout(() => setSelectedWord(null));
};
// Detect change events to ensure that suggestions are hidden when the selection is canceled.
const onSelectionChange = () => {
const selection = document.getSelection();
if (!selection || ('None' === selection.type)) {
setSelectedWord(null);
}
};
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('selectionchange', onSelectionChange);
return () => {
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('selectionchange', onSelectionChange);
}
});
if (0 === solutions.length) {
return null;
}
return (
<IntlProvider scope="solution_list">
<div>
<h3 ref={filterWrapperRef} className={getElementClassNames(TITLE)}>
<span className={getElementClassNames(TITLE_TEXT)}>
<Text id="filter">Filter:</Text>
</span>
<FilterInput
context={context}
matchMode={isFilterWordBased ? STRING_MATCH_MODE_WORDS : STRING_MATCH_MODE_GLOBAL}
matchingData={matchingData}
minQueryLength={isFilterWordBased ? 2 : 1}
filters={filters}
onChange={setFilters}
onFocus={onFilterFocus}
onBlur={onFilterBlur}
/>
</h3>
<div ref={listRef}>
<h3 className={getElementClassNames(TITLE)}>
<span className={getElementClassNames(TITLE_TEXT)}>
<Text id="correct_solutions">Correct solutions:</Text>
</span>
<ListSortLinks
context={context}
availableSortTypes={sortTypes}
sortType={sortType}
nextSortType={nextSortType}
sortDirection={sortDirection}
nextSortDirection={nextSortDirection}
onSortTypeToggle={() => setNextSortType()}
onSortDirectionToggle={() => setNextSortDirection()}
/>
</h3>
{(0 === filteredSolutions.length)
? (
<div className={getElementClassNames(EMPTY_LIST)}>
<Text id="no_matching_solution">There is no matching solution.</Text>
</div>
) : (
<Fragment>
<ul>{solutionItems}</ul>
{selectedWord && (
<SelectedWordActions
{...selectedWord}
context={context}
matchType={isFilterWordBased ? STRING_MATCH_TYPE_EXACT : STRING_MATCH_TYPE_ANYWHERE}
onAddFilter={setFilters([ ...filters, _ ])}
/>
)}
<ListPagination
context={context}
solutionCount={filteredSolutions.length}
page={page}
pageSize={pageSize}
onPageChange={setPage}
onPageSizeChange={setPageSize}
/>
</Fragment>
)}
</div>
</div>
</IntlProvider>
);
}
)
Example #10
Source File: graph.js From rctf with BSD 3-Clause "New" or "Revised" License | 4 votes |
function Graph ({ graphData, classes }) {
const svgRef = useRef(null)
const [width, setWidth] = useState(window.innerWidth)
const updateWidth = useCallback(() => {
if (svgRef.current === null) return
setWidth(svgRef.current.getBoundingClientRect().width)
}, [])
const [tooltipData, setTooltipData] = useState({
x: 0,
y: 0,
content: ''
})
useLayoutEffect(() => {
updateWidth()
}, [updateWidth])
useEffect(() => {
function handleResize () {
updateWidth()
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [updateWidth])
const { polylines, labels } = useMemo(() => {
if (!graphData || graphData.length === 0) {
return {
polylines: [],
labels: []
}
}
const minX = config.startTime
const maxX = Math.min(Date.now(), config.endTime)
let maxY = 0
graphData.graph.forEach((user) => {
user.points.forEach((point) => {
if (point.score > maxY) {
maxY = point.score
}
})
})
const labels = getXLabels({ minX, maxX, width })
const polylines = graphData.graph.map((user) => pointsToPolyline({
points: user.points,
id: user.id,
name: user.name,
currentScore: user.points[0].score,
maxX,
minX,
maxY,
width
}))
return { polylines, labels }
}, [graphData, width])
const handleTooltipIn = useCallback((content) => () => {
setTooltipData(d => ({
...d,
content
}))
}, [])
const handleTooltipMove = useCallback((evt) => {
setTooltipData(d => ({
...d,
x: evt.clientX,
y: evt.clientY
}))
}, [])
const handleTooltipOut = useCallback(() => {
setTooltipData(d => ({
...d,
content: ''
}))
}, [])
if (graphData === null) {
return null
}
return (
<div class={`frame ${classes.root}`}>
<div class='frame__body'>
<svg ref={svgRef} viewBox={`${-stroke - axis} ${-stroke} ${width + stroke * 2 + axis} ${height + stroke * 2 + axis + axisGap}`}>
<Fragment>
{polylines.map(({ points, color, name, currentScore }, i) => (
<GraphLine
key={i}
stroke={color}
points={points}
name={name}
currentScore={currentScore}
onMouseMove={handleTooltipMove}
onMouseOut={handleTooltipOut}
onTooltipIn={handleTooltipIn}
/>
))}
</Fragment>
<Fragment>
{labels.map((label, i) => (
<text x={label.x} y={height + axis + axisGap} key={i} fill='#fff'>{label.label}</text>
))}
</Fragment>
<line
x1={-axisGap}
y1={height + axisGap}
x2={width}
y2={height + axisGap}
stroke='var(--cirrus-bg)'
stroke-linecap='round'
stroke-width={stroke}
/>
<line
x1={-axisGap}
y1='0'
x2={-axisGap}
y2={height + axisGap}
stroke='var(--cirrus-bg)'
stroke-linecap='round'
stroke-width={stroke}
/>
</svg>
</div>
{tooltipData.content && (
<div
class={classes.tooltip}
style={{
transform: `translate(${tooltipData.x}px, ${tooltipData.y}px)`
}}
>
{tooltipData.content}
</div>
)}
</div>
)
}
Example #11
Source File: challs.js From rctf with BSD 3-Clause "New" or "Revised" License | 4 votes |
Challenges = ({ classes }) => {
const challPageState = useMemo(() => JSON.parse(localStorage.getItem('challPageState') || '{}'), [])
const [problems, setProblems] = useState(null)
const [categories, setCategories] = useState(challPageState.categories || {})
const [showSolved, setShowSolved] = useState(challPageState.showSolved || false)
const [solveIDs, setSolveIDs] = useState([])
const [loadState, setLoadState] = useState(loadStates.pending)
const { toast } = useToast()
const setSolved = useCallback(id => {
setSolveIDs(solveIDs => {
if (!solveIDs.includes(id)) {
return [...solveIDs, id]
}
return solveIDs
})
}, [])
const handleShowSolvedChange = useCallback(e => {
setShowSolved(e.target.checked)
}, [])
const handleCategoryCheckedChange = useCallback(e => {
setCategories(categories => ({
...categories,
[e.target.dataset.category]: e.target.checked
}))
}, [])
useEffect(() => {
document.title = `Challenges | ${config.ctfName}`
}, [])
useEffect(() => {
const action = async () => {
if (problems !== null) {
return
}
const { data, error, notStarted } = await getChallenges()
if (error) {
toast({ body: error, type: 'error' })
return
}
setLoadState(notStarted ? loadStates.notStarted : loadStates.loaded)
if (notStarted) {
return
}
const newCategories = { ...categories }
data.forEach(problem => {
if (newCategories[problem.category] === undefined) {
newCategories[problem.category] = false
}
})
setProblems(data)
setCategories(newCategories)
}
action()
}, [toast, categories, problems])
useEffect(() => {
const action = async () => {
const { data, error } = await getPrivateSolves()
if (error) {
toast({ body: error, type: 'error' })
return
}
setSolveIDs(data.map(solve => solve.id))
}
action()
}, [toast])
useEffect(() => {
localStorage.challPageState = JSON.stringify({ categories, showSolved })
}, [categories, showSolved])
const problemsToDisplay = useMemo(() => {
if (problems === null) {
return []
}
let filtered = problems
if (!showSolved) {
filtered = filtered.filter(problem => !solveIDs.includes(problem.id))
}
let filterCategories = false
Object.values(categories).forEach(displayCategory => {
if (displayCategory) filterCategories = true
})
if (filterCategories) {
Object.keys(categories).forEach(category => {
if (categories[category] === false) {
// Do not display this category
filtered = filtered.filter(problem => problem.category !== category)
}
})
}
filtered.sort((a, b) => {
if (a.points === b.points) {
if (a.solves === b.solves) {
const aWeight = a.sortWeight || 0
const bWeight = b.sortWeight || 0
return bWeight - aWeight
}
return b.solves - a.solves
}
return a.points - b.points
})
return filtered
}, [problems, categories, showSolved, solveIDs])
const { categoryCounts, solvedCount } = useMemo(() => {
const categoryCounts = new Map()
let solvedCount = 0
if (problems !== null) {
for (const problem of problems) {
if (!categoryCounts.has(problem.category)) {
categoryCounts.set(problem.category, {
total: 0,
solved: 0
})
}
const solved = solveIDs.includes(problem.id)
categoryCounts.get(problem.category).total += 1
if (solved) {
categoryCounts.get(problem.category).solved += 1
}
if (solved) {
solvedCount += 1
}
}
}
return { categoryCounts, solvedCount }
}, [problems, solveIDs])
if (loadState === loadStates.pending) {
return null
}
if (loadState === loadStates.notStarted) {
return <NotStarted />
}
return (
<div class={`row ${classes.row}`}>
<div class='col-3'>
<div class={`frame ${classes.frame}`}>
<div class='frame__body'>
<div class='frame__title title'>Filters</div>
<div class={classes.showSolved}>
<div class='form-ext-control form-ext-checkbox'>
<input id='show-solved' class='form-ext-input' type='checkbox' checked={showSolved} onChange={handleShowSolvedChange} />
<label for='show-solved' class='form-ext-label'>Show Solved ({solvedCount}/{problems.length} solved)</label>
</div>
</div>
</div>
</div>
<div class={`frame ${classes.frame}`}>
<div class='frame__body'>
<div class='frame__title title'>Categories</div>
{
Array.from(categoryCounts.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([category, { solved, total }]) => {
return (
<div key={category} class='form-ext-control form-ext-checkbox'>
<input id={`category-${category}`} data-category={category} class='form-ext-input' type='checkbox' checked={categories[category]} onChange={handleCategoryCheckedChange} />
<label for={`category-${category}`} class='form-ext-label'>{category} ({solved}/{total} solved)</label>
</div>
)
})
}
</div>
</div>
</div>
<div class='col-6'>
{
problemsToDisplay.map(problem => {
return (
<Problem
key={problem.id}
problem={problem}
solved={solveIDs.includes(problem.id)}
setSolved={setSolved}
/>
)
})
}
</div>
</div>
)
}
Example #12
Source File: scoreboard.js From rctf with BSD 3-Clause "New" or "Revised" License | 4 votes |
Scoreboard = withStyles({
frame: {
paddingBottom: '1.5em',
paddingTop: '2.125em',
background: '#222',
'& .frame__subtitle': {
color: '#fff'
},
'& button, & select, & option': {
background: '#111',
color: '#fff'
}
},
tableFrame: {
paddingTop: '1.5em'
},
selected: {
backgroundColor: 'rgba(216,216,216,.07)',
'&:hover': {
backgroundColor: 'rgba(216,216,216,.20) !important'
}
},
table: {
tableLayout: 'fixed',
'& tbody td': {
overflow: 'hidden',
whiteSpace: 'nowrap'
}
}
}, ({ classes }) => {
const loggedIn = useMemo(() => localStorage.getItem('token') !== null, [])
const scoreboardPageState = useMemo(() => {
const localStorageState = JSON.parse(localStorage.getItem('scoreboardPageState') || '{}')
const queryParams = new URLSearchParams(location.search)
const queryState = {}
if (queryParams.has('page')) {
const page = parseInt(queryParams.get('page'))
if (!isNaN(page)) {
queryState.page = page
}
}
if (queryParams.has('pageSize')) {
const pageSize = parseInt(queryParams.get('pageSize'))
if (!isNaN(pageSize)) {
queryState.pageSize = pageSize
}
}
if (queryParams.has('division')) {
queryState.division = queryParams.get('division')
}
return { ...localStorageState, ...queryState }
}, [])
const [profile, setProfile] = useState(null)
const [pageSize, _setPageSize] = useState(scoreboardPageState.pageSize || 100)
const [scores, setScores] = useState([])
const [graphData, setGraphData] = useState(null)
const [division, _setDivision] = useState(scoreboardPageState.division || 'all')
const [page, setPage] = useState(scoreboardPageState.page || 1)
const [totalItems, setTotalItems] = useState(0)
const [scoreLoadState, setScoreLoadState] = useState(loadStates.pending)
const [graphLoadState, setGraphLoadState] = useState(loadStates.pending)
const selfRow = useRef()
const { toast } = useToast()
const setDivision = useCallback((newDivision) => {
_setDivision(newDivision)
setPage(1)
}, [_setDivision, setPage])
const setPageSize = useCallback((newPageSize) => {
_setPageSize(newPageSize)
// Try to switch to the page containing the teams that were previously
// at the top of the current page
setPage(Math.floor((page - 1) * pageSize / newPageSize) + 1)
}, [pageSize, _setPageSize, page, setPage])
useEffect(() => {
localStorage.setItem('scoreboardPageState', JSON.stringify({ pageSize, division }))
}, [pageSize, division])
useEffect(() => {
if (page !== 1 || location.search !== '') {
history.replaceState({}, '', `?page=${page}&division=${encodeURIComponent(division)}&pageSize=${pageSize}`)
}
}, [pageSize, division, page])
const divisionChangeHandler = useCallback((e) => setDivision(e.target.value), [setDivision])
const pageSizeChangeHandler = useCallback((e) => setPageSize(e.target.value), [setPageSize])
useEffect(() => { document.title = `Scoreboard | ${config.ctfName}` }, [])
useEffect(() => {
if (loggedIn) {
privateProfile()
.then(({ data, error }) => {
if (error) {
toast({ body: error, type: 'error' })
}
setProfile(data)
})
}
}, [loggedIn, toast])
useEffect(() => {
(async () => {
const _division = division === 'all' ? undefined : division
const { kind, data } = await getScoreboard({
division: _division,
offset: (page - 1) * pageSize,
limit: pageSize
})
setScoreLoadState(kind === 'badNotStarted' ? loadStates.notStarted : loadStates.loaded)
if (kind !== 'goodLeaderboard') {
return
}
setScores(data.leaderboard.map((entry, i) => ({
...entry,
rank: i + 1 + (page - 1) * pageSize
})))
setTotalItems(data.total)
})()
}, [division, page, pageSize])
useEffect(() => {
(async () => {
const _division = division === 'all' ? undefined : division
const { kind, data } = await getGraph({ division: _division })
setGraphLoadState(kind === 'badNotStarted' ? loadStates.notStarted : loadStates.loaded)
if (kind !== 'goodLeaderboard') {
return
}
setGraphData(data)
})()
}, [division])
const isUserOnCurrentScoreboard = (
loggedIn &&
profile !== null &&
profile.globalPlace !== null &&
(division === 'all' || Number.parseInt(division) === profile.division)
)
const isSelfVisible = useMemo(() => {
if (profile == null) return false
let isSelfVisible = false
// TODO: maybe avoiding iterating over scores again?
scores.forEach(({ id }) => {
if (id === profile.id) {
isSelfVisible = true
}
})
return isSelfVisible
}, [profile, scores])
const scrollToSelf = useCallback(() => {
selfRow.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
}, [selfRow])
const [needsScrollToSelf, setNeedsScrollToSelf] = useState(false)
const goToSelfPage = useCallback(() => {
if (!isUserOnCurrentScoreboard) return
let place
if (division === 'all') {
place = profile.globalPlace
} else {
place = profile.divisionPlace
}
setPage(Math.floor((place - 1) / pageSize) + 1)
if (isSelfVisible) {
scrollToSelf()
} else {
setNeedsScrollToSelf(true)
}
}, [profile, setPage, pageSize, division, isUserOnCurrentScoreboard, isSelfVisible, scrollToSelf])
useEffect(() => {
if (needsScrollToSelf) {
if (isSelfVisible) {
scrollToSelf()
setNeedsScrollToSelf(false)
}
}
}, [isSelfVisible, needsScrollToSelf, scrollToSelf])
if (scoreLoadState === loadStates.pending || graphLoadState === loadStates.pending) {
return null
}
if (scoreLoadState === loadStates.notStarted || graphLoadState === loadStates.notStarted) {
return <NotStarted />
}
return (
<div class='row u-center' style='align-items: initial !important'>
<div class='col-12 u-center'>
<div class='col-8'>
<Graph graphData={graphData} />
</div>
</div>
<div class='col-3'>
<div class={`frame ${classes.frame}`}>
<div class='frame__body'>
<div class='frame__subtitle'>Filter by division</div>
<div class='input-control'>
<select required class='select' name='division' value={division} onChange={divisionChangeHandler}>
<option value='all' selected>All</option>
{
Object.entries(config.divisions).map(([code, name]) => {
return <option key={code} value={code}>{name}</option>
})
}
</select>
</div>
<div class='frame__subtitle'>Teams per page</div>
<div class='input-control'>
<select required class='select' name='pagesize' value={pageSize} onChange={pageSizeChangeHandler}>
{ PAGESIZE_OPTIONS.map(sz => <option value={sz}>{sz}</option>) }
</select>
</div>
{ loggedIn &&
<div class='btn-container u-center'>
<button disabled={!isUserOnCurrentScoreboard} onClick={goToSelfPage}>
Go to my team
</button>
</div>
}
</div>
</div>
</div>
<div class='col-6'>
<div class={`frame ${classes.frame} ${classes.tableFrame}`}>
<div class='frame__body'>
<table class={`table small ${classes.table}`}>
<thead>
<tr>
<th style='width: 3.5em'>#</th>
<th>Team</th>
<th style='width: 5em'>Points</th>
</tr>
</thead>
<tbody>
{ scores.map(({ id, name, score, rank }) => {
const isSelf = profile != null && profile.id === id
return (
<tr key={id}
class={isSelf ? classes.selected : ''}
ref={isSelf ? selfRow : null}
>
<td>{rank}</td>
<td>
<a href={`/profile/${id}`}>{name}</a>
</td>
<td>{score}</td>
</tr>
)
}) }
</tbody>
</table>
</div>
{ totalItems > pageSize &&
<Pagination
{...{ totalItems, pageSize, page, setPage }}
numVisiblePages={9}
/>
}
</div>
</div>
</div>
)
})