preact#Fragment JavaScript Examples

The following examples show how to use preact#Fragment. 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: dialog_payments.js    From domicilioBoilerplate with GNU Affero General Public License v3.0 6 votes vote down vote up
D_Payments = ({payments}) => {
	return (
		<Fragment>
			<h3 class="text-lg font-bold mb-2 text-gray-700">Metodi di pagamento:</h3>
			<div class="mb-5">
				{payments.map((payment) => {
					return (
						<span
							class="inline-block py-1 px-3 mb-2 mr-2 text-sm rounded-full bg-gray-500 font-semibold text-gray-700"
							target="_blank"
							rel="noopener noreferrer"
						>
							{payment}
						</span>
					);
				})}
			</div>
		</Fragment>
	);
}
Example #2
Source File: graph.js    From rctf with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
GraphLine = memo(({ points, onTooltipIn, onMouseMove, onMouseOut, name, currentScore, ...rest }) => (
  <Fragment>
    <polyline
      {...rest}
      points={points}
      stroke-width={stroke}
      stroke-linecap='round'
      fill='transparent'
      pointer-events='none'
    />
    <polyline
      stroke-width={strokeHoverWidth}
      points={points}
      stroke-linecap='round'
      fill='transparent'
      pointer-events='stroke'
      onMouseOver={onTooltipIn(`${name} - ${currentScore} points`)}
      onMouseMove={onMouseMove}
      onMouseOut={onMouseOut}
    />
  </Fragment>
))
Example #3
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 #4
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 #5
Source File: dialog_notes.js    From domicilioBoilerplate with GNU Affero General Public License v3.0 6 votes vote down vote up
D_Notes = ({note}) => {
	return (
		<Fragment>
			<h3 class="text-lg font-bold mb-2 text-gray-700">Note:</h3>
			<div class="mb-5 text-gray-700">
            <p>{note}</p>
			</div>
		</Fragment>
	);
}
Example #6
Source File: DeoptTables.js    From v8-deopt-viewer with MIT License 6 votes vote down vote up
/**
 * @typedef EntryTitleProps
 * @property {import('v8-deopt-parser').Entry} entry
 * @property {string} relativePath
 * @property {import('../../').Route<[number, string]>} route
 * @property {number} fileId
 * @param {EntryTitleProps} props
 */
function EntryTitle({ entry, relativePath, route, fileId }) {
	const href = route.getHref(fileId, entry.id);
	const linkText = `${entry.functionName} at ${relativePath}:${entry.line}:${entry.column}`;

	return (
		<Fragment>
			<span class={entryIdClass}>{entry.id} </span>
			<a href={href} class={entryLink}>
				{linkText}
			</a>
		</Fragment>
	);
}
Example #7
Source File: dialog_services.js    From domicilioBoilerplate with GNU Affero General Public License v3.0 6 votes vote down vote up
D_Services = ({services}) => {
	
	return (
		<Fragment>
			<h3 class="text-lg font-bold mb-2 text-gray-700">Puoi trovarci anche su:</h3>
			<div>
				{services.map((service) => {
					const colors = getBrandColor(service);

					return (
						<span
							class="inline-block py-1 px-3 mb-2 mr-2 text-sm rounded-full font-semibold text-gray-700"
							target="_blank"
							rel="noopener noreferrer"
							style={colors}
						>
							{service}
						</span>
					);
				})}
			</div>
		</Fragment>
	);
}
Example #8
Source File: SummaryTable.js    From v8-deopt-viewer with MIT License 6 votes vote down vote up
export function SeverityTableSummary(props) {
	return (
		<Fragment>
			{props.severities.map((severityCount, i) => {
				return (
					<td
						class={[
							props.class,
							severityCount > 0 ? severityClass(i + 1) : null,
						].join(" ")}
					>
						{severityCount}
					</td>
				);
			})}
		</Fragment>
	);
}
Example #9
Source File: SummaryTable.js    From v8-deopt-viewer with MIT License 6 votes vote down vote up
export function SeverityTableHeaders(props) {
	return (
		<Fragment>
			<th class={props.class}>Sev 1</th>
			<th class={props.class}>Sev 2</th>
			<th class={props.class}>Sev 3</th>
		</Fragment>
	);
}
Example #10
Source File: SummaryTable.js    From v8-deopt-viewer with MIT License 6 votes vote down vote up
export function CodeTableHeaders(props) {
	return (
		<Fragment>
			<th class={props.class}>Optimized</th>
			<th class={props.class}>Optimizable</th>
			<th class={props.class}>Sev 3</th>
		</Fragment>
	);
}
Example #11
Source File: App.js    From v8-deopt-viewer with MIT License 6 votes vote down vote up
/**
 * @param {import('..').AppProps} props
 */
export function App({ deoptInfo }) {
	const files = Object.keys(deoptInfo.files);

	return (
		<Fragment>
			<Router hook={useHashLocation}>
				<Header />
				<Route path={summaryRoute.route}>
					<Summary deoptInfo={deoptInfo} />
				</Route>
				<Route path={fileRoute.route}>
					{(params) => (
						<FileViewer
							routeParams={{
								fileId: parseInt(params.fileId) || 0,
								tabId: params.tabId,
							}}
							files={files}
							deoptInfo={deoptInfo}
						/>
					)}
				</Route>
			</Router>
		</Fragment>
	);
}
Example #12
Source File: Summary.js    From v8-deopt-viewer with MIT License 6 votes vote down vote up
/**
 * @typedef {{ deoptInfo: import('..').PerFileDeoptInfoWithSources; perFileStats: PerFileStats }} SummaryProps
 * @param {import('..').AppProps} props
 */
export function Summary({ deoptInfo }) {
	const perFileStats = useMemo(() => getPerFileStats(deoptInfo), [deoptInfo]);

	return (
		<Fragment>
			{/* <SummaryList deoptInfo={deoptInfo} perFileStats={perFileStats} /> */}
			<SummaryTable deoptInfo={deoptInfo} perFileStats={perFileStats} />
		</Fragment>
	);
}
Example #13
Source File: home.js    From domicilioBoilerplate with GNU Affero General Public License v3.0 5 votes vote down vote up
render(props, { filter, categoryFilter }) {
		const { results: stores } = props;
    const filteredStores = this.filteredCategories(filter, categoryFilter);
    const storesNumber = this.calculateStoresNumber();
    const finalSentence = this.getFinalSentence(9);
		const isEmptySearch = this.isEmptySearch(filteredStores);
    
		return (
			<Fragment>
            {storesNumber > 0 && (
               <div class="text-center mt-2 mb-5">
                  {storesNumber} attività {finalSentence} che consegnano a domicilio a <span class="capitalize">{process.env.PREACT_APP_CITY}</span>.
               </div>
            )}
				<div class="relative p-5 lg:max-w-5xl xl:max-w-6xl lg:m-auto pb-10">
					<input
						class="bg-white focus:outline-none focus:shadow-outline border border-gray-500 rounded-lg py-2 px-4 block w-full appearance-none leading-normal"
						type="text"
						placeholder="Cerca Attività"
						onInput={this.handleChangeFilter}
					/>
				</div>
				<div class="relative flex overflow-x-scroll text-center mt-2 pb-5">
					{Object.keys(stores).map(key => (
						<button
							onClick={this.handleCategoryFilter(key)}
							class={`m-1 flex-grow-0 flex-shrink-0 items-center border border-blue-500 py-2 px-4 rounded-full ${
								key === categoryFilter
									? "bg-blue-500 hover:bg-blue-500 text-white outline-none text-white"
									: "bg-white hover:bg-blue-500 hover:text-white"
							}`}
						>
							<span>{`${stores[key].icon} ${key}`}</span>
						</button>
					))}
				</div>
				<div class="relative mb-10 font-sans text-md text-gray-800">
					{
						Object.keys(filteredStores)
							.filter(key => filteredStores[key].data.length)
							.map(key => (
								<ListCategory
									name={key}
									category={filteredStores[key]}
									filter={filter}
								/>
							))
					}
				</div>
				{isEmptySearch && (
					<p class="font-bold mt-5 mb-10 text-center">Oops! ? Non ci sono attività corrispondenti alla tua ricerca.</p>
				)}
				<div class="text-center w-full">
					<p class="mb-5">
						Developed with ❤️ by
						<a class="text-orange-500" href={process.env.PREACT_APP_DEV_LINK}> {process.env.PREACT_APP_DEV_NAME}</a>
					</p>
					<a href="https://github.com/tomma5o/domicilioBoilerplate"
						target="_blank"
						rel="noopener noreferrer"
						class="mb-5 text-xs block text-gray-500 hover:underline"
					>Se vuoi crearlo per la tua città visita la pagina GitHub del progetto</a>
				</div>
			</Fragment>
		);
	}
