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 vote down vote up
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}>&hellip;</a>
    </div>
  )
})
Example #2
Source File: invalid.js    From eslint-config-preact with MIT License 7 votes vote down vote up
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 vote down vote up
/**
 * @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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 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 #16
Source File: profile.js    From rctf with BSD 3-Clause "New" or "Revised" License 5 votes vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 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 #24
Source File: profile.js    From rctf with BSD 3-Clause "New" or "Revised" License 4 votes vote down vote up
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 vote down vote up
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 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>
  )
})
Example #27
Source File: solves-dialog.js    From rctf with BSD 3-Clause "New" or "Revised" License 4 votes vote down vote up
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 vote down vote up
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 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>
  )
}