preact/hooks#useCallback JavaScript Examples
The following examples show how to use
preact/hooks#useCallback.
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: pagination.js From rctf with BSD 3-Clause "New" or "Revised" License | 7 votes |
PaginationEllipses = withStyles({
noHover: {
backgroundColor: 'transparent !important'
},
ellipses: {
paddingLeft: '0.1em !important',
paddingRight: '0.1em !important'
}
}, ({ classes }) => {
const noFocus = useCallback(e => e.target.blur(), [])
// TODO: pass props through
return (
<div class={`pagination-item short ${classes.noHover}`}>
<a class={`ellipses ${classes.ellipses}`} tabindex='-1' onFocus={noFocus}>…</a>
</div>
)
})
Example #2
Source File: invalid.js From eslint-config-preact with MIT License | 7 votes |
export function Foo () {
const [value, setValue] = useState(0);
const increment = useCallback(() => setValue(value + 1));
useEffect(() => {
console.log(value);
}, []);
return <button onClick={increment}>{value}</button>;
}
Example #3
Source File: useHashLocation.js From v8-deopt-viewer with MIT License | 6 votes |
/**
* @returns {[string, (to: string) => string]}
*/
export function useHashLocation() {
const [loc, setLoc] = useState(currentLocation());
useEffect(() => {
// this function is called whenever the hash changes
const handler = () => setLoc(currentLocation());
// subscribe to hash changes
window.addEventListener("hashchange", handler);
return () => window.removeEventListener("hashchange", handler);
}, []);
// remember to wrap your function with `useCallback` hook
// a tiny but important optimization
const navigate = useCallback((to) => (window.location.hash = to), []);
return [loc, navigate];
}
Example #4
Source File: toast.js From rctf with BSD 3-Clause "New" or "Revised" License | 6 votes |
function Toast ({ children, remove, type, id }) {
const wrappedRemove = useCallback(() => remove(id), [remove, id])
useEffect(() => {
const duration = 1000 * 5
const timeout = setTimeout(wrappedRemove, duration)
return () => clearTimeout(timeout)
}, [wrappedRemove])
return (
<div className={`toast toast--${type}`}>
{children}
<button onClick={wrappedRemove} className='btn-close' />
</div>
)
}
Example #5
Source File: toast.js From rctf with BSD 3-Clause "New" or "Revised" License | 6 votes |
export function ToastProvider ({ children }) {
const [toasts, setToasts] = useState([])
const remove = useCallback((id) => {
setToasts(toasts => toasts.filter(t => id !== t.id))
}, [])
const add = useCallback((data) => {
const id = genToastId()
const toast = {
...data,
id
}
setToasts(toasts => [...toasts, toast])
return () => remove(id)
}, [remove])
return (
<ToastCtx.Provider value={add}>
{children}
{ toasts.length > 0 &&
<ToastContainer>
{ toasts.map(({ id, type, body }) =>
<Toast type={type} key={id} id={id} remove={remove}>
{body}
</Toast>
)}
</ToastContainer>
}
</ToastCtx.Provider>
)
}
Example #6
Source File: members-card.js From rctf with BSD 3-Clause "New" or "Revised" License | 6 votes |
MemberRow = withStyles({
root: {
alignItems: 'center',
width: '100%',
display: 'flex',
justifyContent: 'space-between'
}
}, ({ classes, id, email, setMembers }) => {
const { toast } = useToast()
const handleDelete = useCallback(() => {
removeMember({ id })
.then(() => {
setMembers(members => members.filter(a => a.id !== id))
toast({ body: 'Team member successfully deleted' })
})
}, [id, setMembers, toast])
return (
<div class={classes.root} key={id}>
<p class='u-no-margin'>{email}</p>
<div class='btn-container u-vertical-center'>
<input onClick={handleDelete} type='submit' class='btn-small btn-danger u-no-margin' value='Delete' />
</div>
</div>
)
})
Example #7
Source File: pending-token.js From rctf with BSD 3-Clause "New" or "Revised" License | 6 votes |
PendingToken = ({ authToken }) => {
const [user, setUser] = useState(null)
useEffect(() => {
(async () => {
if (!authToken) {
return
}
const user = await pendingPrivateProfile({ authToken })
setUser(user)
})()
}, [authToken])
const handleLoginClick = useCallback(() => {
setAuthToken({ authToken })
}, [authToken])
if (!user) {
return null
}
return (
<Fragment>
<div class='row u-center'>
<h3>Login as {user.name}?</h3>
</div>
<div class='row u-center'>
<button class='btn-info' onClick={handleLoginClick}>Login</button>
</div>
</Fragment>
)
}
Example #8
Source File: pagination.js From rctf with BSD 3-Clause "New" or "Revised" License | 6 votes |
function PaginationItem ({ onClick, disabled, selected, children, tabindex, ...props }) {
const className = props.class || ''
delete props.class
const wrappedOnClick = useCallback((e) => {
e.preventDefault()
onClick()
}, [onClick])
return (
<div class={`pagination-item short ${className}${selected ? ' selected' : ''}`} {...props}>
<a disabled={disabled} href={onClick && '#'} onClick={wrappedOnClick} tabindex={tabindex}>
{children}
</a>
</div>
)
}
Example #9
Source File: logout-button.js From rctf with BSD 3-Clause "New" or "Revised" License | 6 votes |
LogoutDialog = withStyles({
button: {
fontFamily: 'inherit'
}
}, ({ onClose, classes, ...props }) => {
const wrappedOnClose = useCallback(e => {
e.preventDefault()
onClose()
}, [onClose])
const doLogout = useCallback(e => {
e.preventDefault()
logout()
onClose()
}, [onClose])
return (
<Modal {...props} onClose={onClose}>
<div class='modal-header'>
<div class='modal-title'>Logout</div>
</div>
<div class='modal-body'>
<div>This will log you out on your current device.</div>
</div>
<div class='modal-footer'>
<div class='btn-container u-inline-block'>
<button class={`btn-small outline ${classes.button}`} onClick={wrappedOnClose}>Cancel</button>
</div>
<div class='btn-container u-inline-block'>
<button class={`btn-small btn-danger outline ${classes.button}`} onClick={doLogout}>Logout</button>
</div>
</div>
</Modal>
)
})
Example #10
Source File: logout-button.js From rctf with BSD 3-Clause "New" or "Revised" License | 6 votes |
function LogoutButton ({ ...props }) {
const [isDialogVisible, setIsDialogVisible] = useState(false)
const onClick = useCallback(e => {
e.preventDefault()
setIsDialogVisible(true)
}, [])
const onClose = useCallback(() => setIsDialogVisible(false), [])
return (
<Fragment>
<a {...props} href='#' native onClick={onClick}>
Logout
</a>
<LogoutDialog open={isDialogVisible} onClose={onClose} />
</Fragment>
)
}
Example #11
Source File: valid.js From eslint-config-preact with MIT License | 6 votes |
export function Foo () {
const [value, setValue] = useState(0);
const increment = useCallback(() => setValue(v => v + 1), [setValue]);
useEffect(() => {
console.log(value);
}, [value]);
return <button onClick={increment}>{value}</button>;
}
Example #12
Source File: index.js From duolingo-solution-viewer with MIT License | 6 votes |
useStyles = (classNames, styleSheets = {}, stateKeys = []) => {
return useCallback(elementKeys => {
const keys = isArray(elementKeys) ? elementKeys : [ elementKeys ];
return keys.flatMap(elementKey => {
const allClassNames = [];
if (styleSheets[BASE]?.[elementKey]) {
allClassNames.push(css(styleSheets[BASE][elementKey]));
}
if (classNames[BASE]?.[elementKey]) {
allClassNames.push(...classNames[BASE][elementKey]);
}
stateKeys.forEach(stateKey => {
if (stateKey) {
if (styleSheets[stateKey]?.[elementKey]) {
allClassNames.push(css(styleSheets[stateKey][elementKey]));
}
if (classNames[stateKey]?.[elementKey]) {
allClassNames.push(...classNames[stateKey][elementKey]);
}
}
});
return allClassNames;
}).join(' ')
}, stateKeys.concat([ classNames, styleSheets ])); // eslint-disable-line react-hooks/exhaustive-deps
}
Example #13
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 #14
Source File: problem.js From rctf with BSD 3-Clause "New" or "Revised" License | 5 votes |
DeleteModal = withStyles({
modalBody: {
paddingTop: '0em !important' // reduce space between header and body
},
controls: {
display: 'flex',
justifyContent: 'center',
'& :first-child': {
marginLeft: '0em'
},
'& :last-child': {
marginRight: '0em'
}
}
}, ({ open, onClose, onDelete, classes }) => {
const wrappedOnClose = useCallback((e) => {
e.preventDefault()
onClose()
}, [onClose])
const wrappedOnDelete = useCallback((e) => {
e.preventDefault()
onDelete()
}, [onDelete])
return (
<Modal open={open} onClose={onClose}>
<div class='modal-header'>
<div class='modal-title'>Delete Challenge?</div>
</div>
<div class={`modal-body ${classes.modalBody}`}>
This is an irreversible action that permanently deletes the challenge and revokes all solves.
<div class={classes.controls}>
<div class='btn-container u-inline-block'>
<button type='button' class='btn-small' onClick={wrappedOnClose}>Cancel</button>
</div>
<div class='btn-container u-inline-block'>
<button type='submit' class='btn-small btn-danger' onClick={wrappedOnDelete}>Delete Challenge</button>
</div>
</div>
</div>
</Modal>
)
})
Example #15
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 #16
Source File: profile.js From rctf with BSD 3-Clause "New" or "Revised" License | 5 votes |
TeamCodeCard = withStyles({
btn: {
marginRight: '10px'
}
}, ({ teamToken, classes }) => {
const { toast } = useToast()
const tokenUrl = `${location.origin}/login?token=${encodeURIComponent(teamToken)}`
const [reveal, setReveal] = useState(false)
const toggleReveal = useCallback(
() => setReveal(!reveal),
[reveal]
)
const onCopyClick = useCallback(() => {
if (navigator.clipboard) {
try {
navigator.clipboard.writeText(tokenUrl).then(() => {
toast({ body: 'Copied team invite URL to clipboard' })
})
} catch {}
}
}, [toast, tokenUrl])
return (
<div class='card'>
<div class='content'>
<p>Team Invite</p>
<p class='font-thin'>Send this team invite URL to your teammates so they can login.</p>
<button onClick={onCopyClick} class={`${classes.btn} btn-info u-center`} name='btn' value='submit' type='submit'>Copy</button>
<button onClick={toggleReveal} class='btn-info u-center' name='btn' value='submit' type='submit'>{reveal ? 'Hide' : 'Reveal'}</button>
{
reveal &&
<TokenPreview token={tokenUrl} />
}
</div>
</div>
)
})
Example #17
Source File: SolutionList.js From duolingo-solution-viewer with MIT License | 5 votes |
ListPagination = ({ context, solutionCount, page, pageSize, onPageChange, onPageSizeChange, }) => { const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ context ]); const getSizeLabel = size => (PAGE_SIZE_ALL !== size) ? `${size}` : <Text id="all">all</Text>; const renderSizeLink = useCallback(size => ( isEqualPageSizes(size, pageSize) ? ( // Same page size <span className={getElementClassNames(CURRENT_PAGE_SIZE)}> {getSizeLabel(size)} </span> ) : ( // Different page size <a onClick={() => onPageSizeChange(size)} className={getElementClassNames(PAGE_SIZE_LINK)}> {getSizeLabel(size)} </a> ) ), [ pageSize, onPageSizeChange, getElementClassNames ]); const renderSizeOption = useCallback(size => ( <option value={size} selected={isEqualPageSizes(size, pageSize)} className={getElementClassNames(PAGE_SIZE_OPTION)} > {getSizeLabel(size)} </option> ), [ pageSize, getElementClassNames ]); const [ firstIndex, lastIndex ] = (PAGE_SIZE_ALL === pageSize) ? [ 1, solutionCount ] : [ (page - 1) * pageSize + 1, Math.min(solutionCount, page * pageSize) ]; return ( <div className={getElementClassNames(PAGINATION_WRAPPER)}> {(PAGE_SIZE_ALL !== pageSize) && ( <Pagination activePage={page} itemCountPerPage={pageSize} totalItemCount={solutionCount} onPageChange={onPageChange} context={context} /> )} <div className={getElementClassNames(PAGINATION_FOOTER)}> <div className={getElementClassNames(PAGINATION_STATE)}> {firstIndex} - {lastIndex} / {solutionCount} </div> <div className={getElementClassNames(PAGINATION_SIZE_WRAPPER)}> <Text id="per_page">per page:</Text> {PAGE_SIZES.map(renderSizeLink)} <div className={getElementClassNames(PAGE_SIZE_SELECT_WRAPPER)}> <select onChange={event => onPageSizeChange(event.target.value)} className={getElementClassNames(PAGE_SIZE_SELECT)} > {PAGE_SIZES.map(renderSizeOption)} </select> </div> </div> </div> </div> ) }
Example #18
Source File: index.js From rctf with BSD 3-Clause "New" or "Revised" License | 5 votes |
function useTriggerRerender () {
const setToggle = useState(false)[1]
return useCallback(() => setToggle(t => !t), [setToggle])
}
Example #19
Source File: ctftime-card.js From rctf with BSD 3-Clause "New" or "Revised" License | 5 votes |
CtftimeCard = withStyles({
ctftimeButton: {
'& button': {
borderColor: '#d9d9d9 !important'
}
}
}, ({ classes, ctftimeId, onUpdate }) => {
const { toast } = useToast()
const handleCtftimeDone = useCallback(async ({ ctftimeToken, ctftimeId }) => {
const { kind, message } = await putCtftime({ ctftimeToken })
if (kind !== 'goodCtftimeAuthSet') {
toast({ body: message, type: 'error' })
return
}
onUpdate({ ctftimeId })
}, [toast, onUpdate])
const handleRemoveClick = useCallback(async () => {
const { kind, message } = await deleteCtftime()
if (kind !== 'goodCtftimeRemoved') {
toast({ body: message, type: 'error' })
return
}
onUpdate({ ctftimeId: null })
}, [toast, onUpdate])
return (
<div class='card'>
<div class='content'>
<p>CTFtime Integration</p>
{ctftimeId === null ? (
<Fragment>
<p class='font-thin u-no-margin'>To login with CTFtime and get a badge on your profile, connect CTFtime to your account.</p>
<div class='row u-center'>
<CtftimeButton class={classes.ctftimeButton} onCtftimeDone={handleCtftimeDone} />
</div>
</Fragment>
) : (
<Fragment>
<p class='font-thin u-no-margin'>Your account is already connected to CTFtime. You can disconnect CTFtime from your account.</p>
<div class='row u-center'>
<button class='btn-info u-center' onClick={handleRemoveClick}>Remove</button>
</div>
</Fragment>
)}
</div>
</div>
)
})
Example #20
Source File: recover.js From rctf with BSD 3-Clause "New" or "Revised" License | 5 votes |
Recover = ({ classes }) => {
const [disabled, setDisabled] = useState(false)
const [errors, setErrors] = useState({})
const [verifySent, setVerifySent] = useState(false)
const [email, setEmail] = useState('')
const requestRecaptchaCode = useRecaptcha('recover')
const handleSubmit = useCallback(async (evt) => {
evt.preventDefault()
const recaptchaCode = await requestRecaptchaCode?.()
setDisabled(true)
const { errors, verifySent } = await recover({
email,
recaptchaCode
})
setErrors(errors)
setVerifySent(verifySent)
setDisabled(false)
}, [setDisabled, email, setErrors, setVerifySent, requestRecaptchaCode])
const handleEmailChange = useCallback(evt => setEmail(evt.target.value), [setEmail])
if (verifySent) {
return (
<div class='row u-center'>
<h3>Recovery email sent</h3>
</div>
)
}
return (
<div class={`row u-center ${classes.root}`}>
<h4 class={classes.title}>Recover your {config.ctfName} account</h4>
<Form class={`${classes.form} col-6`} onSubmit={handleSubmit} disabled={disabled} errors={errors} buttonText='Recover'>
<input
class={classes.input}
autofocus
required
autocomplete='email'
autocorrect='off'
icon={<EnvelopeOpen />}
name='email'
placeholder='Email'
type='email'
value={email}
onChange={handleEmailChange}
/>
</Form>
{requestRecaptchaCode && (
<div class={classes.recaptchaLegalNotice}>
<RecaptchaLegalNotice />
</div>
)}
</div>
)
}
Example #21
Source File: members-card.js From rctf with BSD 3-Clause "New" or "Revised" License | 5 votes |
MembersCard = withStyles({
form: {
'& button': {
display: 'block',
marginLeft: 'auto',
marginRight: '0',
marginTop: '10px'
}
}
}, ({ classes }) => {
const { toast } = useToast()
const [email, setEmail] = useState('')
const handleEmailChange = useCallback(e => setEmail(e.target.value), [])
const [buttonDisabled, setButtonDisabled] = useState(false)
const [members, setMembers] = useState([])
const handleSubmit = useCallback(e => {
e.preventDefault()
setButtonDisabled(true)
addMember({ email })
.then(({ error, data }) => {
setButtonDisabled(false)
if (error) {
toast({ body: error, type: 'error' })
} else {
toast({ body: 'Team member successfully added' })
setMembers(members => [...members, data])
setEmail('')
}
})
}, [email, toast])
useEffect(() => {
getMembers()
.then(data => setMembers(data))
}, [])
return (
<div class='card'>
<div class='content'>
<p>Team Information</p>
<p class='font-thin u-no-margin'>Please enter a separate email for each team member. This data is collected for informational purposes only. Ensure that this section is up to date in order to remain prize eligible.</p>
<div class='row u-center'>
<Form class={`col-12 ${classes.form}`} onSubmit={handleSubmit} disabled={buttonDisabled} buttonText='Add Member'>
<input
required
autocomplete='email'
autocorrect='off'
icon={<EnvelopeOpen />}
name='email'
placeholder='Email'
type='email'
value={email}
onChange={handleEmailChange}
/>
</Form>
{
members.length !== 0 &&
<div class='row'>
{
members.map(data => <MemberRow setMembers={setMembers} { ...data } />)
}
</div>
}
</div>
</div>
</div>
)
})
Example #22
Source File: CorrectedAnswer.js From duolingo-solution-viewer with MIT License | 5 votes |
CorrectedAnswer = ({ diffTokens = [], result = RESULT_CORRECT }) => {
const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ result ]);
// Renders a diff token.
const renderToken = useCallback((token, displayMode) => {
let elementKey = null;
if (token.added) {
if (DISPLAY_MODE_CORRECTED === displayMode) {
return null;
} else if (!token.ignorable) {
elementKey = ADDED_TOKEN;
}
} else if (token.removed) {
if (DISPLAY_MODE_ORIGINAL === displayMode) {
return null;
} else if (!token.ignorable) {
elementKey = REMOVED_TOKEN;
}
}
return (
<span className={getElementClassNames(elementKey)}>
{token.value}
</span>
);
}, [ getElementClassNames ]);
const [ originalAnswer, setOriginalAnswer ] = useState([]);
const [ correctedAnswer, setCorrectedAnswer ] = useState([]);
// Refreshes both versions of the answer when the diff tokens change.
useEffect(() => {
setOriginalAnswer(diffTokens.map(renderToken(_, DISPLAY_MODE_ORIGINAL)));
setCorrectedAnswer(diffTokens.map(renderToken(_, DISPLAY_MODE_CORRECTED)));
}, [ diffTokens, renderToken ]);
if (0 === diffTokens.length) {
return null;
}
return (
<IntlProvider scope="corrected_answer">
<h2 className={getElementClassNames(WRAPPER)}>
<Text id="title">Corrected answer:</Text>
<div className={getElementClassNames(VALUE)}>
{originalAnswer}
</div>
<div className={getElementClassNames(VALUE)}>
{correctedAnswer}
</div>
</h2>
</IntlProvider>
);
}
Example #23
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 #24
Source File: profile.js From rctf with BSD 3-Clause "New" or "Revised" License | 4 votes |
UpdateCard = withStyles({
form: {
'& button': {
margin: 0,
marginBottom: '0.4em',
float: 'right'
},
padding: '0 !important'
},
divisionSelect: {
paddingLeft: '2.75rem'
},
recaptchaLegalNotice: {
marginTop: '20px'
}
}, ({ name: oldName, email: oldEmail, divisionId: oldDivision, allowedDivisions, onUpdate, classes }) => {
const { toast } = useToast()
const requestRecaptchaCode = useRecaptcha('setEmail')
const [name, setName] = useState(oldName)
const handleSetName = useCallback((e) => setName(e.target.value), [])
const [email, setEmail] = useState(oldEmail)
const handleSetEmail = useCallback(e => setEmail(e.target.value), [])
const [division, setDivision] = useState(oldDivision)
const handleSetDivision = useCallback(e => setDivision(e.target.value), [])
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const doUpdate = useCallback(async (e) => {
e.preventDefault()
let updated = false
if (name !== oldName || division !== oldDivision) {
updated = true
setIsButtonDisabled(true)
const { error, data } = await updateAccount({
name: oldName === name ? undefined : name,
division: oldDivision === division ? undefined : division
})
setIsButtonDisabled(false)
if (error !== undefined) {
toast({ body: error, type: 'error' })
return
}
toast({ body: 'Profile updated' })
onUpdate({
name: data.user.name,
divisionId: data.user.division
})
}
if (email !== oldEmail) {
updated = true
let error, data
if (email === '') {
setIsButtonDisabled(true)
;({ error, data } = await deleteEmail())
} else {
const recaptchaCode = await requestRecaptchaCode?.()
setIsButtonDisabled(true)
;({ error, data } = await updateEmail({
email,
recaptchaCode
}))
}
setIsButtonDisabled(false)
if (error !== undefined) {
toast({ body: error, type: 'error' })
return
}
toast({ body: data })
onUpdate({ email })
}
if (!updated) {
toast({ body: 'Nothing to update!' })
}
}, [name, email, division, oldName, oldEmail, oldDivision, onUpdate, toast, requestRecaptchaCode])
return (
<div class='card'>
<div class='content'>
<p>Update Information</p>
<p class='font-thin u-no-margin'>This will change how your team appears on the scoreboard. You may only change your team's name once every 10 minutes.</p>
<div class='row u-center'>
<Form class={`col-12 ${classes.form}`} onSubmit={doUpdate} disabled={isButtonDisabled} buttonText='Update'>
<input
required
autocomplete='username'
autocorrect='off'
maxLength='64'
minLength='2'
icon={<UserCircle />}
name='name'
placeholder='Team Name'
type='text'
value={name}
onChange={handleSetName}
/>
<input
autocomplete='email'
autocorrect='off'
icon={<EnvelopeOpen />}
name='email'
placeholder='Email'
type='email'
value={email}
onChange={handleSetEmail}
/>
<select icon={<AddressBook />} class={`select ${classes.divisionSelect}`} name='division' value={division} onChange={handleSetDivision}>
<option value='' disabled>Division</option>
{
allowedDivisions.map(code => {
return <option key={code} value={code}>{config.divisions[code]}</option>
})
}
</select>
</Form>
{requestRecaptchaCode && (
<div class={classes.recaptchaLegalNotice}>
<RecaptchaLegalNotice />
</div>
)}
</div>
</div>
</div>
)
})
Example #25
Source File: profile.js From rctf with BSD 3-Clause "New" or "Revised" License | 4 votes |
Profile = ({ uuid, classes }) => {
const [loaded, setLoaded] = useState(false)
const [error, setError] = useState(null)
const [data, setData] = useState({})
const { toast } = useToast()
const {
name,
email,
division: divisionId,
score,
solves,
teamToken,
ctftimeId,
allowedDivisions
} = data
const division = config.divisions[data.division]
const divisionPlace = util.strings.placementString(data.divisionPlace)
const globalPlace = util.strings.placementString(data.globalPlace)
const isPrivate = uuid === undefined || uuid === 'me'
useEffect(() => {
setLoaded(false)
if (isPrivate) {
privateProfile()
.then(({ data, error }) => {
if (error) {
toast({ body: error, type: 'error' })
} else {
setData(data)
}
setLoaded(true)
})
} else {
publicProfile(uuid)
.then(({ data, error }) => {
if (error) {
setError('Profile not found')
} else {
setData(data)
}
setLoaded(true)
})
}
}, [uuid, isPrivate, toast])
const onProfileUpdate = useCallback(({ name, email, divisionId, ctftimeId }) => {
setData(data => ({
...data,
name: name === undefined ? data.name : name,
email: email === undefined ? data.email : email,
division: divisionId === undefined ? data.division : divisionId,
ctftimeId: ctftimeId === undefined ? data.ctftimeId : ctftimeId
}))
}, [])
useEffect(() => { document.title = `Profile | ${config.ctfName}` }, [])
if (!loaded) return null
if (error !== null) {
return (
<div class='row u-center'>
<div class='col-4'>
<div class={`card ${classes.errorCard}`}>
<div class='content'>
<p class='title'>There was an error</p>
<p class='font-thin'>{error}</p>
</div>
</div>
</div>
</div>
)
}
return (
<div class={classes.root}>
{isPrivate && (
<div class={classes.privateCol}>
<TeamCodeCard {...{ teamToken }} />
<UpdateCard {...{ name, email, divisionId, allowedDivisions, onUpdate: onProfileUpdate }} />
{config.ctftime && (
<CtftimeCard {...{ ctftimeId, onUpdate: onProfileUpdate }} />
)}
</div>
)}
<div class={classes.col}>
<SummaryCard {...{ name, score, division, divisionPlace, globalPlace, ctftimeId, isPrivate }} />
{isPrivate && config.userMembers && (
<MembersCard />
)}
{isPrivate ? (
<PrivateSolvesCard solves={solves} />
) : (
<PublicSolvesCard solves={solves} />
)}
</div>
</div>
)
}
Example #26
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>
)
})
Example #27
Source File: solves-dialog.js From rctf with BSD 3-Clause "New" or "Revised" License | 4 votes |
SolvesDialog = withStyles({
button: {
fontFamily: 'inherit'
},
table: {
display: 'grid',
gridTemplateColumns: 'repeat(3, max-content)',
'& div': {
margin: 'auto',
padding: '5px 10px',
textAlign: 'center',
whiteSpace: 'nowrap'
}
},
label: {
borderBottom: '1px solid #fff',
width: '100%',
textAlign: 'center'
},
name: {
overflow: 'hidden',
width: '300px'
},
inlineLabel: {
display: 'none'
},
icon: {
width: '60px',
margin: 'auto'
},
empty: {
'& h5': {
color: '#fff !important'
},
padding: '0 3rem',
paddingTop: '3rem'
},
modalBody: {
maxHeight: '60vh !important'
},
'@media (max-width: 768px)': {
inlineLabel: {
display: 'initial',
borderRight: '1px solid #fff'
},
table: {
gridTemplateColumns: 'repeat(2, minmax(max-content, 1fr))',
'& div': {
margin: '0'
}
},
label: {
display: 'none'
},
number: {
borderTop: '1px solid #fff'
},
name: {
width: 'initial',
maxWidth: '300px'
}
}
}, ({
onClose,
classes,
challName,
solveCount,
solves,
page,
setPage,
pageSize,
modalBodyRef,
...props
}) => {
const wrappedOnClose = useCallback(e => {
e.preventDefault()
onClose()
}, [onClose])
return (
<Modal {...props} open={solves !== null} onClose={onClose}>
{solves !== null && (
<Fragment>
{solves.length === 0 ? (
<div class={classes.empty}>
<div class={classes.icon}>
<Clock />
</div>
<h5>{challName} has no solves.</h5>
</div>
) : (
<Fragment>
<div class='modal-header'>
<div class='modal-title'>Solves for {challName}</div>
</div>
<div class={`modal-body ${classes.modalBody}`} ref={modalBodyRef}>
<div class={classes.table}>
<div class={classes.label}>#</div>
<div class={classes.label}>Team</div>
<div class={classes.label}>Solve time</div>
{solves.map((solve, i) => (
<Fragment>
<div class={`${classes.inlineLabel} ${classes.number}`}>#</div>
<div class={classes.number}>{(page - 1) * pageSize + i + 1}</div>
<div class={classes.inlineLabel}>Team</div>
<div class={classes.name}>
<a href={`/profile/${solve.userId}`}>{solve.userName}</a>
</div>
<div class={classes.inlineLabel}>Solve time</div>
<div>{formatRelativeTime(solve.createdAt)}</div>
</Fragment>
))}
</div>
<Pagination
{...{ totalItems: solveCount, pageSize, page, setPage }}
numVisiblePages={9}
/>
</div>
</Fragment>
)}
<div class='modal-footer'>
<div class='btn-container u-inline-block'>
<button class={`btn-small outline ${classes.button}`} onClick={wrappedOnClose}>Close</button>
</div>
</div>
</Fragment>
)}
</Modal>
)
})
Example #28
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 #29
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>
)
}