Example #14
Source File: dialog_contacts.js    From domicilioBoilerplate with GNU Affero General Public License v3.0 5 votes vote down vote up
D_Contacts = ({tel, mail, site}) => {
	return (
		<Fragment>
			<h3 class="text-lg font-bold mb-2 text-gray-700">Contatti</h3>
			<div class="mb-5">
				{tel && (
					<p class="mb-2">
						<span
							class="inline-block mr-2 w-6 h-6 bg-green-300 text-xs text-center leading-6 rounded-lg cursor-pointer"
							role="img"
							aria-label="telephone"
						>
						?
						</span>
						<Tel {...{tel}} />
					</p>
				)}
				{site && (
					<p class="mb-2">
						<span
							class="inline-block mr-2 w-6 h-6 bg-orange-300 text-xs text-center leading-6 rounded-lg cursor-pointer"
							role="img"
							aria-label="see the site"
						>
						?
						</span>
						<a href={site} class="inline-block rounded-lg text-sm font-semibold text-gray-700">
							<span>{cleanUrls(site)}</span>
						</a>
					</p>
				)}
				{mail && (
					<p class="mb-2">
						<span
							class="inline-block mr-2 w-6 h-6 bg-blue-300 text-xs text-center leading-6 rounded-lg cursor-pointer"
							role="img"
							aria-label="send mail"
						>
						✉️
						</span>
						<a href={`mailto:${mail}`} class="inline-block rounded-lg text-sm font-semibold text-gray-700">
							<span>{mail}</span>
						</a>
					</p>
				)}
			</div>
		</Fragment>
	);
}
Example #15
Source File: solves-card.js    From rctf with BSD 3-Clause "New" or "Revised" License 5 votes vote down vote up
makeSolvesCard = isPrivate => withStyles({
  root: {
    display: 'grid',
    padding: '20px',
    paddingTop: '0',
    gridTemplateColumns: 'repeat(4, minmax(max-content, 1fr))',
    '& div': {
      margin: 'auto',
      padding: '10px'
    }
  },
  title: {
    gridColumn: '1 / -1',
    margin: '20px auto !important'
  },
  label: {
    borderBottom: '1px solid #fff',
    width: '100%',
    textAlign: 'center'
  },
  inlineLabel: {
    display: 'none'
  },
  icon: {
    width: '60px',
    margin: 'auto !important'
  },
  [`@media (max-width: ${isPrivate ? '1500px' : '800px'})`]: {
    inlineLabel: {
      display: 'initial',
      borderRight: '1px solid #fff'
    },
    root: {
      gridTemplateColumns: 'repeat(2, minmax(max-content, 1fr))',
      '& div': {
        margin: '0'
      }
    },
    label: {
      display: 'none'
    },
    category: {
      borderTop: '1px solid #fff'
    }
  }
}, ({ classes, solves }) => {
  return (
    <div class={`card ${classes.root}`}>
      {solves.length === 0 ? (
        <div class={classes.title}>
          <div class={classes.icon}>
            <Clock />
          </div>
          <h5>This team has no solves.</h5>
        </div>
      ) : (
        <Fragment>
          <h5 class={`title ${classes.title}`}>Solves</h5>
          <div class={classes.label}>Category</div>
          <div class={classes.label}>Challenge</div>
          <div class={classes.label}>Solve time</div>
          <div class={classes.label}>Points</div>
          {solves.map((solve) => (
            <Fragment key={solve.id}>
              <div class={`${classes.inlineLabel} ${classes.category}`}>Category</div>
              <div class={classes.category}>{solve.category}</div>
              <div class={classes.inlineLabel}>Name</div>
              <div>{solve.name}</div>
              <div class={classes.inlineLabel}>Solve time</div>
              <div>{formatRelativeTime(solve.createdAt)}</div>
              <div class={classes.inlineLabel}>Points</div>
              <div>{solve.points}</div>
            </Fragment>
          ))}
        </Fragment>
      )}
    </div>
  )
})
Example #16
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 #17
Source File: pagination.js    From rctf with BSD 3-Clause "New" or "Revised" License 5 votes vote down vote up
function Pagination ({ totalItems, pageSize, page, setPage, numVisiblePages }) {
  numVisiblePages = numVisiblePages || 9
  const totalPages = Math.ceil(totalItems / pageSize)
  const { pages, startPage, endPage } = useMemo(() => {
    // Follow the google pagination principle of always showing 10 items
    let startPage, endPage
    if (totalPages <= numVisiblePages) {
      // Display all
      startPage = 1
      endPage = totalPages
    } else {
      // We need to hide some pages
      startPage = page - Math.ceil((numVisiblePages - 1) / 2)
      endPage = page + Math.floor((numVisiblePages - 1) / 2)
      if (startPage < 1) {
        startPage = 1
        endPage = numVisiblePages
      } else if (endPage > totalPages) {
        endPage = totalPages
        startPage = totalPages - numVisiblePages + 1
      }
      if (startPage > 1) {
        startPage += 2
      }
      if (endPage < totalPages) {
        endPage -= 2
      }
    }

    const pages = [] // ...Array((endPage + 1) - startPage).keys()].map(i => startPage + i)
    for (let i = startPage; i <= endPage; i++) {
      pages.push(i)
    }
    return { pages, startPage, endPage }
  }, [totalPages, page, numVisiblePages])

  const boundSetPages = useMemo(() => {
    const bsp = []
    for (let i = 1; i <= totalPages; i++) {
      bsp.push(() => setPage(i))
    }
    return bsp
  }, [setPage, totalPages])

  return (
    <div class='pagination u-center'>
      <PaginationItem disabled={page === 1} key='<' onClick={boundSetPages[page - 1 - 1]}>&lt;</PaginationItem>
      { startPage > 1 &&
        <Fragment>
          <PaginationItem key={1} onClick={boundSetPages[0]}>1</PaginationItem>
          <PaginationEllipses key='.<' />
        </Fragment>
      }
      {pages.map((p) => <PaginationItem selected={p === page} key={p} onClick={boundSetPages[p - 1]}>{p}</PaginationItem>)}
      { endPage < totalPages &&
        <Fragment>
          <PaginationEllipses key='.>' />
          <PaginationItem key={totalPages} onClick={boundSetPages[totalPages - 1]}>{totalPages}</PaginationItem>
        </Fragment>
      }
      <PaginationItem disabled={page === totalPages} key='>' onClick={boundSetPages[page + 1 - 1]}>&gt;</PaginationItem>
    </div>
  )
}
Example #18
Source File: page.jsx    From paypal-installments with Apache License 2.0 5 votes vote down vote up
function Page({ cspNonce, content } : PageProps) : mixed {
    const { data, close } = useXProps();
    const [ visible, setVisible ] = useState(false);

    useEffect(() => {
        const hasOptions = Boolean(data && data.options && data.options.length);
        setVisible(hasOptions);
    }, [ data ]);

    return (
        <Fragment>
            <style nonce={ cspNonce }>
                {`
                    * {
                        box-sizing: border-box;
                    }

                    html, body {
                        margin: 0;
                        padding: 0;
                        font-family: Helvetica, sans-serif;
                        font-size: 14px;
                    }

                    body {
                        width: 100%;
                        overflow:auto;
                    }
                `}
            </style>

            {
                (visible)
                    ? <Installments
                        data={ data }
                        cspNonce={ cspNonce }
                        close={ close }
                        content={ content } />
                    : null
            }
        </Fragment>
    );
}
Example #19
Source File: SummaryList.js    From v8-deopt-viewer with MIT License 5 votes vote down vote up
// TODO:
// - Consider putting each file into a Spectre Panel.
// - Consider using Panel tabs for each of the classifications. Maybe make the list a two column "field: value" list
//   but current header text is too big for Panel tabs

