preact/hooks#useRef JavaScript Examples
The following examples show how to use
preact/hooks#useRef.
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: DeoptTables.js From v8-deopt-viewer with MIT License | 6 votes |
/**
* @param {boolean} selected
* @returns {import("preact").RefObject<HTMLDivElement>}
*/
function useScrollIntoView(selected) {
/** @type {import("preact").RefObject<HTMLDivElement>} */
const ref = useRef(null);
useEffect(() => {
if (selected) {
// TODO: Why doesn't the smooth behavior always work? It seems that only
// the first or last call to scrollIntoView with behavior smooth works?
ref.current.scrollIntoView({ block: "center" });
}
}, [selected]);
return selected ? ref : null;
}
Example #2
Source File: index.js From duolingo-solution-viewer with MIT License | 6 votes |
useLocalStorageList = (key, stateSet, initialValue) => {
const isInitialized = useRef(false);
const [ storedState, storeState ] = useLocalStorage(key, initialValue);
const { state, prevState, nextState, prev, next } = useStateList(
stateSet,
stateSet.indexOf(storedState) === -1 ? initialValue : storedState
);
useEffect(() => {
// Skip the first call to prevent overriding the stored state with a temporary default state.
if (isInitialized.current) {
storeState(state)
} else {
isInitialized.current = true;
}
}, [ state, storeState ]);
return { state, prevState, nextState, prev, next };
}
Example #3
Source File: index.js From duolingo-solution-viewer with MIT License | 6 votes |
useThrottledCallback = (callback, delay = 200, args) => {
const timeout = useRef();
return useCallback(() => {
if (!timeout.current) {
callback(...args);
timeout.current = setTimeout(() => (timeout.current = null), delay);
}
}, args.concat(callback, delay)); // eslint-disable-line react-hooks/exhaustive-deps
}
Example #4
Source File: CodePanel.js From v8-deopt-viewer with MIT License | 5 votes |
/**
* @typedef CodePanelProps
* @property {import("../").FileV8DeoptInfoWithSources} fileDeoptInfo
* @property {number} fileId
* @property {import('./CodeSettings').CodeSettingsState} settings
* @param {CodePanelProps} props
*/
export function CodePanel({ fileDeoptInfo, fileId, settings }) {
if (fileDeoptInfo.srcError) {
return <CodeError srcError={fileDeoptInfo.srcError} />;
} else if (!fileDeoptInfo.src) {
return <CodeError srcError="No sources for the file were found." />;
}
const lang = determineLanguage(fileDeoptInfo.srcPath);
const state = useAppState();
const selectedLine = state.selectedPosition?.line;
/**
* @typedef {Map<string, import('../utils/deoptMarkers').Marker>} MarkerMap
* @type {[MarkerMap, import('preact/hooks').StateUpdater<MarkerMap>]}
*/
const [markers, setMarkers] = useState(null);
/** @type {import('preact').RefObject<HTMLElement>} */
const codeRef = useRef(null);
useLayoutEffect(() => {
// Saved the new markers so we can select them when CodePanelContext changes
const markers = addDeoptMarkers(codeRef.current, fileId, fileDeoptInfo);
setMarkers(new Map(markers.map((marker) => [marker.id, marker])));
}, [fileId, fileDeoptInfo]);
useEffect(() => {
if (state.prevSelectedEntry) {
markers
.get(getMarkerId(state.prevSelectedEntry))
?.classList.remove(active);
}
/** @type {ScrollIntoViewOptions} */
const scrollIntoViewOpts = { block: "center", behavior: "smooth" };
if (state.selectedEntry) {
const target = markers.get(getMarkerId(state.selectedEntry));
target.classList.add(active);
// TODO: Why doesn't the smooth behavior always work? It seems that only
// the first or last call to scrollIntoView with behavior smooth works?
target.scrollIntoView(scrollIntoViewOpts);
} else if (state.selectedPosition) {
const lineSelector = `.line-numbers-rows > span:nth-child(${state.selectedPosition.line})`;
document.querySelector(lineSelector)?.scrollIntoView(scrollIntoViewOpts);
}
// TODO: Figure out how to scroll line number into view when
// selectedPosition is set but selectedMarkerId is not
}, [state]);
return (
<div
class={[
codePanel,
(settings.showLowSevs && showLowSevsClass) || null,
].join(" ")}
>
<PrismCode
src={fileDeoptInfo.src}
lang={lang}
class={(!settings.hideLineNums && "line-numbers") || null}
ref={codeRef}
>
<LineNumbers selectedLine={selectedLine} contents={fileDeoptInfo.src} />
</PrismCode>
</div>
);
}
Example #5
Source File: Dropdown.js From duolingo-solution-viewer with MIT License | 5 votes |
Dropdown = forwardRef(
(
{
context = CONTEXT_CHALLENGE,
getOptionKey = ((option, index) => index),
renderOption = (option => <Item {...option} context={context} />),
options = [],
onSelect = noop,
onClose = noop,
},
ref
) => {
const wrapper = useRef();
const content = useRef();
const portalContainer = usePortalContainer();
const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ context ]);
// Positions the content at the right spot, and closes the dropdown on any scroll or resize event.
useEffect(() => {
if (wrapper.current && content.current) {
const { left: wrapperLeft, top: wrapperTop } = wrapper.current.getBoundingClientRect();
const itemsWidth = content.current.clientWidth;
const itemsBaseLeft = wrapperLeft - Math.ceil(itemsWidth / 2);
const itemsMinLeft = 10;
const itemsMaxLeft = document.body.clientWidth - itemsWidth - itemsMinLeft;
const itemsLeft = Math.max(itemsMinLeft, Math.min(itemsBaseLeft, itemsMaxLeft));
content.current.style.setProperty('top', `${wrapperTop}px`);
content.current.style.setProperty('left', `${itemsLeft}px`);
content.current.style.setProperty('visibility', 'visible', 'important');
const scrollableAncestors = getAncestorsWithScrollOverflow(wrapper.current);
window.addEventListener('resize', onClose);
scrollableAncestors.forEach(it.addEventListener('scroll', onClose));
return () => {
window.removeEventListener('resize', onClose);
scrollableAncestors.forEach(it.removeEventListener('scroll', onClose));
}
}
}, [ onClose, wrapper, content ]);
// Renders a single option.
const renderOptionItem = (option, index) => {
const key = getOptionKey(option, index);
const onClick = event => {
discardEvent(event);
onSelect(key);
};
return (
<div key={key} onClick={onClick} className={getElementClassNames(ITEM_WRAPPER)}>
{renderOption(option)}
</div>
);
};
return (
<div ref={wrapper} className={getElementClassNames(WRAPPER)}>
{createPortal(
<div ref={useMergeRefs([ ref, content ])} className={getElementClassNames(CONTENT)}>
<div className={getElementClassNames(ITEMS)}>
{options.map(renderOptionItem)}
</div>
</div>,
portalContainer
)}
{/* Keep the arrow within the DOM hierarchy so that it follows the content. */}
<div className={getElementClassNames(ARROW)}>
<div className={getElementClassNames(ARROW_ICON)} />
</div>
</div>
);
}
)
Example #6
Source File: FilterInput.js From duolingo-solution-viewer with MIT License | 5 votes |
FilterSetting = ({ context, setting: { key, values }, currentFilter, onUpdate }) => {
const menu = useRef();
const wrapper = useRef();
const [ isMenuDisplayed, setIsMenuDisplayed ] = useState(false);
const currentValue = values.find(it.value === currentFilter[key]);
const onCloseMenu = () => setIsMenuDisplayed(false);
const onSelect = value => {
onCloseMenu();
onUpdate({ ...currentFilter, [key]: value });
};
const onToggleMenu = event => {
discardEvent(event);
setIsMenuDisplayed(!isMenuDisplayed);
};
useClickAway([ wrapper, menu ], onCloseMenu);
const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ context ]);
return (
<Localizer>
<div
ref={wrapper}
onClick={onToggleMenu}
title={<Text id={currentValue.labelId}>{currentValue.defaultLabel}</Text>}
className={getElementClassNames(FILTER_SETTING)}
>
<FontAwesomeIcon
icon={currentValue.icon}
size="xs"
fixedWidth
className={getElementClassNames(FILTER_SETTING_ICON)}
/>
{isMenuDisplayed && (
<div>
<Dropdown
ref={menu}
context={context}
options={values}
getOptionKey={({ value }) => value}
onSelect={onSelect}
onClose={onCloseMenu}
/>
</div>
)}
</div>
</Localizer>
);
}
Example #7
Source File: Item.js From vegemite with MIT License | 5 votes |
// ---
export default function (props) {
const { id, title, completed } = props;
const editor = useRef(null);
const [editing, setEditing] = useState(false);
useEffect(() => {
if (editing) {
editor.current.value = title;
editor.current.focus();
}
}, [editing]);
const classname = [
editing && 'editing',
completed && 'completed',
].filter(Boolean).join(' ');
function onToggle() {
todomvc.dispatch('todo:toggle', id);
}
function onDestroy() {
todomvc.dispatch('todo:del', id);
}
function onDblClick() {
setEditing(true);
}
function onblur(ev) {
let value = ev.target.value.trim();
if (value.length > 0) {
todomvc.dispatch('todo:put', { id, value });
}
ev.target.value = null;
setEditing(false);
}
function onkeydown(ev) {
if (ev.which === 27) {
ev.target.value = null;
setEditing(false);
} else if (ev.which === 13) {
onblur(ev);
}
}
return (
<li class={classname}>
<div class="view">
<input type="checkbox" class="toggle" checked={completed} onchange={onToggle} />
<label onDblClick={onDblClick}>{title}</label>
<button class="destroy" onclick={onDestroy} />
</div>
{ editing && <input ref={editor} class="edit" onblur={onblur} onkeydown={onkeydown} /> }
</li>
);
}
Example #8
Source File: Form.jsx From todo-pwa with MIT License | 5 votes |
Form = ({ className = '', itemsAdd }) => {
const input = useRef(null);
const [value, setValue] = useState(
new URL(window.location).searchParams.get('title') || ''
);
useEffect(() => {
// The shareTargetAPI creates a get Request that looks like this:
// /preact/?title={title}&text={text}&url={url}
const params = new URL(window.location).searchParams;
const v = [
...(params.get('title') ? [params.get('title')] : []),
...(params.get('text') ? [params.get('text')] : []),
...(params.get('url') ? [params.get('url')] : []),
];
setValue(v.join(' - '));
}, []);
return (
<div className={className}>
<form
className="flex items-stretch"
autocomplete="off"
onSubmit={e => {
e.preventDefault();
if (value !== '') {
itemsAdd(value);
setValue('');
input.current.focus();
}
}}
>
<input
type="text"
name="title"
id="title"
className="appearance-none border rounded rounded-r-none w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
ref={input}
value={value}
onKeyUp={e => setValue(e.target.value)}
autocomplete="off"
/>
<button
type="submit"
className="font-bold rounded rounded-l-none text-white px-4 hover:bg-indigo-700 bg-indigo-800 text-center no-underline block focus:shadow-outline focus:outline-none"
>
Add
</button>
</form>
<ContactPicker value={value} setValue={setValue} className="w-full" />
</div>
);
}
Example #9
Source File: ChallengeSolutions.js From duolingo-solution-viewer with MIT License | 4 votes |
ChallengeSolutions =
({
context = CONTEXT_CHALLENGE,
statement = '',
solutions = [],
matchingData = {},
userReference = '',
onUserReferenceUpdate = noop,
isUserReferenceEditable = true,
scrollOffsetGetter = (() => 0),
}) => {
const [ isLoading, setIsLoading ] = useState(false);
const [ currentSolutions, setCurrentSolutions ] = useState(solutions);
const [ currentUserReference, setCurrentUserReference ] = useState(userReference);
const [ isUserReferencePinned, setIsUserReferencedPinned ] = useLocalStorage('user_reference_pinned', false);
// Updates the user reference and waits for a new list of solutions.
const updateUserReference = useCallback(newReference => {
setIsLoading(true);
setCurrentUserReference(newReference);
Promise.resolve(onUserReferenceUpdate(newReference))
.then(solutions => {
if (isArray(solutions)) {
setCurrentSolutions(solutions)
} else {
setCurrentUserReference(currentUserReference);
}
}).catch(() => (
setCurrentUserReference(currentUserReference)
)).then(() => {
setIsLoading(false);
});
}, [
onUserReferenceUpdate,
setIsLoading,
setCurrentSolutions,
currentUserReference,
setCurrentUserReference,
]);
const listWrapper = useRef();
const referenceWrapper = useRef();
const fullScrollOffsetGetter = useCallback(() => (
10
+ scrollOffsetGetter()
+ (isUserReferencePinned && referenceWrapper.current?.offsetHeight || 0)
), [ scrollOffsetGetter, isUserReferencePinned, referenceWrapper ]);
// Scrolls to the top of the solution list whenever it changes.
const onSolutionListChange = useCallback(() => {
listWrapper.current
&& scrollElementIntoParentView(listWrapper.current, fullScrollOffsetGetter(), 'smooth');
}, [ // eslint-disable-line react-hooks/exhaustive-deps
listWrapper,
fullScrollOffsetGetter,
currentSolutions,
]);
const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ context ]);
if (0 === currentSolutions.length) {
return null;
}
return (
<IntlProvider scope="challenge">
{('' !== statement) && (
<Fragment>
<h3>
<Text id="statement">Statement:</Text>
</h3>
<p>{statement}</p>
</Fragment>
)}
<div
ref={referenceWrapper}
className={getElementClassNames([
REFERENCE_WRAPPER,
isUserReferencePinned && REFERENCE_WRAPPER__PINNED
])}
>
<UserReference
context={context}
reference={currentUserReference}
onUpdate={updateUserReference}
isEditable={isUserReferenceEditable && !isLoading}
/>
{(CONTEXT_CHALLENGE === context)
&& (
<IntlProvider scope="user_reference">
<Localizer>
<div
onClick={() => setIsUserReferencedPinned(!isUserReferencePinned)}
title={(
<Text id={isUserReferencePinned ? 'unpin' : 'pin'}>
{isUserReferencePinned ? 'Unpin' : 'Pin'}
</Text>
)}
className={getElementClassNames([
PIN_BUTTON,
isUserReferencePinned && PIN_BUTTON__PINNED
])}
>
<FontAwesomeIcon
icon={[ 'far', 'thumbtack' ]}
className={getElementClassNames(PIN_BUTTON_ICON)}
/>
</div>
</Localizer>
</IntlProvider>
)}
</div>
<div>
{isLoading
? (
<div className={getElementClassNames(LOADER)}>
<Loader />
</div>
) : (
<SolutionList
ref={listWrapper}
context={context}
solutions={currentSolutions}
matchingData={matchingData}
onPageChange={onSolutionListChange}
scrollOffsetGetter={fullScrollOffsetGetter}
/>
)}
</div>
</IntlProvider>
);
}
Example #10
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 #11
Source File: Modal.js From duolingo-solution-viewer with MIT License | 4 votes |
Modal =
({
children,
opened = true,
onAfterOpen = noop,
onAfterClose = noop,
onRequestClose = noop,
}) => {
const [ modalState, modalStateRef, setModalState ] = useStateRef(STATE_CLOSED);
const {
state: modalSize,
nextState: nextModalSize,
next: setNextModalSize,
} = useLocalStorageList(
'modal-size',
Object.keys(MODAL_SIZES),
MODAL_SIZE_DEFAULT
);
const contentWrapper = useRef();
const openedTimeout = useRef(null);
// Opens the modal with an effect similar to Duolingo's.
const openModal = useCallback(() => {
setModalState(STATE_WILL_OPEN);
setTimeout(() => setModalState(STATE_OPENING), 1);
openedTimeout.current = setTimeout(() => {
setModalState(STATE_OPENED);
setTimeout(() => onAfterOpen());
contentWrapper.current?.focus();
}, 300);
}, [ onAfterOpen, setModalState, openedTimeout ]);
// Closes the modal with an effect similar to Duolingo's.
const closeModal = useCallback(() => {
setModalState(STATE_CLOSING);
setTimeout(() => {
setModalState(STATE_CLOSED);
setTimeout(() => onAfterClose());
}, 300);
openedTimeout.current && clearTimeout(openedTimeout.current);
}, [ onAfterClose, setModalState, openedTimeout ]);
// Opens / closes the modal when requested.
useEffect(() => {
const isCurrentlyOpened = [ STATE_WILL_OPEN, STATE_OPENING, STATE_OPENED ].indexOf(modalState) >= 0;
if (opened && !isCurrentlyOpened) {
openModal();
} else if (!opened && isCurrentlyOpened) {
closeModal();
}
}, [ opened, modalState, openModal, closeModal ]);
// Closes the modal when the "Escape" key is pressed.
useEffect(() => {
const handleKeyDown = event => {
if (!isAnyInputFocused()) {
if ('Escape' === event.key) {
onRequestClose();
discardEvent(event);
} else if ('Enter' === event.key) {
if (modalStateRef.current === STATE_OPENED) {
discardEvent(event);
}
}
}
};
document.addEventListener('keydown', handleKeyDown, true);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [ onRequestClose ]);
const { modalSizeTitle } = useText({
modalSizeTitle: (
<Text id={`modal.${MODAL_SIZES[nextModalSize].actionTitleId}`}>
{MODAL_SIZES[nextModalSize].defaultActionTitle}
</Text>
)
});
const closeIconUrl = useImageCdnUrl(CLOSE_ICON_CDN_PATH);
const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ modalState, modalSize ]);
if (STATE_CLOSED === modalState) {
return null;
}
return (
<IntlProvider scope="modal">
<div onClick={onRequestClose} className={getElementClassNames(OVERLAY)}>
<div role="dialog" tabIndex="-1" onClick={discardEvent} className={getElementClassNames(WRAPPER)}>
<div onClick={onRequestClose} className={getElementClassNames(CLOSE_BUTTON)}>
<Localizer>
<img
src={closeIconUrl}
alt={<Text id="close">Close</Text>}
title={<Text id="close">Close</Text>}
/>
</Localizer>
</div>
<div title={modalSizeTitle} onClick={setNextModalSize} className={getElementClassNames(SIZE_BUTTON)}>
{MODAL_SIZES[nextModalSize].actionLabel}
</div>
<div ref={contentWrapper} tabIndex="0" className={getElementClassNames(CONTENT)}>
{children}
</div>
</div>
</div>
</IntlProvider>
);
}
Example #12
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 #13
Source File: UserReference.js From duolingo-solution-viewer with MIT License | 4 votes |
UserReference =
({
context = CONTEXT_CHALLENGE,
reference = '',
isEditable = true,
onUpdate = noop,
}) => {
const editInput = useRef();
const [ isEditing, setIsEditing ] = useState(false);
const commitEdit = useCallback(event => {
discardEvent(event);
if (editInput.current) {
const newReference = String(editInput.current.value || '').trim();
if (('' !== newReference) && (newReference !== reference)) {
onUpdate(newReference);
}
}
setIsEditing(false);
}, [ reference, onUpdate, setIsEditing ]);
const rollbackEdit = useCallback(event => {
discardEvent(event);
setIsEditing(false);
}, [ setIsEditing ]);
const onEditKeyDown = useCallback(event => {
if ('Enter' === event.key) {
commitEdit(event);
} else if ('Escape' === event.key) {
rollbackEdit(event);
}
}, [ commitEdit, rollbackEdit ]);
// Focuses the input when we just have switched to edit mode.
useEffect(() => {
if (editInput.current) {
setTimeout(() => {
if (document.activeElement !== editInput.current.focused) {
const length = editInput.current.value.length;
editInput.current.focus();
// Place the cursor at the end of the text.
editInput.current.setSelectionRange(length + 1, length + 1);
}
});
}
}, [ isEditing, editInput ]);
const [ Wrapper, Title, Value, EditWrapper ] = (CONTEXT_CHALLENGE === context)
? [ 'div', 'h3', 'p', 'p' ]
: [ 'h2', 'span', 'span', Fragment ];
const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ context ]);
let buttonInlineStyles = {};
let additionalButtonClass = null;
if (CONTEXT_FORUM === context) {
buttonInlineStyles = getForumNewPostButtonsInlineStyles();
if (null === buttonInlineStyles) {
const inlineStyles = getForumFollowButtonInlineStyles();
buttonInlineStyles = {
[COMMIT_BUTTON]: inlineStyles,
[ROLLBACK_BUTTON]: inlineStyles,
};
additionalButtonClass = FALLBACK_BUTTON;
}
}
const valueKeys = [
VALUE,
isEditable && EDITABLE_VALUE,
('' === reference) && EMPTY_VALUE,
].filter(Boolean);
return (
<IntlProvider scope="user_reference">
<Wrapper className={getElementClassNames(WRAPPER)}>
<Title className={getElementClassNames(TITLE)}>
<Text id="your_reference">Your reference:</Text>
</Title>
{!isEditing
? ( // Not editing.
<Value onClick={() => isEditable && setIsEditing(true)} className={getElementClassNames(valueKeys)}>
{('' !== reference) ? reference : <Text id="none">None yet</Text>}
</Value>
) : ( // Editing.
<EditWrapper>
<textarea
ref={editInput}
defaultValue={reference}
dir="auto"
onKeyDown={onEditKeyDown}
className={getElementClassNames(EDIT_FIELD)}
/>
<button
onClick={commitEdit}
style={buttonInlineStyles[COMMIT_BUTTON] || ''}
className={getElementClassNames([ BUTTON, COMMIT_BUTTON, additionalButtonClass ])}
>
<Text id="update">Update</Text>
</button>
<span className={getElementClassNames(BUTTON_SPACER)}>
<button
onClick={rollbackEdit}
style={buttonInlineStyles[ROLLBACK_BUTTON] || ''}
className={getElementClassNames([ BUTTON, ROLLBACK_BUTTON, additionalButtonClass ])}
>
<Text id="cancel">Cancel</Text>
</button>
</span>
</EditWrapper>
)}
</Wrapper>
</IntlProvider>
);
}
Example #14
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 #15
Source File: problem.js From rctf with BSD 3-Clause "New" or "Revised" License | 4 votes |
Problem = ({ classes, problem, solved, setSolved }) => {
const { toast } = useToast()
const hasDownloads = problem.files.length !== 0
const [error, setError] = useState(undefined)
const hasError = error !== undefined
const [value, setValue] = useState('')
const handleInputChange = useCallback(e => setValue(e.target.value), [])
const handleSubmit = useCallback(e => {
e.preventDefault()
submitFlag(problem.id, value.trim())
.then(({ error }) => {
if (error === undefined) {
toast({ body: 'Flag successfully submitted!' })
setSolved(problem.id)
} else {
toast({ body: error, type: 'error' })
setError(error)
}
})
}, [toast, setSolved, problem, value])
const [solves, setSolves] = useState(null)
const [solvesPending, setSolvesPending] = useState(false)
const [solvesPage, setSolvesPage] = useState(1)
const modalBodyRef = useRef(null)
const handleSetSolvesPage = useCallback(async (newPage) => {
const { kind, message, data } = await getSolves({
challId: problem.id,
limit: solvesPageSize,
offset: (newPage - 1) * solvesPageSize
})
if (kind !== 'goodChallengeSolves') {
toast({ body: message, type: 'error' })
return
}
setSolves(data.solves)
setSolvesPage(newPage)
modalBodyRef.current.scrollTop = 0
}, [problem.id, toast])
const onSolvesClick = useCallback(async (e) => {
e.preventDefault()
if (solvesPending) {
return
}
setSolvesPending(true)
const { kind, message, data } = await getSolves({
challId: problem.id,
limit: solvesPageSize,
offset: 0
})
setSolvesPending(false)
if (kind !== 'goodChallengeSolves') {
toast({ body: message, type: 'error' })
return
}
setSolves(data.solves)
setSolvesPage(1)
}, [problem.id, toast, solvesPending])
const onSolvesClose = useCallback(() => setSolves(null), [])
return (
<div class={`frame ${classes.frame}`}>
<div class='frame__body'>
<div class='row u-no-padding'>
<div class='col-6 u-no-padding'>
<div class='frame__title title'>{problem.category}/{problem.name}</div>
<div class='frame__subtitle u-no-margin'>{problem.author}</div>
</div>
<div class='col-6 u-no-padding u-text-right'>
<a
class={`${classes.points} ${solvesPending ? classes.solvesPending : ''}`}
onClick={onSolvesClick}>
{problem.solves}
{problem.solves === 1 ? ' solve / ' : ' solves / '}
{problem.points}
{problem.points === 1 ? ' point' : ' points'}
</a>
</div>
</div>
<div class='content-no-padding u-center'><div class={`divider ${classes.divider}`} /></div>
<div class={`${classes.description} frame__subtitle`}>
<Markdown content={problem.description} components={markdownComponents} />
</div>
<form class='form-section' onSubmit={handleSubmit}>
<div class='form-group'>
<input
autocomplete='off'
autocorrect='off'
class={`form-group-input input-small ${classes.input} ${hasError ? 'input-error' : ''} ${solved ? 'input-success' : ''}`}
placeholder={`Flag${solved ? ' (solved)' : ''}`}
value={value}
onChange={handleInputChange}
/>
<button class={`form-group-btn btn-small ${classes.submit}`}>Submit</button>
</div>
</form>
{
hasDownloads &&
<div>
<p class='frame__subtitle u-no-margin'>Downloads</p>
<div class='tag-container'>
{
problem.files.map(file => {
return (
<div class={`tag ${classes.tag}`} key={file.url}>
<a native download href={`${file.url}`}>
{file.name}
</a>
</div>
)
})
}
</div>
</div>
}
</div>
<SolvesDialog
solves={solves}
challName={problem.name}
solveCount={problem.solves}
pageSize={solvesPageSize}
page={solvesPage}
setPage={handleSetSolvesPage}
onClose={onSolvesClose}
modalBodyRef={modalBodyRef}
/>
</div>
)
}
Example #16
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>
)
})