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 vote down vote up
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 vote down vote up
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 vote down vote up
/**
 * @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 vote down vote up
/**
 * @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 vote down vote up
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]}>&lt;</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]}>&gt;</PaginationItem>
    </div>
  )
}
Example #6
Source File: challs.js    From rctf with BSD 3-Clause "New" or "Revised" License 5 votes vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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>
  )
})