/**
 * @param {import('./Summary').SummaryProps} props
 */
export function SummaryList({ deoptInfo, perFileStats }) {
	return (
		<Fragment>
			<div class={globalHeaders}></div>
			<ul class={summaryList}>
				{Object.keys(perFileStats).map((fileName, i) => {
					const summaryInfo = perFileStats[fileName];

					return (
						<li key={fileName}>
							<div>
								<a href={`#/file/${i}`}>{deoptInfo[fileName].relativePath}</a>
							</div>
							<InlineSeverityTable
								class={codes}
								caption="Optimizations"
								severities={summaryInfo.codes}
								header={<CodeTableHeaders />}
							/>
							<InlineSeverityTable
								class={deopts}
								caption="Deoptimizations"
								severities={summaryInfo.deopts}
								header={<SeverityTableHeaders />}
							/>
							<InlineSeverityTable
								class={ics}
								caption="Inline Caches"
								severities={summaryInfo.ics}
								header={<SeverityTableHeaders />}
							/>
						</li>
					);
				})}
			</ul>
		</Fragment>
	);
}
Example #20
Source File: graph.js    From rctf with BSD 3-Clause "New" or "Revised" License 4 votes vote down vote up
function Graph ({ graphData, classes }) {
  const svgRef = useRef(null)
  const [width, setWidth] = useState(window.innerWidth)
  const updateWidth = useCallback(() => {
    if (svgRef.current === null) return
    setWidth(svgRef.current.getBoundingClientRect().width)
  }, [])

  const [tooltipData, setTooltipData] = useState({
    x: 0,
    y: 0,
    content: ''
  })

  useLayoutEffect(() => {
    updateWidth()
  }, [updateWidth])
  useEffect(() => {
    function handleResize () {
      updateWidth()
    }
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [updateWidth])

  const { polylines, labels } = useMemo(() => {
    if (!graphData || graphData.length === 0) {
      return {
        polylines: [],
        labels: []
      }
    }
    const minX = config.startTime
    const maxX = Math.min(Date.now(), config.endTime)
    let maxY = 0
    graphData.graph.forEach((user) => {
      user.points.forEach((point) => {
        if (point.score > maxY) {
          maxY = point.score
        }
      })
    })
    const labels = getXLabels({ minX, maxX, width })
    const polylines = graphData.graph.map((user) => pointsToPolyline({
      points: user.points,
      id: user.id,
      name: user.name,
      currentScore: user.points[0].score,
      maxX,
      minX,
      maxY,
      width
    }))
    return { polylines, labels }
  }, [graphData, width])

  const handleTooltipIn = useCallback((content) => () => {
    setTooltipData(d => ({
      ...d,
      content
    }))
  }, [])

  const handleTooltipMove = useCallback((evt) => {
    setTooltipData(d => ({
      ...d,
      x: evt.clientX,
      y: evt.clientY
    }))
  }, [])

  const handleTooltipOut = useCallback(() => {
    setTooltipData(d => ({
      ...d,
      content: ''
    }))
  }, [])

  if (graphData === null) {
    return null
  }

  return (
    <div class={`frame ${classes.root}`}>
      <div class='frame__body'>
        <svg ref={svgRef} viewBox={`${-stroke - axis} ${-stroke} ${width + stroke * 2 + axis} ${height + stroke * 2 + axis + axisGap}`}>
          <Fragment>
            {polylines.map(({ points, color, name, currentScore }, i) => (
              <GraphLine
                key={i}
                stroke={color}
                points={points}
                name={name}
                currentScore={currentScore}
                onMouseMove={handleTooltipMove}
                onMouseOut={handleTooltipOut}
                onTooltipIn={handleTooltipIn}
              />
            ))}
          </Fragment>
          <Fragment>
            {labels.map((label, i) => (
              <text x={label.x} y={height + axis + axisGap} key={i} fill='#fff'>{label.label}</text>
            ))}
          </Fragment>
          <line
            x1={-axisGap}
            y1={height + axisGap}
            x2={width}
            y2={height + axisGap}
            stroke='var(--cirrus-bg)'
            stroke-linecap='round'
            stroke-width={stroke}
          />
          <line
            x1={-axisGap}
            y1='0'
            x2={-axisGap}
            y2={height + axisGap}
            stroke='var(--cirrus-bg)'
            stroke-linecap='round'
            stroke-width={stroke}
          />
        </svg>
      </div>
      {tooltipData.content && (
        <div
          class={classes.tooltip}
          style={{
            transform: `translate(${tooltipData.x}px, ${tooltipData.y}px)`
          }}
        >
          {tooltipData.content}
        </div>
      )}
    </div>
  )
}
Example #21
Source File: problem.js    From rctf with BSD 3-Clause "New" or "Revised" License 4 votes vote down vote up
Problem = ({ classes, problem, update: updateClient }) => {
  const { toast } = useToast()

  const [flag, setFlag] = useState(problem.flag)
  const handleFlagChange = useCallback(e => setFlag(e.target.value), [])

  const [description, setDescription] = useState(problem.description)
  const handleDescriptionChange = useCallback(e => setDescription(e.target.value), [])

  const [category, setCategory] = useState(problem.category)
  const handleCategoryChange = useCallback(e => setCategory(e.target.value), [])

  const [author, setAuthor] = useState(problem.author)
  const handleAuthorChange = useCallback(e => setAuthor(e.target.value), [])

  const [name, setName] = useState(problem.name)
  const handleNameChange = useCallback(e => setName(e.target.value), [])

  const [minPoints, setMinPoints] = useState(problem.points.min)
  const handleMinPointsChange = useCallback(e => setMinPoints(Number.parseInt(e.target.value)), [])

  const [maxPoints, setMaxPoints] = useState(problem.points.max)
  const handleMaxPointsChange = useCallback(e => setMaxPoints(Number.parseInt(e.target.value)), [])

  const [tiebreakEligible, setTiebreakEligible] = useState(problem.tiebreakEligible !== false)
  const handleTiebreakEligibleChange = useCallback(e => setTiebreakEligible(e.target.checked), [])

  const handleFileUpload = useCallback(async e => {
    e.preventDefault()

    const fileData = await Promise.all(
      Array.from(e.target.files)
        .map(async file => {
          const data = await encodeFile(file)

          return {
            data,
            name: file.name
          }
        })
    )

    const fileUpload = await uploadFiles({
      files: fileData
    })

    if (fileUpload.error) {
      toast({ body: fileUpload.error, type: 'error' })
      return
    }

    const data = await updateChallenge({
      id: problem.id,
      data: {
        files: fileUpload.data.concat(problem.files)
      }
    })

    e.target.value = null

    updateClient({
      problem: data
    })

    toast({ body: 'Problem successfully updated' })
  }, [problem.id, problem.files, updateClient, toast])

  const handleRemoveFile = file => async () => {
    const newFiles = problem.files.filter(f => f !== file)

    const data = await updateChallenge({
      id: problem.id,
      data: {
        files: newFiles
      }
    })

    updateClient({
      problem: data
    })

    toast({ body: 'Problem successfully updated' })
  }

  const handleUpdate = async e => {
    e.preventDefault()

    const data = await updateChallenge({
      id: problem.id,
      data: {
        flag,
        description,
        category,
        author,
        name,
        tiebreakEligible,
        points: {
          min: minPoints,
          max: maxPoints
        }
      }
    })

    updateClient({
      problem: data
    })

    toast({ body: 'Problem successfully updated' })
  }

  const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
  const openDeleteModal = useCallback(e => {
    e.preventDefault()
    setIsDeleteModalOpen(true)
  }, [])
  const closeDeleteModal = useCallback(() => {
    setIsDeleteModalOpen(false)
  }, [])
  const handleDelete = useCallback(() => {
    const action = async () => {
      await deleteChallenge({
        id: problem.id
      })
      toast({
        body: `${problem.name} successfully deleted`,
        type: 'success'
      })
      closeDeleteModal()
    }
    action()
  }, [problem, toast, closeDeleteModal])

  return (
    <Fragment>
      <div class={`frame ${classes.frame}`}>
        <div class='frame__body'>
          <form onSubmit={handleUpdate}>
            <div class='row u-no-padding'>
              <div class={`col-6 ${classes.header}`}>
                <input
                  autocomplete='off'
                  autocorrect='off'
                  required
                  class='form-group-input input-small'
                  placeholder='Category'
                  value={category}
                  onChange={handleCategoryChange}
                />
                <input
                  autocomplete='off'
                  autocorrect='off'
                  required
                  class='form-group-input input-small'
                  placeholder='Problem Name'
                  value={name}
                  onChange={handleNameChange}
                />
                <div class='form-ext-control form-ext-checkbox'>
                  <input
                    id={`chall-${problem.id}-tiebreak-eligible`}
                    type='checkbox'
                    class='form-ext-input'
                    checked={tiebreakEligible}
                    onChange={handleTiebreakEligibleChange}
                  />
                  <label for={`chall-${problem.id}-tiebreak-eligible`} class='form-ext-label'>Eligible for tiebreaks?</label>
                </div>
              </div>
              <div class={`col-6 ${classes.header}`}>
                <input
                  autocomplete='off'
                  autocorrect='off'
                  required
                  class='form-group-input input-small'
                  placeholder='Author'
                  value={author}
                  onChange={handleAuthorChange}
                />
                <input
                  class='form-group-input input-small'
                  type='number'
                  required
                  value={minPoints}
                  onChange={handleMinPointsChange}
                />
                <input
                  class='form-group-input input-small'
                  type='number'
                  required
                  value={maxPoints}
                  onChange={handleMaxPointsChange}
                />
              </div>
            </div>

            <div class='content-no-padding u-center'><div class={`divider ${classes.divider}`} /></div>

            <textarea
              autocomplete='off'
              autocorrect='off'
              placeholder='Description'
              value={description}
              onChange={handleDescriptionChange}
            />
            <div class='input-control'>
              <input
                autocomplete='off'
                autocorrect='off'
                required
                class='form-group-input input-small'
                placeholder='Flag'
                value={flag}
                onChange={handleFlagChange}
              />
            </div>

            {
              problem.files.length !== 0 &&
                <div>
                  <p class={`frame__subtitle u-no-margin ${classes.downloadsHeader}`}>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 class='tag tag--delete' style='margin: 0; margin-left: 3px' onClick={handleRemoveFile(file)} />
                          </div>
                        )
                      })
                    }

                  </div>
                </div>
            }

            <div class='input-control'>
              <input class='form-group-input input-small' type='file' multiple onChange={handleFileUpload} />
            </div>

            <div class={`form-section ${classes.controls}`}>
              <button class='btn-small btn-info'>Update</button>
              <button class='btn-small btn-danger' onClick={openDeleteModal} type='button' >Delete</button>
            </div>
          </form>
        </div>
      </div>
      <DeleteModal open={isDeleteModalOpen} onClose={closeDeleteModal} onDelete={handleDelete} />
    </Fragment>
  )
}
Example #22
Source File: installments.jsx    From paypal-installments with Apache License 2.0 4 votes vote down vote up
export function Installments({ data, cspNonce, close, content } : InstallmentsProps) : any {
    const [ selectedOption, setSelectedOption ] = useState(null);
    const [ selectedIndex, setSelectedIndex ] = useState(null);

    const selectOption = (event, option, index) => {
        event.preventDefault();
        setSelectedOption(option);
        setSelectedIndex(index);

        return option.onSelect(option);
    };

    const onPay = () => {
        close();
        return data.onPay(selectedOption);
    };

    const closeInstallments = () => {
        close();
        return data.onClose();
    };

    const renderContent = (line, dataObj) => {
        return line.replace(/{([^{]+)}/g, (match, key) => {
            return dataObj[key] !== undefined ? dataObj[key] : '';
        });
    };

    return (
        <Fragment>
            <style nonce={ cspNonce }>
                {`
                    .installments {
                        outline-style: none;
                        padding-bottom: 20px;
                        position: relative;
                    }

                    .installments .header {
                        box-shadow: 0 2px 3px 0 rgba(0,0,0,0.2);
                        padding-right: 50px;
                        position: relative;
                    }

                    .installments h3 {
                        padding: 15px 20px;
                        margin:0;
                        font-weight: normal;
                        font-size: 1em;
                    }

                    .installments button {
                        background: #fff;
                        border: none;
                    }

                    .installments ul {
                        margin:0;
                        padding:0;
                    }
                    .installments li {
                        padding: 0 10px;
                    }
                    .installments li .list-wrap {
                        border: 1px solid #fff;
                        border-bottom-color: #ccc;
                        padding: 13px;
                        display: flex;
                        text-decoration: none;
                        color: inherit;
                        width: 100%;
                        text-align: left;
                        align-items: inherit;
                        font-size: 1em;
                        font-family: inherit;
                        cursor: pointer;
                    }
                    .installments li .list-wrap:hover {
                        background: #F6F7FA;
                    }
                    .installments li .list-wrap:active {
                        border:1px solid #000;
                    }
                    .installments li.selected .list-wrap {
                        background: #F6F7FA;
                    }
                    .installments .months {
                        align-items: center;
                        display: flex;
                        width: 50px;
                        position: relative;
                    }
                    .installments .months:after {
                        content: "";
                        width: 1px;
                        top: 0;
                        bottom: 0;
                        background: #ccc;
                        position: absolute;
                        right: 15px;
                    }
                    .installments .details {
                        font-size: 0.9em;
                    }
                    .installments .details .price {
                        display: block;
                        font-weight:bold;
                        margin-bottom:6px;
                    }

                    .installments .agree-info {
                        font-size: 0.9em;
                        text-align: center;
                        padding: 0 10px;
                    }

                    .installments .btn-container {
                        text-align: center;
                        padding: 15px;
                    }
                    .installments .pay-btn{
                        border: 0;
                        background: #2C2E2F;
                        padding: 10px 20px;
                        border-radius: 25px;
                        line-height: 1.5em;
                        color: #fff;
                        font-weight: bold;
                        font-size: 1em;
                        transition: background-color 240ms ease;
                        cursor: pointer;
                    }
                    .installments .pay-btn:hover,
                    .installments .pay-btn:focus {
                        filter: brightness(1.2);
                        outline: 0;
                    }
                    .installments .pay-btn:active,
                    .installments .pay-btn:focus {
                        text-decoration: underline;
                    }
                    .installments .pay-btn .amount{
                        margin-left:5px;
                    }

                    .installments .close-btn {
                        position: absolute;
                        right: 11px;
                        top: 11px;
                        width: 26px;
                        height: 26px;
                        opacity: 0.6;
                        z-index: 1;
                        cursor: pointer;
                    }
                    .installments .close-btn:hover {
                        opacity: 1;
                    }
                    .installments .close-btn:before,
                    .installments .close-btn:after {
                        position: absolute;
                        left: 12px;
                        content: ' ';
                        height: 16px;
                        width: 2px;
                        background-color: #000;
                        transform: rotate(45deg);
                        top: 5px;
                    }
                    .installments .close-btn:after {
                        transform: rotate(-45deg);
                    }
                `}
            </style>

            <div class='installments'>
                <button class="close-btn" onClick={ closeInstallments } aria-label="close" type="button" />
                <div className="header">
                    <h3 className="title">{content.header}</h3>
                </div>
                <ul id="installments-list">
                    {
                        data.options.map((option, i) => {
                            return (
                                <li className={ (selectedIndex === i) ? 'selected' : '' }>
                                    <button type="button" class="list-wrap" onClick={ (event) => { selectOption(event, option, i); } }>
                                        <div className="months">{ option.term }x</div>
                                        <div className="details">
                                            { option.term === 1 ?
                                                <span className="price">
                                                    { option.percent ?
                                                        renderContent(content.monthly1xWithDiscount, { percent: Number(option.percent) }) :
                                                        content.monthly1x }
                                                </span> :
                                                <span className="price">{ renderContent(content.monthly, option) }</span> }
                                            <span className="total">
                                                { option.term === 1 && option.percent ?
                                                    renderContent(content.totalWithDiscount, option) :
                                                    renderContent(content.totalAmount, option) }
                                            </span>
                                        </div>
                                    </button>
                                </li>
                            );
                        })
                    }
                </ul>
                <div className="btn-container">
                    <button type="button" className="pay-btn" onClick={ onPay }>
                        { renderContent(content.payLabel, { payAmount: selectedOption ? selectedOption.totalAmount : data.cartAmount }) }
                    </button>
                </div>
                <div className="agree-info">{ content.disclaimer }</div>
            </div>
        </Fragment>
    );
}
Example #23
Source File: UserReference.js    From duolingo-solution-viewer with MIT License 4 votes vote down vote up
UserReference =
  ({
     context = CONTEXT_CHALLENGE,
     reference = '',
     isEditable = true,
     onUpdate = noop,
   }) => {
    const editInput = useRef();
    const [ isEditing, setIsEditing ] = useState(false);

    const commitEdit = useCallback(event => {
      discardEvent(event);

      if (editInput.current) {
        const newReference = String(editInput.current.value || '').trim();

        if (('' !== newReference) && (newReference !== reference)) {
          onUpdate(newReference);
        }
      }

      setIsEditing(false);
    }, [ reference, onUpdate, setIsEditing ]);

    const rollbackEdit = useCallback(event => {
      discardEvent(event);
      setIsEditing(false);
    }, [ setIsEditing ]);

    const onEditKeyDown = useCallback(event => {
      if ('Enter' === event.key) {
        commitEdit(event);
      } else if ('Escape' === event.key) {
        rollbackEdit(event);
      }
    }, [ commitEdit, rollbackEdit ]);

    // Focuses the input when we just have switched to edit mode.
    useEffect(() => {
      if (editInput.current) {
        setTimeout(() => {
          if (document.activeElement !== editInput.current.focused) {
            const length = editInput.current.value.length;
            editInput.current.focus();
            // Place the cursor at the end of the text.
            editInput.current.setSelectionRange(length + 1, length + 1);
          }
        });
      }
    }, [ isEditing, editInput ]);

    const [ Wrapper, Title, Value, EditWrapper ] = (CONTEXT_CHALLENGE === context)
      ? [ 'div', 'h3', 'p', 'p' ]
      : [ 'h2', 'span', 'span', Fragment ];

    const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ context ]);

    let buttonInlineStyles = {};
    let additionalButtonClass = null;

    if (CONTEXT_FORUM === context) {
      buttonInlineStyles = getForumNewPostButtonsInlineStyles();

      if (null === buttonInlineStyles) {
        const inlineStyles = getForumFollowButtonInlineStyles();

        buttonInlineStyles = {
          [COMMIT_BUTTON]: inlineStyles,
          [ROLLBACK_BUTTON]: inlineStyles,
        };

        additionalButtonClass = FALLBACK_BUTTON;
      }
    }

    const valueKeys = [
      VALUE,
      isEditable && EDITABLE_VALUE,
      ('' === reference) && EMPTY_VALUE,
    ].filter(Boolean);

    return (
      <IntlProvider scope="user_reference">
        <Wrapper className={getElementClassNames(WRAPPER)}>
          <Title className={getElementClassNames(TITLE)}>
            <Text id="your_reference">Your reference:</Text>
          </Title>
          {!isEditing
            ? ( // Not editing.
              <Value onClick={() => isEditable && setIsEditing(true)} className={getElementClassNames(valueKeys)}>
                {('' !== reference) ? reference : <Text id="none">None yet</Text>}
              </Value>
            ) : ( // Editing.
              <EditWrapper>
                <textarea
                  ref={editInput}
                  defaultValue={reference}
                  dir="auto"
                  onKeyDown={onEditKeyDown}
                  className={getElementClassNames(EDIT_FIELD)}
                />

                <button
                  onClick={commitEdit}
                  style={buttonInlineStyles[COMMIT_BUTTON] || ''}
                  className={getElementClassNames([ BUTTON, COMMIT_BUTTON, additionalButtonClass ])}
                >
                  <Text id="update">Update</Text>
                </button>

                <span className={getElementClassNames(BUTTON_SPACER)}>
                  <button
                    onClick={rollbackEdit}
                    style={buttonInlineStyles[ROLLBACK_BUTTON] || ''}
                    className={getElementClassNames([ BUTTON, ROLLBACK_BUTTON, additionalButtonClass ])}
                  >
                    <Text id="cancel">Cancel</Text>
                  </button>
                </span>
              </EditWrapper>
            )}
        </Wrapper>
      </IntlProvider>
    );
  }
