Implement quick betting: directly from the market card (#291)

* Play with using 3 icons for 1-click usage

* Align bet icons with the percentages

* Hide liquidity injection star, for now

* Fix Free Response card layouts

* Use triangles instead of planes

* Set correct hover states the arrows

* Fix down triangle & padding

* Default large nums to 2 sigfigs

* Clean up hover areas

* Fix bet width, remove "chance/expected"

* Show "M$20" on hover, hide arrows when closed

* Improve click targets

* FR: "MULTI" => "MANY", single => "TOP"

* Install react-hot-toaster

* Implement quick betting on binary questions

* Handle different kinds of markets

* Extract out QuickBet into its own component

* Minor tweaks

* Visually separate out quick bet pane

* Hide quick bet for FR markets with no answers

* Fill in which bets the user has already placed

* Animate movements, fix binary direction

* Hover arrows are now always gray

* Pull out code into quick-bet.tsx

* Minor comments

* Fix import

ts-ignore is scary

* Fixes from James's feedback

* Hide text only on quickbet
This commit is contained in:
Austin Chen 2022-05-23 23:44:16 -07:00 committed by GitHub
parent a8e47d4fc7
commit 8cedf93901
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 435 additions and 167 deletions

View File

@ -30,17 +30,17 @@ export function formatPercent(zeroToOne: number) {
}
// Eg 1234567.89 => 1.23M; 5678 => 5.68K
export function formatLargeNumber(num: number, sigfigs = 3): string {
export function formatLargeNumber(num: number, sigfigs = 2): string {
const absNum = Math.abs(num)
if (absNum < 1000) {
return num.toPrecision(sigfigs)
return '' + Number(num.toPrecision(sigfigs))
}
const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
const suffixIdx = Math.floor(Math.log10(absNum) / 3)
const suffixStr = suffix[suffixIdx]
const numStr = (num / Math.pow(10, 3 * suffixIdx)).toPrecision(sigfigs)
return `${numStr}${suffixStr}`
return `${Number(numStr)}${suffixStr}`
}
export function toCamelCase(words: string) {

View File

@ -6,7 +6,7 @@ import {
Contract,
contractPath,
getBinaryProbPercent,
getBinaryProb,
listenForContract,
} from 'web/lib/firebase/contracts'
import { Col } from '../layout/col'
import {
@ -22,54 +22,13 @@ import {
AnswerLabel,
BinaryContractOutcomeLabel,
FreeResponseOutcomeLabel,
OUTCOME_TO_COLOR,
} from '../outcome-label'
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
import { AbbrContractDetails } from './contract-details'
import { AvatarDetails, MiscDetails } from './contract-details'
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
// Return a number from 0 to 1 for this contract
// Resolved contracts are set to 1, for coloring purposes (even if NO)
function getProb(contract: Contract) {
const { outcomeType, resolution } = contract
return resolution
? 1
: outcomeType === 'BINARY'
? getBinaryProb(contract)
: outcomeType === 'FREE_RESPONSE'
? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '')
: outcomeType === 'NUMERIC'
? getNumericScale(contract as NumericContract)
: 1 // Should not happen
}
function getNumericScale(contract: NumericContract) {
const { min, max } = contract
const ev = getExpectedValue(contract)
return (ev - min) / (max - min)
}
function getColor(contract: Contract) {
const { resolution } = contract
if (resolution) {
return (
// @ts-ignore; TODO: Have better typing for contract.resolution?
OUTCOME_TO_COLOR[resolution] ||
// If resolved to a FR answer, use 'primary'
'primary'
)
}
if (contract.outcomeType === 'NUMERIC') {
return 'blue-400'
}
const marketClosed = (contract.closeTime || Infinity) < Date.now()
return marketClosed
? 'gray-400'
: getProb(contract) >= 0.5
? 'primary'
: 'red-400'
}
import { useEffect, useState } from 'react'
import { QuickBet, QuickOutcomeView, ProbBar, getColor } from './quick-bet'
import { useContractWithPreload } from 'web/hooks/use-contract'
export function ContractCard(props: {
contract: Contract
@ -77,80 +36,67 @@ export function ContractCard(props: {
showCloseTime?: boolean
className?: string
}) {
const { contract, showHotVolume, showCloseTime, className } = props
const { showHotVolume, showCloseTime, className } = props
const contract = useContractWithPreload(props.contract) ?? props.contract
const { question, outcomeType } = contract
const prob = getProb(contract)
const color = getColor(contract)
const marketClosed = (contract.closeTime || Infinity) < Date.now()
const showTopBar = prob >= 0.5 || marketClosed
const showQuickBet = !(
marketClosed ||
(outcomeType === 'FREE_RESPONSE' && getTopAnswer(contract) === undefined)
)
return (
<div>
<Col
className={clsx(
'relative gap-3 rounded-lg bg-white p-6 pr-7 shadow-md hover:bg-gray-100',
'relative gap-3 rounded-lg bg-white py-4 pl-6 pr-5 shadow-md hover:cursor-pointer hover:bg-gray-100',
className
)}
>
<Link href={contractPath(contract)}>
<a className="absolute left-0 right-0 top-0 bottom-0" />
</Link>
<AbbrContractDetails
contract={contract}
showHotVolume={showHotVolume}
showCloseTime={showCloseTime}
/>
<Row className={clsx('justify-between gap-4')}>
<Col className="gap-3">
<Row className={clsx(showQuickBet ? 'divide-x' : '')}>
<Col className="relative flex-1 gap-3 pr-1">
<div
className={clsx(
'peer absolute -left-6 -top-4 -bottom-4 z-10',
// Hack: Extend the clickable area for closed markets
showQuickBet ? 'right-0' : 'right-[-6.5rem]'
)}
>
<Link href={contractPath(contract)}>
<a className="absolute top-0 left-0 right-0 bottom-0" />
</Link>
</div>
<AvatarDetails contract={contract} />
<p
className="break-words font-medium text-indigo-700"
className="break-words font-medium text-indigo-700 peer-hover:underline peer-hover:decoration-indigo-400 peer-hover:decoration-2"
style={{ /* For iOS safari */ wordBreak: 'break-word' }}
>
{question}
</p>
</Col>
{outcomeType === 'BINARY' && (
<BinaryResolutionOrChance
className="items-center"
contract={contract}
/>
)}
{outcomeType === 'NUMERIC' && (
<NumericResolutionOrExpectation
className="items-center"
contract={contract as NumericContract}
{outcomeType === 'FREE_RESPONSE' && (
<FreeResponseTopAnswer
contract={contract as FullContract<DPM, FreeResponse>}
truncate="long"
/>
)}
<MiscDetails
contract={contract}
showHotVolume={showHotVolume}
showCloseTime={showCloseTime}
/>
</Col>
{showQuickBet ? (
<QuickBet contract={contract} />
) : (
<Col className="m-auto pl-2">
<QuickOutcomeView contract={contract} />
</Col>
)}
</Row>
{outcomeType === 'FREE_RESPONSE' && (
<FreeResponseResolutionOrChance
className="self-end text-gray-600"
contract={contract as FullContract<DPM, FreeResponse>}
truncate="long"
/>
)}
<div
className={clsx(
'absolute right-0 top-0 w-2 rounded-tr-md',
'bg-gray-200'
)}
style={{ height: `${100 * (1 - prob)}%` }}
></div>
<div
className={clsx(
'absolute right-0 bottom-0 w-2 rounded-br-md',
`bg-${color}`,
// If we're showing the full bar, also round the top
prob === 1 ? 'rounded-tr-md' : ''
)}
style={{ height: `${100 * prob}%` }}
></div>
<ProbBar contract={contract} />
</Col>
</div>
)
@ -160,8 +106,9 @@ export function BinaryResolutionOrChance(props: {
contract: FullContract<DPM | CPMM, Binary>
large?: boolean
className?: string
hideText?: boolean
}) {
const { contract, large, className } = props
const { contract, large, className, hideText } = props
const { resolution } = contract
const textColor = `text-${getColor(contract)}`
@ -182,21 +129,42 @@ export function BinaryResolutionOrChance(props: {
) : (
<>
<div className={textColor}>{getBinaryProbPercent(contract)}</div>
<div className={clsx(textColor, large ? 'text-xl' : 'text-base')}>
chance
</div>
{!hideText && (
<div className={clsx(textColor, large ? 'text-xl' : 'text-base')}>
chance
</div>
)}
</>
)}
</Col>
)
}
export function FreeResponseResolutionOrChance(props: {
function FreeResponseTopAnswer(props: {
contract: FreeResponseContract
truncate: 'short' | 'long' | 'none'
className?: string
}) {
const { contract, truncate, className } = props
const { contract, truncate } = props
const topAnswer = getTopAnswer(contract)
return topAnswer ? (
<AnswerLabel
className="!text-gray-600"
answer={topAnswer}
truncate={truncate}
/>
) : null
}
export function FreeResponseResolutionOrChance(props: {
contract: FreeResponseContract
truncate: 'short' | 'long' | 'none'
className?: string
hideText?: boolean
}) {
const { contract, truncate, className, hideText } = props
const { resolution } = contract
const topAnswer = getTopAnswer(contract)
@ -217,16 +185,11 @@ export function FreeResponseResolutionOrChance(props: {
) : (
topAnswer && (
<Row className="items-center gap-6">
<AnswerLabel
className="!text-gray-600"
answer={topAnswer}
truncate={truncate}
/>
<Col className={clsx('text-3xl', textColor)}>
<div>
{formatPercent(getOutcomeProbability(contract, topAnswer.id))}
</div>
<div className="text-base">chance</div>
{!hideText && <div className="text-base">chance</div>}
</Col>
</Row>
)
@ -238,9 +201,11 @@ export function FreeResponseResolutionOrChance(props: {
export function NumericResolutionOrExpectation(props: {
contract: NumericContract
className?: string
hideText?: boolean
}) {
const { contract, className } = props
const { contract, className, hideText } = props
const { resolution } = contract
const textColor = `text-${getColor(contract)}`
const resolutionValue =
contract.resolutionValue ?? getValueFromBucket(resolution ?? '', contract)
@ -254,10 +219,12 @@ export function NumericResolutionOrExpectation(props: {
</>
) : (
<>
<div className="text-3xl text-blue-400">
<div className={clsx('text-3xl', textColor)}>
{formatLargeNumber(getExpectedValue(contract))}
</div>
<div className="text-base text-blue-400">expected</div>
{!hideText && (
<div className={clsx('text-base', textColor)}>expected</div>
)}
</>
)}
</Col>

View File

@ -5,7 +5,9 @@ import {
PencilIcon,
CurrencyDollarIcon,
TrendingUpIcon,
StarIcon,
} from '@heroicons/react/outline'
import { StarIcon as SolidStarIcon } from '@heroicons/react/solid'
import { Row } from '../layout/row'
import { formatMoney } from 'common/util/format'
import { UserLink } from '../user-page'
@ -26,20 +28,13 @@ import NewContractBadge from '../new-contract-badge'
import { CATEGORY_LIST } from 'common/categories'
import { TagsList } from '../tags-list'
export function AbbrContractDetails(props: {
export function MiscDetails(props: {
contract: Contract
showHotVolume?: boolean
showCloseTime?: boolean
}) {
const { contract, showHotVolume, showCloseTime } = props
const {
volume,
volume24Hours,
creatorName,
creatorUsername,
closeTime,
tags,
} = contract
const { volume, volume24Hours, closeTime, tags } = contract
const { volumeLabel } = contractMetrics(contract)
// Show at most one category that this contract is tagged by
const categories = CATEGORY_LIST.filter((category) =>
@ -47,41 +42,62 @@ export function AbbrContractDetails(props: {
).slice(0, 1)
return (
<Col className={clsx('gap-2 text-sm text-gray-500')}>
<Row className="items-center justify-between">
<Row className="items-center gap-2">
<Avatar
username={creatorUsername}
avatarUrl={contract.creatorAvatarUrl}
size={6}
/>
<UserLink name={creatorName} username={creatorUsername} />
</Row>
<Row className="items-center gap-3 text-sm text-gray-400">
{categories.length > 0 && (
<TagsList className="text-gray-400" tags={categories} noLabel />
)}
<Row className="gap-3 text-gray-400">
{categories.length > 0 && (
<TagsList className="text-gray-400" tags={categories} noLabel />
)}
{showHotVolume ? (
<Row className="gap-0.5">
<TrendingUpIcon className="h-5 w-5" />{' '}
{formatMoney(volume24Hours)}
</Row>
) : showCloseTime ? (
<Row className="gap-0.5">
<ClockIcon className="h-5 w-5" />
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
{fromNow(closeTime || 0)}
</Row>
) : volume > 0 ? (
<Row>{volumeLabel}</Row>
) : (
<NewContractBadge />
)}
{showHotVolume ? (
<Row className="gap-0.5">
<TrendingUpIcon className="h-5 w-5" /> {formatMoney(volume24Hours)}
</Row>
</Row>
</Col>
) : showCloseTime ? (
<Row className="gap-0.5">
<ClockIcon className="h-5 w-5" />
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
{fromNow(closeTime || 0)}
</Row>
) : volume > 0 ? (
<Row>{volumeLabel}</Row>
) : (
<NewContractBadge />
)}
</Row>
)
}
export function AvatarDetails(props: { contract: Contract }) {
const { contract } = props
const { creatorName, creatorUsername } = contract
return (
<Row className="items-center gap-2 text-sm text-gray-500">
<Avatar
username={creatorUsername}
avatarUrl={contract.creatorAvatarUrl}
size={6}
/>
<UserLink name={creatorName} username={creatorUsername} />
</Row>
)
}
export function AbbrContractDetails(props: {
contract: Contract
showHotVolume?: boolean
showCloseTime?: boolean
}) {
const { contract, showHotVolume, showCloseTime } = props
return (
<Row className="items-center justify-between">
<AvatarDetails contract={contract} />
<MiscDetails
contract={contract}
showHotVolume={showHotVolume}
showCloseTime={showCloseTime}
/>
</Row>
)
}
@ -93,7 +109,7 @@ export function ContractDetails(props: {
}) {
const { contract, bets, isCreator, disabled } = props
const { closeTime, creatorName, creatorUsername } = contract
const { volumeLabel, createdDate, resolvedDate } = contractMetrics(contract)
const { volumeLabel, resolvedDate } = contractMetrics(contract)
return (
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500">

View File

@ -0,0 +1,241 @@
import clsx from 'clsx'
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
import { getExpectedValue } from 'common/calculate-dpm'
import {
Contract,
FullContract,
CPMM,
DPM,
Binary,
NumericContract,
FreeResponse,
} from 'common/contract'
import { formatMoney } from 'common/util/format'
import toast from 'react-hot-toast'
import { useUser } from 'web/hooks/use-user'
import { useUserContractBets } from 'web/hooks/use-user-bets'
import { placeBet } from 'web/lib/firebase/api-call'
import { getBinaryProb } from 'web/lib/firebase/contracts'
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
import { Col } from '../layout/col'
import { OUTCOME_TO_COLOR } from '../outcome-label'
import { useSaveShares } from '../use-save-shares'
import {
BinaryResolutionOrChance,
NumericResolutionOrExpectation,
FreeResponseResolutionOrChance,
} from './contract-card'
export function QuickBet(props: { contract: Contract }) {
const { contract } = props
const user = useUser()
const userBets = useUserContractBets(user?.id, contract.id)
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
contract as FullContract<CPMM | DPM, Binary>,
userBets
)
// TODO: For some reason, Floor Shares are inverted for non-BINARY markets
const hasUpShares =
contract.outcomeType === 'BINARY' ? yesFloorShares : noFloorShares
const hasDownShares =
contract.outcomeType === 'BINARY' ? noFloorShares : yesFloorShares
const color = getColor(contract)
async function placeQuickBet(direction: 'UP' | 'DOWN') {
const betPromise = async () => {
const outcome = quickOutcome(contract, direction)
return await placeBet({
amount: 10,
outcome,
contractId: contract.id,
})
}
const shortQ = contract.question.slice(0, 20)
toast.promise(betPromise(), {
loading: `${formatMoney(10)} on "${shortQ}"...`,
success: `${formatMoney(10)} on "${shortQ}"...`,
error: (err) => `${err.message}`,
})
}
function quickOutcome(contract: Contract, direction: 'UP' | 'DOWN') {
if (contract.outcomeType === 'BINARY') {
return direction === 'UP' ? 'YES' : 'NO'
}
if (contract.outcomeType === 'FREE_RESPONSE') {
// TODO: Implement shorting of free response answers
if (direction === 'DOWN') {
throw new Error("Can't short free response answers")
}
return getTopAnswer(contract)?.id
}
if (contract.outcomeType === 'NUMERIC') {
// TODO: Ideally an 'UP' bet would be a uniform bet between [current, max]
throw new Error("Can't quick bet on numeric markets")
}
}
return (
<Col
className={clsx(
'relative -my-4 -mr-5 min-w-[6rem] justify-center gap-2 pr-5 pl-3 align-middle',
// Use this for colored QuickBet panes
// `bg-opacity-10 bg-${color}`
'bg-gray-50'
)}
>
{/* Up bet triangle */}
<div>
<div
className="peer absolute top-0 left-0 right-0 h-[50%]"
onClick={() => placeQuickBet('UP')}
></div>
<div className="mt-2 text-center text-xs text-transparent peer-hover:text-gray-400">
{formatMoney(10)}
</div>
{hasUpShares > 0 ? (
<TriangleFillIcon
className={clsx(
'mx-auto h-5 w-5',
`text-${color} text-opacity-70 peer-hover:text-gray-400`
)}
/>
) : (
<TriangleFillIcon className="mx-auto h-5 w-5 text-gray-200 peer-hover:text-gray-400" />
)}
</div>
<QuickOutcomeView contract={contract} />
{/* Down bet triangle */}
<div>
<div
className="peer absolute bottom-0 left-0 right-0 h-[50%]"
onClick={() => placeQuickBet('DOWN')}
></div>
{hasDownShares > 0 ? (
<TriangleDownFillIcon
className={clsx(
'mx-auto h-5 w-5',
`text-${color} text-opacity-70 peer-hover:text-gray-400`
)}
/>
) : (
<TriangleDownFillIcon className="mx-auto h-5 w-5 text-gray-200 peer-hover:text-gray-400" />
)}
<div className="mb-2 text-center text-xs text-transparent peer-hover:text-gray-400">
{formatMoney(10)}
</div>
</div>
</Col>
)
}
export function ProbBar(props: { contract: Contract }) {
const { contract } = props
const color = getColor(contract)
const prob = getProb(contract)
return (
<>
<div
className={clsx(
'absolute right-0 top-0 w-2 rounded-tr-md transition-all',
'bg-gray-200'
)}
style={{ height: `${100 * (1 - prob)}%` }}
></div>
<div
className={clsx(
'absolute right-0 bottom-0 w-2 rounded-br-md transition-all',
`bg-${color}`,
// If we're showing the full bar, also round the top
prob === 1 ? 'rounded-tr-md' : ''
)}
style={{ height: `${100 * prob}%` }}
></div>
</>
)
}
export function QuickOutcomeView(props: { contract: Contract }) {
const { contract } = props
const { outcomeType } = contract
return (
<>
{outcomeType === 'BINARY' && (
<BinaryResolutionOrChance
className="items-center"
contract={contract}
hideText
/>
)}
{outcomeType === 'NUMERIC' && (
<NumericResolutionOrExpectation
className="items-center"
contract={contract as NumericContract}
hideText
/>
)}
{outcomeType === 'FREE_RESPONSE' && (
<FreeResponseResolutionOrChance
className="self-end text-gray-600"
contract={contract as FullContract<DPM, FreeResponse>}
truncate="long"
hideText
/>
)}
</>
)
}
// Return a number from 0 to 1 for this contract
// Resolved contracts are set to 1, for coloring purposes (even if NO)
function getProb(contract: Contract) {
const { outcomeType, resolution } = contract
return resolution
? 1
: outcomeType === 'BINARY'
? getBinaryProb(contract)
: outcomeType === 'FREE_RESPONSE'
? getOutcomeProbability(contract, getTopAnswer(contract)?.id || '')
: outcomeType === 'NUMERIC'
? getNumericScale(contract as NumericContract)
: 1 // Should not happen
}
function getNumericScale(contract: NumericContract) {
const { min, max } = contract
const ev = getExpectedValue(contract)
return (ev - min) / (max - min)
}
export function getColor(contract: Contract) {
// TODO: Not sure why eg green-400 doesn't work here; try upgrading Tailwind
// TODO: Try injecting a gradient here
// return 'primary'
const { resolution } = contract
if (resolution) {
return (
// @ts-ignore; TODO: Have better typing for contract.resolution?
OUTCOME_TO_COLOR[resolution] ||
// If resolved to a FR answer, use 'primary'
'primary'
)
}
if (contract.outcomeType === 'NUMERIC') {
return 'blue-400'
}
const marketClosed = (contract.closeTime || Infinity) < Date.now()
return marketClosed
? 'gray-400'
: getProb(contract) >= 0.5
? 'primary'
: 'red-400'
}

View File

@ -84,11 +84,7 @@ export function FreeResponseOutcomeLabel(props: {
if (!chosen) return <AnswerNumberLabel number={resolution} />
return (
<FreeResponseAnswerToolTip text={chosen.text}>
<AnswerLabel
answer={chosen}
truncate={truncate}
className={answerClassName}
/>
<span className="text-blue-400">TOP</span>
</FreeResponseAnswerToolTip>
)
}
@ -117,7 +113,7 @@ export function ProbLabel() {
}
export function MultiLabel() {
return <span className="text-blue-400">MULTI</span>
return <span className="text-blue-400">MANY</span>
}
export function ProbPercentLabel(props: { prob: number }) {

View File

@ -1,6 +1,7 @@
import clsx from 'clsx'
import { BottomNavBar } from './nav/nav-bar'
import Sidebar from './nav/sidebar'
import { Toaster } from 'react-hot-toast'
export function Page(props: {
margin?: boolean
@ -20,6 +21,7 @@ export function Page(props: {
)}
style={suspend ? visuallyHiddenStyle : undefined}
>
<Toaster />
<Sidebar className="sticky top-4 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:block" />
<main
className={clsx(

View File

@ -0,0 +1,17 @@
// Icon from Bootstrap: https://icons.getbootstrap.com/
export default function TriangleDownFillIcon(props: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className={props.className}
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
transform="rotate(-180 8 8)"
d="M7.022 1.566a1.13 1.13 0 0 1 1.96 0l6.857 11.667c.457.778-.092 1.767-.98 1.767H1.144c-.889 0-1.437-.99-.98-1.767L7.022 1.566z"
/>
</svg>
)
}

View File

@ -0,0 +1,16 @@
// Icon from Bootstrap: https://icons.getbootstrap.com/
export default function TriangleFillIcon(props: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className={props.className}
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M7.022 1.566a1.13 1.13 0 0 1 1.96 0l6.857 11.667c.457.778-.092 1.767-.98 1.767H1.144c-.889 0-1.437-.99-.98-1.767L7.022 1.566z"
/>
</svg>
)
}

View File

@ -35,6 +35,7 @@
"react-confetti": "6.0.1",
"react-dom": "17.0.2",
"react-expanding-textarea": "2.3.5",
"react-hot-toast": "^2.2.0",
"react-instantsearch-hooks-web": "6.24.1"
},
"devDependencies": {

View File

@ -2974,6 +2974,11 @@ globby@^11.1.0:
merge2 "^1.4.1"
slash "^3.0.0"
goober@^2.1.1:
version "2.1.9"
resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.9.tgz#0faee08fab1a5d55b23e9ec043bb5a1b46fa025a"
integrity sha512-PAtnJbrWtHbfpJUIveG5PJIB6Mc9Kd0gimu9wZwPyA+wQUSeOeA4x4Ug16lyaaUUKZ/G6QEH1xunKOuXP1F4Vw==
google-auth-library@^7.14.0, google-auth-library@^7.14.1:
version "7.14.1"
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.14.1.tgz#e3483034162f24cc71b95c8a55a210008826213c"
@ -4501,6 +4506,13 @@ react-expanding-textarea@2.3.5:
react-with-forwarded-ref "^0.3.3"
tslib "^2.0.3"
react-hot-toast@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.2.0.tgz#ab6f4caed4214b9534f94bb8cfaaf21b051e62b9"
integrity sha512-248rXw13uhf/6TNDVzagX+y7R8J183rp7MwUMNkcrBRyHj/jWOggfXTGlM8zAOuh701WyVW+eUaWG2LeSufX9g==
dependencies:
goober "^2.1.1"
react-instantsearch-hooks-web@6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/react-instantsearch-hooks-web/-/react-instantsearch-hooks-web-6.24.1.tgz#392be70c584583f3cd9fe22eda5a59f7449e5ac9"