Example #24
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 #25
Source File: SolutionList.js    From duolingo-solution-viewer with MIT License 4 votes vote down vote up
SolutionList =
  forwardRef(
    (
      {
        context = CONTEXT_CHALLENGE,
        solutions = [],
        matchingData = {},
        onPageChange = noop,
        scrollOffsetGetter = (() => 0),
      },
      listRef
    ) => {
      const isScoreAvailable = useMemo(() => {
        return solutions.some('score' in it);
      }, [ solutions ]);

      const sortTypes = getAvailableSortTypes(isScoreAvailable);

      const {
        state: sortType,
        nextState: nextSortType,
        next: setNextSortType,
      } = useLocalStorageList(
        'sort-type',
        sortTypes,
        sortTypes[0]
      );

      const {
        state: sortDirection,
        nextState: nextSortDirection,
        next: setNextSortDirection,
      } = useLocalStorageList(
        'sort-direction',
        Object.keys(SORT_DIRECTIONS),
        SORT_DIRECTION_DESC
      );

      const isFilterWordBased = !!matchingData.words;

      // Sort the solutions.

      const sortedSolutions = useMemo(() => (
        solutions.slice()
          .sort(
            SORT_TYPE_SIMILARITY === sortType
              ? (SORT_DIRECTION_ASC === sortDirection ? invertComparison : identity)(Solution.compareByScore)
              : (SORT_DIRECTION_ASC === sortDirection ? identity : invertComparison)(Solution.compareByReference)
          )
      ), [ solutions, sortType, sortDirection ]);

      // Filter the solutions.

      const filterCache = useRef({}).current;
      const [ filters, filtersRef, setFilters ] = useStateRef([]);

      const filteredSolutions = useMemo(
        () => (
          isFilterWordBased
            ? filterSolutionsUsingWords
            : filterSolutionsUsingSummaries
        )(sortedSolutions, filters, filterCache),
        [ sortedSolutions, filters, filterCache, isFilterWordBased ]
      );

      // Paginate and render the current solutions.

      const [ rawPage, setRawPage ] = useState(1);
      const shouldTriggerPageChange = useRef(false);
      const [ pageSize, setRawPageSize ] = useLocalStorage('page_size', DEFAULT_PAGE_SIZE);

      const page = (PAGE_SIZE_ALL === pageSize)
        ? 1
        : Math.min(rawPage, Math.ceil(filteredSolutions.length / pageSize));

      const setPage = useCallback(page => {
        setRawPage(page);
        shouldTriggerPageChange.current = true;
      }, [ setRawPage ]);

      const setPageSize = useCallback(size => {
        setRawPageSize(size);

        if (PAGE_SIZE_ALL === size) {
          setRawPage(1);
        } else {
          // Update the current page to keep the same solution at the top of the list.
          const sizeValue = Number(size);

          if (PAGE_SIZES.indexOf(sizeValue) === -1) {
            return;
          }

          const oldSize = (PAGE_SIZE_ALL === pageSize)
            ? filteredSolutions.length
            : Math.min(pageSize, filteredSolutions.length);

          setRawPage(Math.ceil(((page - 1) * oldSize + 1) / sizeValue));
        }

        shouldTriggerPageChange.current = true;
      }, [ page, pageSize, filteredSolutions.length, setRawPageSize ]);

      const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ context ]);

      const solutionItems = useMemo(() => {
        const renderSolutionItem = solution => (
          <li className={getElementClassNames(SOLUTION)}>
            {Solution.getReaderFriendlySummary(solution)}
          </li>
        );

        const pageSolutions = (PAGE_SIZE_ALL === pageSize)
          ? filteredSolutions
          : filteredSolutions.slice((page - 1) * pageSize, page * pageSize);

        return pageSolutions.map(renderSolutionItem);
      }, [ page, pageSize, filteredSolutions, getElementClassNames ]);

      // Triggers the "page change" callback asynchronously,
      // to make sure it is run only when the changes have been applied to the UI.
      useEffect(() => {
        if (shouldTriggerPageChange.current) {
          setTimeout(onPageChange());
          shouldTriggerPageChange.current = false;
        }
      }, [ solutionItems, onPageChange, shouldTriggerPageChange ]);

      const filterWrapperRef = useRef();

      // Scrolls the filter input into view when it is focused.
      const onFilterFocus = useCallback(() => {
        filterWrapperRef.current
        && scrollElementIntoParentView(filterWrapperRef.current, scrollOffsetGetter(), 'smooth');
      }, [ scrollOffsetGetter, filterWrapperRef ]);

      // Focuses the solution list when the filter input loses focus, to ensure that the list is scrollable again.
      const onFilterBlur = useCallback(() => listRef.current?.closest('[tabindex]')?.focus(), [ listRef ]);

      // Detects selected words, and proposes new filter options when relevant.
      const [ selectedWord, setSelectedWord ] = useState(null);

      useEffect(() => {
        // Detect when the left button is released to only propose suggestions when a selection has been committed.
        const onMouseUp = event => {
          if (listRef.current && (event.button === 0)) {
            const selection = document.getSelection();

            if (
              selection.anchorNode
              && (selection.anchorNode === selection.focusNode)
              && listRef.current.contains(selection.anchorNode)
              && (selection.anchorNode.parentNode.nodeName === 'LI')
            ) {
              // We are only interested in single-word selections.
              const words = Solution.getStringMatchableWords(
                selection.toString().trim(),
                matchingData.locale,
                matchingData.matchingOptions
              );

              if (1 === words.length) {
                const selectedText = !isFilterWordBased
                  ? selection.toString()
                  : getWordAt(
                    selection.anchorNode.wholeText,
                    Math.floor((selection.anchorOffset + selection.focusOffset) / 2)
                  );

                const [ word = '' ] = Solution.getStringMatchableWords(
                  selectedText,
                  matchingData.locale,
                  matchingData.matchingOptions
                );

                if (
                  (!isFilterWordBased || (word.length > 1))
                  && !(filtersRef.current || []).some(it.word === word)
                ) {
                  const bbox = selection.getRangeAt(0).getBoundingClientRect();
                  const offsetParent = getFixedElementPositioningParent(listRef.current);

                  if (offsetParent) {
                    const parentBbox = offsetParent.getBoundingClientRect();
                    bbox.x -= parentBbox.x;
                    bbox.y -= parentBbox.y;
                  }

                  setSelectedWord({
                    word,
                    bbox: {
                      left: `${Math.floor(bbox.x)}px`,
                      top: `${Math.floor(bbox.y)}px`,
                      width: `${Math.ceil(bbox.width)}px`,
                      height: `${Math.ceil(bbox.height)}px`,
                    },
                  });

                  return;
                }
              }
            }
          }

          // Delay hiding the actions dropdown to let "click" events be triggered normally.
          setTimeout(() => setSelectedWord(null));
        };

        // Detect change events to ensure that suggestions are hidden when the selection is canceled.
        const onSelectionChange = () => {
          const selection = document.getSelection();

          if (!selection || ('None' === selection.type)) {
            setSelectedWord(null);
          }
        };

        document.addEventListener('mouseup', onMouseUp);
        document.addEventListener('selectionchange', onSelectionChange);

        return () => {
          document.removeEventListener('mouseup', onMouseUp);
          document.removeEventListener('selectionchange', onSelectionChange);
        }
      });

      if (0 === solutions.length) {
        return null;
      }

      return (
        <IntlProvider scope="solution_list">
          <div>
            <h3 ref={filterWrapperRef} className={getElementClassNames(TITLE)}>
              <span className={getElementClassNames(TITLE_TEXT)}>
                <Text id="filter">Filter:</Text>
              </span>

              <FilterInput
                context={context}
                matchMode={isFilterWordBased ? STRING_MATCH_MODE_WORDS : STRING_MATCH_MODE_GLOBAL}
                matchingData={matchingData}
                minQueryLength={isFilterWordBased ? 2 : 1}
                filters={filters}
                onChange={setFilters}
                onFocus={onFilterFocus}
                onBlur={onFilterBlur}
              />
            </h3>

            <div ref={listRef}>
              <h3 className={getElementClassNames(TITLE)}>
                <span className={getElementClassNames(TITLE_TEXT)}>
                  <Text id="correct_solutions">Correct solutions:</Text>
                </span>

                <ListSortLinks
                  context={context}
                  availableSortTypes={sortTypes}
                  sortType={sortType}
                  nextSortType={nextSortType}
                  sortDirection={sortDirection}
                  nextSortDirection={nextSortDirection}
                  onSortTypeToggle={() => setNextSortType()}
                  onSortDirectionToggle={() => setNextSortDirection()}
                />
              </h3>

              {(0 === filteredSolutions.length)
                ? (
                  <div className={getElementClassNames(EMPTY_LIST)}>
                    <Text id="no_matching_solution">There is no matching solution.</Text>
                  </div>
                ) : (
                  <Fragment>
                    <ul>{solutionItems}</ul>

                    {selectedWord && (
                      <SelectedWordActions
                        {...selectedWord}
                        context={context}
                        matchType={isFilterWordBased ? STRING_MATCH_TYPE_EXACT : STRING_MATCH_TYPE_ANYWHERE}
                        onAddFilter={setFilters([ ...filters, _ ])}
                      />
                    )}

                    <ListPagination
                      context={context}
                      solutionCount={filteredSolutions.length}
                      page={page}
                      pageSize={pageSize}
                      onPageChange={setPage}
                      onPageSizeChange={setPageSize}
                    />
                  </Fragment>
                )}
            </div>
          </div>
        </IntlProvider>
      );
    }
  )
Example #26
Source File: ChallengeSolutions.js    From duolingo-solution-viewer with MIT License 4 votes vote down vote up
ChallengeSolutions =
  ({
     context = CONTEXT_CHALLENGE,
     statement = '',
     solutions = [],
     matchingData = {},
     userReference = '',
     onUserReferenceUpdate = noop,
     isUserReferenceEditable = true,
     scrollOffsetGetter = (() => 0),
   }) => {
    const [ isLoading, setIsLoading ] = useState(false);
    const [ currentSolutions, setCurrentSolutions ] = useState(solutions);
    const [ currentUserReference, setCurrentUserReference ] = useState(userReference);
    const [ isUserReferencePinned, setIsUserReferencedPinned ] = useLocalStorage('user_reference_pinned', false);

    // Updates the user reference and waits for a new list of solutions.
    const updateUserReference = useCallback(newReference => {
      setIsLoading(true);
      setCurrentUserReference(newReference);

      Promise.resolve(onUserReferenceUpdate(newReference))
        .then(solutions => {
          if (isArray(solutions)) {
            setCurrentSolutions(solutions)
          } else {
            setCurrentUserReference(currentUserReference);
          }
        }).catch(() => (
          setCurrentUserReference(currentUserReference)
        )).then(() => {
          setIsLoading(false);
        });
    }, [
      onUserReferenceUpdate,
      setIsLoading,
      setCurrentSolutions,
      currentUserReference,
      setCurrentUserReference,
    ]);

    const listWrapper = useRef();
    const referenceWrapper = useRef();

    const fullScrollOffsetGetter = useCallback(() => (
      10
      + scrollOffsetGetter()
      + (isUserReferencePinned && referenceWrapper.current?.offsetHeight || 0)
    ), [ scrollOffsetGetter, isUserReferencePinned, referenceWrapper ]);

    // Scrolls to the top of the solution list whenever it changes.
    const onSolutionListChange = useCallback(() => {
      listWrapper.current
      && scrollElementIntoParentView(listWrapper.current, fullScrollOffsetGetter(), 'smooth');
    }, [ // eslint-disable-line react-hooks/exhaustive-deps
      listWrapper,
      fullScrollOffsetGetter,
      currentSolutions,
    ]);

    const getElementClassNames = useStyles(CLASS_NAMES, STYLE_SHEETS, [ context ]);

    if (0 === currentSolutions.length) {
      return null;
    }

    return (
      <IntlProvider scope="challenge">
        {('' !== statement) && (
          <Fragment>
            <h3>
              <Text id="statement">Statement:</Text>
            </h3>
            <p>{statement}</p>
          </Fragment>
        )}

        <div
          ref={referenceWrapper}
          className={getElementClassNames([
            REFERENCE_WRAPPER,
            isUserReferencePinned && REFERENCE_WRAPPER__PINNED
          ])}
        >
          <UserReference
            context={context}
            reference={currentUserReference}
            onUpdate={updateUserReference}
            isEditable={isUserReferenceEditable && !isLoading}
          />
          {(CONTEXT_CHALLENGE === context)
          && (
            <IntlProvider scope="user_reference">
              <Localizer>
                <div
                  onClick={() => setIsUserReferencedPinned(!isUserReferencePinned)}
                  title={(
                    <Text id={isUserReferencePinned ? 'unpin' : 'pin'}>
                      {isUserReferencePinned ? 'Unpin' : 'Pin'}
                    </Text>
                  )}
                  className={getElementClassNames([
                    PIN_BUTTON,
                    isUserReferencePinned && PIN_BUTTON__PINNED
                  ])}
                >
                  <FontAwesomeIcon
                    icon={[ 'far', 'thumbtack' ]}
                    className={getElementClassNames(PIN_BUTTON_ICON)}
                  />
                </div>
              </Localizer>
            </IntlProvider>
          )}
        </div>

        <div>
          {isLoading
            ? (
              <div className={getElementClassNames(LOADER)}>
                <Loader />
              </div>
            ) : (
              <SolutionList
                ref={listWrapper}
                context={context}
                solutions={currentSolutions}
                matchingData={matchingData}
                onPageChange={onSolutionListChange}
                scrollOffsetGetter={fullScrollOffsetGetter}
              />
            )}
        </div>
      </IntlProvider>
    );
  }
Example #27
Source File: MapExplorer.js    From v8-deopt-viewer with MIT License 4 votes vote down vote up
/**
 * @typedef {import('v8-deopt-parser').MapData} MapData
 * @typedef {import('v8-deopt-parser').MapEntry} MapEntry
 * @typedef {import('v8-deopt-parser').MapEdge} MapEdge
 * @typedef {import("../..").FileV8DeoptInfoWithSources} FileV8DeoptInfo
 *
 * @typedef MapExplorerRouteParams
 * @property {string} fileId
 * @property {MapGrouping} [grouping]
 * @property {string} [groupValue]
 * @property {string} [mapId]
 *
 * @typedef MapExplorerProps
 * @property {MapData} mapData
 * @property {FileV8DeoptInfo} fileDeoptInfo
 * @property {MapExplorerRouteParams} routeParams
 * @property {import('../CodeSettings').CodeSettingsState} settings
 * @property {number} fileId
 *
 * @param {MapExplorerProps} props
 */
export function MapExplorer(props) {
	const hasMaps = hasMapData(props.mapData);
	if (!hasMaps) {
		return (
			<div>
				<p>
					No map data found in this log file. In order to explore maps, re-run
					v8-deopt-viewer without the '--skipMaps' flag to include map data in
					your log
				</p>
			</div>
		);
	}

	// TODO:
	//  - Hmm should switching tabs loose state in Map Explorer? Probs not :(
	//  - Can selecting a loadIC location also highlight that IC entry in the IC
	//    Explorer tab to maintain context between the two?
	//
	//  Re: the above - perhaps FileViewer should on initial render read the URL
	//  param to determine the initial deopt panel to display (pass it into the
	//  initialState arg of useState) but use local state when changing tabs. Then
	//  each deopt panel can use the `useRoute` hook to determine if it is the
	//  active route and read it's state from that route, else rely on local or
	//  global shared state.

	// TODO: Handle cross file map source links since maps aren't file specific.
	// Consider where currentFile id should be stored. Currently it is a prop to
	// MapExplorer but should probably live somewhere central and managed there.

	// TODO: Consider showing map details inline on the ICEntry table

	// TODO: Show entire tree (all children) for selected map

	// TODO: Maybe I just just re-design the entire UI lol. Initial drawings I
	// like:
	//
	// File selector and viewer on the left with inline dropdown as well as link
	// to replace file viewer with a file list. Also includes toggle to filter
	// data entries by the selected file.
	//
	// On the right, a VSCode- or Teams-like UI with icons running down the right
	// side (with labels underneath or tooltips) for various views: ?Summary,
	// ?Opts, ?Deopts, ☎Inline Caches, ?Map explorer, ⚙Settings, ❔Help.
	//
	// Since map explorer is across files, re-consider how general nav works. Some
	// Spectre components/experiments that may be useful:
	// - https://picturepan2.github.io/spectre/experimentals/filters.html
	//    - filter entries by file
	// - https://picturepan2.github.io/spectre/experimentals/autocomplete.html
	//    - filter entries by file
	//    - select which file's code to show
	//    - select which data tables to show (Summary, Opts, Deopts, IC Entries,
	//      etc.)
	// - https://picturepan2.github.io/spectre/components/breadcrumbs.html
	//    - Display which file is shown and filter file results
	// - https://picturepan2.github.io/spectre/components/cards.html
	//    - File summary organizer
	// - https://picturepan2.github.io/spectre/components/modals.html
	//    - More advanced settings menu
	// - https://picturepan2.github.io/spectre/components/panels.html
	//    - File summary organizer
	//    - Data view organizer
	// - https://picturepan2.github.io/spectre/components/tiles.html
	//    - ?
	// - https://picturepan2.github.io/spectre/components/tabs.html
	//    - ?

	const { mapData, fileDeoptInfo, routeParams, settings, fileId } = props;

	// const [state, dispatch] = useReducer(
	// 	mapGroupingReducer,
	// 	props,
	// 	initGroupingState
	// );

	// useEffect(() => {
	// 	if (state.grouping == "loadic") {
	// 		// Re-use SET_GROUPING action to recompute groupingValues. Currently this
	// 		// only needs to happen on the loadic grouping
	// 		dispatch({
	// 			type: "SET_GROUPING",
	// 			newGrouping: state.grouping,
	// 			newGroupValues: getGroupingValues(
	// 				state.grouping,
	// 				mapData,
	// 				fileDeoptInfo,
	// 				settings
	// 			),
	// 		});
	// 	}
	// }, [mapData, fileDeoptInfo, settings]);

	const state = initGroupingState(props);

	const [_, setLocation] = useLocation();

	const mapIds = state.selectedGroup?.mapIds ?? [];

	const appState = useAppState();
	const { setSelectedEntry, setSelectedPosition } = useAppDispatch();
	const showICLocation =
		appState.selectedEntry == null &&
		appState.selectedPosition != null &&
		state.grouping == "loadic";

	useEffect(() => {
		if (state.selectedGroup) {
			if (
				state.selectedGroup.group == "loadic" &&
				state.selectedGroup.entry.file == fileDeoptInfo.id
			) {
				setSelectedEntry(state.selectedGroup.entry);
			} else if (
				state.selectedGroup.group == "create" &&
				state.selectedGroup.filePosition.file == fileDeoptInfo.id
			) {
				setSelectedPosition(state.selectedGroup.filePosition);
			} else {
				setSelectedEntry(null);
			}
		}
	}, [state.selectedGroup, fileDeoptInfo]);

	return (
		<Fragment>
			<div class={map_selectors}>
				<div class={[form_group, map_grouping].join(" ")}>
					<label for="map-grouping" class={form_label}>
						{/* "Find Maps by" or "Map grouping" */}
						Group Maps by:
					</label>
					<select
						value={state.grouping}
						onChange={(e) => {
							/** @type {MapGrouping} */
							// @ts-ignore
							const grouping = e.currentTarget.value;
							// dispatch({
							// 	type: "SET_GROUPING",
							// 	newGrouping: grouping,
							// 	newGroupValues: getGroupingValues(
							// 		grouping,
							// 		mapData,
							// 		fileDeoptInfo,
							// 		settings
							// 	),
							// });
							if (grouping !== state.grouping) {
								setLocation(mapsRoute.getHref(fileId, grouping));
							}
						}}
						id="map-grouping"
						class={form_select}
					>
						{Object.values(mapGroupings).map((group) => (
							<option key={group.value} value={group.value}>
								{group.label}
							</option>
						))}
					</select>
				</div>
				<div class={[form_group, group_value].join(" ")}>
					<label for="map-group" class={form_label}>
						{mapGroupings[state.grouping].valueLabel}:
					</label>
					<select
						value={state.selectedGroup?.id ?? ""}
						disabled={state.groupValues.length == 0}
						onChange={(e) => {
							const newGroupValue = e.currentTarget.value;
							// dispatch({
							// 	type: "SET_GROUP_VALUE",
							// 	newValue: newGroupValue,
							// });
							if (newGroupValue.id !== state.selectedGroup.id) {
								setLocation(
									mapsRoute.getHref(fileId, state.grouping, newGroupValue)
								);
							}
						}}
						id="map-group"
						class={form_select}
					>
						{state.groupValues.length == 0 ? (
							<option>No values available</option>
						) : (
							state.groupValues.map((value) => (
								<option value={value.id}>{value.label}</option>
							))
						)}
					</select>
				</div>
				<div class={[form_group, map_ids].join(" ")}>
					<label for="map-id" class={form_label}>
						Map:
					</label>
					<select
						value={state.selectedMapId}
						id="map-id"
						class={form_select}
						disabled={mapIds.length < 2}
						onChange={(e) => {
							const newMapId = e.currentTarget.value;
							// dispatch({
							// 	type: "SET_MAP_ID",
							// 	newMapId,
							// });
							setLocation(
								mapsRoute.getHref(
									fileId,
									state.grouping,
									state.selectedGroup.id,
									newMapId
								)
							);
						}}
					>
						{mapIds.length == 0 ? (
							<option>No values available</option>
						) : (
							mapIds.map((mapId) => (
								<option value={mapId}>{formatMapId(mapId)}</option>
							))
						)}
					</select>
				</div>
			</div>
			<p style={{ marginBottom: "1.5rem" }}>
				{showICLocation && (
					<button
						class={`${btn} ${btn_link} ${goto_loc_btn}`}
						style={{ float: "right" }}
						onClick={() => {
							if (state.selectedGroup.group == "loadic") {
								setSelectedEntry(state.selectedGroup.entry);
							}
						}}
					>
						Show IC location
					</button>
				)}
			</p>
			{state.selectedMapId && (
				<MapTimeline
					mapData={mapData}
					selectedEntry={mapData.nodes[state.selectedMapId]}
					selectedPosition={appState.selectedPosition}
					currentFile={fileDeoptInfo.id}
				/>
			)}
		</Fragment>
	);
}
Example #28
Source File: DeoptTables.js    From v8-deopt-viewer with MIT License 4 votes vote down vote up
/**
 * @typedef {import("../..").FileV8DeoptInfoWithSources} FileV8DeoptInfo
 *
 * @typedef DeoptTablesProps
 * @property {FileV8DeoptInfo} fileDeoptInfo
 * @property {"codes" | "deopts" | "ics"} entryKind
 * @property {string} selectedEntryId
 * @property {number} fileId
 * @property {import('../CodeSettings').CodeSettingsState} settings
 * @property {() => void} toggleShowLowSevs
 * @property {boolean} hasMapData
 *
 * @param {DeoptTablesProps} props
 */
export function DeoptTables({
	entryKind,
	selectedEntryId,
	fileDeoptInfo,
	fileId,
	settings,
	toggleShowLowSevs,
	hasMapData,
}) {
	const selectedId = selectedEntryId;

	let totalCount = 0;
	let lowSevCount = 0;
	let hiddenCount = 0;

	/**
	 * @param {import('v8-deopt-parser').Entry} entry
	 */
	function filterEntries(entry) {
		totalCount++;

		if (entry.severity <= MIN_SEVERITY) {
			lowSevCount++;
		}

		if (settings.showLowSevs) {
			return true;
		} else if (entry.severity > MIN_SEVERITY) {
			return true;
		} else {
			hiddenCount++;
			return false;
		}
	}

	let entries;
	if (entryKind == "codes") {
		entries = fileDeoptInfo[entryKind]
			.filter(filterEntries)
			.map((entry) => (
				<CodeEntry
					key={entry.id}
					entry={entry}
					selected={entry.id == selectedId}
					title={
						<EntryTitle
							entry={entry}
							route={codeRoute}
							relativePath={fileDeoptInfo.relativePath}
							fileId={fileId}
						/>
					}
				/>
			));
	} else if (entryKind == "deopts") {
		entries = fileDeoptInfo[entryKind]
			.filter(filterEntries)
			.map((entry) => (
				<DeoptEntry
					key={entry.id}
					entry={entry}
					selected={entry.id == selectedId}
					title={
						<EntryTitle
							entry={entry}
							route={deoptsRoute}
							relativePath={fileDeoptInfo.relativePath}
							fileId={fileId}
						/>
					}
				/>
			));
	} else if (entryKind == "ics") {
		entries = fileDeoptInfo[entryKind]
			.filter(filterEntries)
			.map((entry) => (
				<ICEntry
					key={entry.id}
					entry={entry}
					selected={entry.id == selectedId}
					showAllICs={settings.showAllICs}
					hasMapData={hasMapData}
					fileId={fileId}
					title={
						<EntryTitle
							entry={entry}
							route={icsRoute}
							relativePath={fileDeoptInfo.relativePath}
							fileId={fileId}
						/>
					}
				/>
			));
	} else {
		throw new Error(`Unknown entry kind: "${entryKind}"`);
	}

	let toggleElements = null;
	if (totalCount > 0) {
		let text = `Hiding ${hiddenCount} entries. Show all`;
		if (hiddenCount == 0 && lowSevCount > MIN_SEVERITY) {
			text = `Hide ${lowSevCount} low severity entries.`;
		}

		toggleElements = (
			<p>
				<button
					type="button"
					class={`${btn} ${btn_inline}`}
					onClick={toggleShowLowSevs}
				>
					{text}
				</button>
			</p>
		);
	}

	return (
		<Fragment>
			{entries.length == 0 ? <p>None!</p> : entries}
			{toggleElements}
		</Fragment>
	);
}