Merge branch 'main' into new-home

This commit is contained in:
James Grugett 2022-09-11 19:37:37 -05:00
commit 299f2f24c6
47 changed files with 673 additions and 417 deletions

View File

@ -73,6 +73,7 @@ export const PROD_CONFIG: EnvConfig = {
'manticmarkets@gmail.com', // Manifold 'manticmarkets@gmail.com', // Manifold
'iansphilips@gmail.com', // Ian 'iansphilips@gmail.com', // Ian
'd4vidchee@gmail.com', // D4vid 'd4vidchee@gmail.com', // D4vid
'federicoruizcassarino@gmail.com', // Fede
], ],
visibility: 'PUBLIC', visibility: 'PUBLIC',

View File

@ -13,7 +13,6 @@ import { addObjects } from './util/object'
export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => { export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => {
const { pool } = contract const { pool } = contract
const poolTotal = sum(Object.values(pool)) const poolTotal = sum(Object.values(pool))
console.log('resolved N/A, pool M$', poolTotal)
const betSum = sumBy(bets, (b) => b.amount) const betSum = sumBy(bets, (b) => b.amount)
@ -58,17 +57,6 @@ export const getDpmStandardPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved',
outcome,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -110,17 +98,6 @@ export const getNumericDpmPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved numeric bucket: ',
outcome,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -163,17 +140,6 @@ export const getDpmMktPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved MKT',
p,
'pool',
pool,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -216,16 +182,6 @@ export const getPayoutsMultiOutcome = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved',
resolutions,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,

View File

@ -1,4 +1,3 @@
import { sum } from 'lodash'
import { Bet } from './bet' import { Bet } from './bet'
import { getProbability } from './calculate' import { getProbability } from './calculate'
@ -43,18 +42,6 @@ export const getStandardFixedPayouts = (
const { collectedFees } = contract const { collectedFees } = contract
const creatorPayout = collectedFees.creatorFee const creatorPayout = collectedFees.creatorFee
console.log(
'resolved',
outcome,
'pool',
contract.pool[outcome],
'payouts',
sum(payouts),
'creator fee',
creatorPayout
)
const liquidityPayouts = getLiquidityPoolPayouts( const liquidityPayouts = getLiquidityPoolPayouts(
contract, contract,
outcome, outcome,
@ -98,18 +85,6 @@ export const getMktFixedPayouts = (
const { collectedFees } = contract const { collectedFees } = contract
const creatorPayout = collectedFees.creatorFee const creatorPayout = collectedFees.creatorFee
console.log(
'resolved PROB',
p,
'pool',
p * contract.pool.YES + (1 - p) * contract.pool.NO,
'payouts',
sum(payouts),
'creator fee',
creatorPayout
)
const liquidityPayouts = getLiquidityPoolProbPayouts(contract, p, liquidities) const liquidityPayouts = getLiquidityPoolProbPayouts(contract, p, liquidities)
return { payouts, creatorPayout, liquidityPayouts, collectedFees } return { payouts, creatorPayout, liquidityPayouts, collectedFees }

View File

@ -13,7 +13,10 @@ export const getRedeemableAmount = (bets: RedeemableBet[]) => {
const yesShares = sumBy(yesBets, (b) => b.shares) const yesShares = sumBy(yesBets, (b) => b.shares)
const noShares = sumBy(noBets, (b) => b.shares) const noShares = sumBy(noBets, (b) => b.shares)
const shares = Math.max(Math.min(yesShares, noShares), 0) const shares = Math.max(Math.min(yesShares, noShares), 0)
const soldFrac = shares > 0 ? Math.min(yesShares, noShares) / shares : 0 const soldFrac =
shares > 0
? Math.min(yesShares, noShares) / Math.max(yesShares, noShares)
: 0
const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0) const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
const loanPayment = loanAmount * soldFrac const loanPayment = loanAmount * soldFrac
const netAmount = shares - loanPayment const netAmount = shares - loanPayment

View File

@ -1,8 +1,8 @@
import { groupBy, sumBy, mapValues, partition } from 'lodash' import { groupBy, sumBy, mapValues } from 'lodash'
import { Bet } from './bet' import { Bet } from './bet'
import { getContractBetMetrics } from './calculate'
import { Contract } from './contract' import { Contract } from './contract'
import { getPayouts } from './payouts'
export function scoreCreators(contracts: Contract[]) { export function scoreCreators(contracts: Contract[]) {
const creatorScore = mapValues( const creatorScore = mapValues(
@ -30,46 +30,8 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
} }
export function scoreUsersByContract(contract: Contract, bets: Bet[]) { export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
const { resolution } = contract const betsByUser = groupBy(bets, bet => bet.userId)
const resolutionProb = return mapValues(betsByUser, bets => getContractBetMetrics(contract, bets).profit)
contract.outcomeType == 'BINARY'
? contract.resolutionProbability
: undefined
const [closedBets, openBets] = partition(
bets,
(bet) => bet.isSold || bet.sale
)
const { payouts: resolvePayouts } = getPayouts(
resolution as string,
contract,
openBets,
[],
{},
resolutionProb
)
const salePayouts = closedBets.map((bet) => {
const { userId, sale } = bet
return { userId, payout: sale ? sale.amount : 0 }
})
const investments = bets
.filter((bet) => !bet.sale)
.map((bet) => {
const { userId, amount, loanAmount } = bet
const payout = -amount - (loanAmount ?? 0)
return { userId, payout }
})
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
const userScore = mapValues(
groupBy(netPayouts, (payout) => payout.userId),
(payouts) => sumBy(payouts, ({ payout }) => payout)
)
return userScore
} }
export function addUserScores( export function addUserScores(

View File

@ -60,23 +60,27 @@ Parameters:
Requires no authorization. Requires no authorization.
### `GET /v0/groups/[slug]` ### `GET /v0/group/[slug]`
Gets a group by its slug. Gets a group by its slug.
Requires no authorization. Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/group/by-id/[id]` ### `GET /v0/group/by-id/[id]`
Gets a group by its unique ID. Gets a group by its unique ID.
Requires no authorization. Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/group/by-id/[id]/markets` ### `GET /v0/group/by-id/[id]/markets`
Gets a group's markets by its unique ID. Gets a group's markets by its unique ID.
Requires no authorization. Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/markets` ### `GET /v0/markets`

View File

@ -12,7 +12,9 @@ service cloud.firestore {
'taowell@gmail.com', 'taowell@gmail.com',
'abc.sinclair@gmail.com', 'abc.sinclair@gmail.com',
'manticmarkets@gmail.com', 'manticmarkets@gmail.com',
'iansphilips@gmail.com' 'iansphilips@gmail.com',
'd4vidchee@gmail.com',
'federicoruizcassarino@gmail.com'
] ]
} }

View File

@ -37,6 +37,45 @@ export const changeUser = async (
avatarUrl?: string avatarUrl?: string
} }
) => { ) => {
// Update contracts, comments, and answers outside of a transaction to avoid contention.
// Using bulkWriter to supports >500 writes at a time
const contractsRef = firestore
.collection('contracts')
.where('creatorId', '==', user.id)
const contracts = await contractsRef.get()
const contractUpdate: Partial<Contract> = removeUndefinedProps({
creatorName: update.name,
creatorUsername: update.username,
creatorAvatarUrl: update.avatarUrl,
})
const commentSnap = await firestore
.collectionGroup('comments')
.where('userUsername', '==', user.username)
.get()
const commentUpdate: Partial<Comment> = removeUndefinedProps({
userName: update.name,
userUsername: update.username,
userAvatarUrl: update.avatarUrl,
})
const answerSnap = await firestore
.collectionGroup('answers')
.where('username', '==', user.username)
.get()
const answerUpdate: Partial<Answer> = removeUndefinedProps(update)
const bulkWriter = firestore.bulkWriter()
commentSnap.docs.forEach((d) => bulkWriter.update(d.ref, commentUpdate))
answerSnap.docs.forEach((d) => bulkWriter.update(d.ref, answerUpdate))
contracts.docs.forEach((d) => bulkWriter.update(d.ref, contractUpdate))
await bulkWriter.flush()
console.log('Done writing!')
// Update the username inside a transaction
return await firestore.runTransaction(async (transaction) => { return await firestore.runTransaction(async (transaction) => {
if (update.username) { if (update.username) {
update.username = cleanUsername(update.username) update.username = cleanUsername(update.username)
@ -58,42 +97,7 @@ export const changeUser = async (
const userRef = firestore.collection('users').doc(user.id) const userRef = firestore.collection('users').doc(user.id)
const userUpdate: Partial<User> = removeUndefinedProps(update) const userUpdate: Partial<User> = removeUndefinedProps(update)
const contractsRef = firestore
.collection('contracts')
.where('creatorId', '==', user.id)
const contracts = await transaction.get(contractsRef)
const contractUpdate: Partial<Contract> = removeUndefinedProps({
creatorName: update.name,
creatorUsername: update.username,
creatorAvatarUrl: update.avatarUrl,
})
const commentSnap = await transaction.get(
firestore
.collectionGroup('comments')
.where('userUsername', '==', user.username)
)
const commentUpdate: Partial<Comment> = removeUndefinedProps({
userName: update.name,
userUsername: update.username,
userAvatarUrl: update.avatarUrl,
})
const answerSnap = await transaction.get(
firestore
.collectionGroup('answers')
.where('username', '==', user.username)
)
const answerUpdate: Partial<Answer> = removeUndefinedProps(update)
transaction.update(userRef, userUpdate) transaction.update(userRef, userUpdate)
commentSnap.docs.forEach((d) => transaction.update(d.ref, commentUpdate))
answerSnap.docs.forEach((d) => transaction.update(d.ref, answerUpdate))
contracts.docs.forEach((d) => transaction.update(d.ref, contractUpdate))
}) })
} }

View File

@ -186,8 +186,9 @@
font-family: Readex Pro, Arial, Helvetica, font-family: Readex Pro, Arial, Helvetica,
sans-serif; sans-serif;
font-size: 17px; font-size: 17px;
">Did you know you create your own prediction market on <a class="link-build-content" ">Did you know you can create your own prediction market on <a
style="color: #55575d" target="_blank" href="https://manifold.markets">Manifold</a> for class="link-build-content" style="color: #55575d" target="_blank"
href="https://manifold.markets">Manifold</a> on
any question you care about?</span> any question you care about?</span>
</p> </p>

View File

@ -122,6 +122,18 @@ export function BuyAmountInput(props: {
} }
} }
const parseRaw = (x: number) => {
if (x <= 100) return x
if (x <= 130) return 100 + (x - 100) * 5
return 250 + (x - 130) * 10
}
const getRaw = (x: number) => {
if (x <= 100) return x
if (x <= 250) return 100 + (x - 100) / 5
return 130 + (x - 250) / 10
}
return ( return (
<> <>
<AmountInput <AmountInput
@ -138,10 +150,10 @@ export function BuyAmountInput(props: {
<input <input
type="range" type="range"
min="0" min="0"
max="200" max="205"
value={amount ?? 0} value={getRaw(amount ?? 0)}
onChange={(e) => onAmountChange(parseInt(e.target.value))} onChange={(e) => onAmountChange(parseRaw(parseInt(e.target.value)))}
className="range range-lg z-40 mb-2 xl:hidden" className="range range-lg only-thumb z-40 mb-2 xl:hidden"
step="5" step="5"
/> />
)} )}

View File

@ -1,5 +1,5 @@
import clsx from 'clsx' import clsx from 'clsx'
import React, { useEffect, useRef, useState } from 'react' import React, { useState } from 'react'
import { XIcon } from '@heroicons/react/solid' import { XIcon } from '@heroicons/react/solid'
import { Answer } from 'common/answer' import { Answer } from 'common/answer'
@ -25,8 +25,7 @@ import {
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { BetSignUpPrompt } from '../sign-up-prompt' import { BetSignUpPrompt } from '../sign-up-prompt'
import { isIOS } from 'web/lib/util/device' import { WarningConfirmationButton } from '../warning-confirmation-button'
import { AlertBox } from '../alert-box'
export function AnswerBetPanel(props: { export function AnswerBetPanel(props: {
answer: Answer answer: Answer
@ -44,12 +43,6 @@ export function AnswerBetPanel(props: {
const [error, setError] = useState<string | undefined>() const [error, setError] = useState<string | undefined>()
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const inputRef = useRef<HTMLElement>(null)
useEffect(() => {
if (isIOS()) window.scrollTo(0, window.scrollY + 200)
inputRef.current && inputRef.current.focus()
}, [])
async function submitBet() { async function submitBet() {
if (!user || !betAmount) return if (!user || !betAmount) return
@ -116,6 +109,15 @@ export function AnswerBetPanel(props: {
const bankrollFraction = (betAmount ?? 0) / (user?.balance ?? 1e9) const bankrollFraction = (betAmount ?? 0) / (user?.balance ?? 1e9)
const warning =
(betAmount ?? 0) >= 100 && bankrollFraction >= 0.5 && bankrollFraction <= 1
? `You might not want to spend ${formatPercent(
bankrollFraction
)} of your balance on a single bet. \n\nCurrent balance: ${formatMoney(
user?.balance ?? 0
)}`
: undefined
return ( return (
<Col className={clsx('px-2 pb-2 pt-4 sm:pt-0', className)}> <Col className={clsx('px-2 pb-2 pt-4 sm:pt-0', className)}>
<Row className="items-center justify-between self-stretch"> <Row className="items-center justify-between self-stretch">
@ -144,25 +146,9 @@ export function AnswerBetPanel(props: {
error={error} error={error}
setError={setError} setError={setError}
disabled={isSubmitting} disabled={isSubmitting}
inputRef={inputRef}
showSliderOnMobile showSliderOnMobile
/> />
{(betAmount ?? 0) > 10 &&
bankrollFraction >= 0.5 &&
bankrollFraction <= 1 ? (
<AlertBox
title="Whoa, there!"
text={`You might not want to spend ${formatPercent(
bankrollFraction
)} of your balance on a single bet. \n\nCurrent balance: ${formatMoney(
user?.balance ?? 0
)}`}
/>
) : (
''
)}
<Col className="mt-3 w-full gap-3"> <Col className="mt-3 w-full gap-3">
<Row className="items-center justify-between text-sm"> <Row className="items-center justify-between text-sm">
<div className="text-gray-500">Probability</div> <div className="text-gray-500">Probability</div>
@ -198,16 +184,17 @@ export function AnswerBetPanel(props: {
<Spacer h={6} /> <Spacer h={6} />
{user ? ( {user ? (
<button <WarningConfirmationButton
className={clsx( warning={warning}
onSubmit={submitBet}
isSubmitting={isSubmitting}
disabled={!!betDisabled}
openModalButtonClass={clsx(
'btn self-stretch', 'btn self-stretch',
betDisabled ? 'btn-disabled' : 'btn-primary', betDisabled ? 'btn-disabled' : 'btn-primary',
isSubmitting ? 'loading' : '' isSubmitting ? 'loading' : ''
)} )}
onClick={betDisabled ? undefined : submitBet} />
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
) : ( ) : (
<BetSignUpPrompt /> <BetSignUpPrompt />
)} )}

View File

@ -194,7 +194,7 @@ function OpenAnswer(props: {
return ( return (
<Col className={'border-base-200 bg-base-200 flex-1 rounded-md px-2'}> <Col className={'border-base-200 bg-base-200 flex-1 rounded-md px-2'}>
<Modal open={open} setOpen={setOpen}> <Modal open={open} setOpen={setOpen} position="center">
<AnswerBetPanel <AnswerBetPanel
answer={answer} answer={answer}
contract={contract} contract={contract}

View File

@ -41,7 +41,7 @@ export default function BetButton(props: {
)} )}
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
> >
Trade Predict
</Button> </Button>
) : ( ) : (
<BetSignUpPrompt /> <BetSignUpPrompt />
@ -60,7 +60,7 @@ export default function BetButton(props: {
)} )}
</Col> </Col>
<Modal open={open} setOpen={setOpen}> <Modal open={open} setOpen={setOpen} position="center">
<SimpleBetPanel <SimpleBetPanel
className={betPanelClassName} className={betPanelClassName}
contract={contract} contract={contract}

View File

@ -40,7 +40,8 @@ import { LimitBets } from './limit-bets'
import { PillButton } from './buttons/pill-button' import { PillButton } from './buttons/pill-button'
import { YesNoSelector } from './yes-no-selector' import { YesNoSelector } from './yes-no-selector'
import { PlayMoneyDisclaimer } from './play-money-disclaimer' import { PlayMoneyDisclaimer } from './play-money-disclaimer'
import { AlertBox } from './alert-box' import { isAndroid, isIOS } from 'web/lib/util/device'
import { WarningConfirmationButton } from './warning-confirmation-button'
export function BetPanel(props: { export function BetPanel(props: {
contract: CPMMBinaryContract | PseudoNumericContract contract: CPMMBinaryContract | PseudoNumericContract
@ -184,17 +185,13 @@ function BuyPanel(props: {
const [inputRef, focusAmountInput] = useFocus() const [inputRef, focusAmountInput] = useFocus()
// useEffect(() => {
// if (selected) {
// if (isIOS()) window.scrollTo(0, window.scrollY + 200)
// focusAmountInput()
// }
// }, [selected, focusAmountInput])
function onBetChoice(choice: 'YES' | 'NO') { function onBetChoice(choice: 'YES' | 'NO') {
setOutcome(choice) setOutcome(choice)
setWasSubmitted(false) setWasSubmitted(false)
focusAmountInput()
if (!isIOS() && !isAndroid()) {
focusAmountInput()
}
} }
function onBetChange(newAmount: number | undefined) { function onBetChange(newAmount: number | undefined) {
@ -274,30 +271,20 @@ function BuyPanel(props: {
const bankrollFraction = (betAmount ?? 0) / (user?.balance ?? 1e9) const bankrollFraction = (betAmount ?? 0) / (user?.balance ?? 1e9)
const warning = const warning =
(betAmount ?? 0) > 10 && (betAmount ?? 0) >= 100 && bankrollFraction >= 0.5 && bankrollFraction <= 1
bankrollFraction >= 0.5 && ? `You might not want to spend ${formatPercent(
bankrollFraction <= 1 ? (
<AlertBox
title="Whoa, there!"
text={`You might not want to spend ${formatPercent(
bankrollFraction bankrollFraction
)} of your balance on a single trade. \n\nCurrent balance: ${formatMoney( )} of your balance on a single trade. \n\nCurrent balance: ${formatMoney(
user?.balance ?? 0 user?.balance ?? 0
)}`} )}`
/> : (betAmount ?? 0) > 10 && probChange >= 0.3 && bankrollFraction <= 1
) : (betAmount ?? 0) > 10 && probChange >= 0.3 && bankrollFraction <= 1 ? ( ? `Are you sure you want to move the market by ${displayedDifference}?`
<AlertBox : undefined
title="Whoa, there!"
text={`Are you sure you want to move the market by ${displayedDifference}?`}
/>
) : (
<></>
)
return ( return (
<Col className={hidden ? 'hidden' : ''}> <Col className={hidden ? 'hidden' : ''}>
<div className="my-3 text-left text-sm text-gray-500"> <div className="my-3 text-left text-sm text-gray-500">
{isPseudoNumeric ? 'Direction' : 'Buy'} {isPseudoNumeric ? 'Direction' : 'Outcome'}
</div> </div>
<YesNoSelector <YesNoSelector
className="mb-4" className="mb-4"
@ -325,8 +312,6 @@ function BuyPanel(props: {
showSliderOnMobile showSliderOnMobile
/> />
{warning}
<Col className="mt-3 w-full gap-3"> <Col className="mt-3 w-full gap-3">
<Row className="items-center justify-between text-sm"> <Row className="items-center justify-between text-sm">
<div className="text-gray-500"> <div className="text-gray-500">
@ -367,20 +352,20 @@ function BuyPanel(props: {
<Spacer h={8} /> <Spacer h={8} />
{user && ( {user && (
<button <WarningConfirmationButton
className={clsx( warning={warning}
onSubmit={submitBet}
isSubmitting={isSubmitting}
disabled={!!betDisabled}
openModalButtonClass={clsx(
'btn mb-2 flex-1', 'btn mb-2 flex-1',
betDisabled betDisabled
? 'btn-disabled' ? 'btn-disabled'
: outcome === 'YES' : outcome === 'YES'
? 'btn-primary' ? 'btn-primary'
: 'border-none bg-red-400 hover:bg-red-500', : 'border-none bg-red-400 hover:bg-red-500'
isSubmitting ? 'loading' : ''
)} )}
onClick={betDisabled ? undefined : submitBet} />
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)} )}
{wasSubmitted && <div className="mt-4">Trade submitted!</div>} {wasSubmitted && <div className="mt-4">Trade submitted!</div>}
@ -750,9 +735,7 @@ function QuickOrLimitBet(props: {
return ( return (
<Row className="align-center mb-4 justify-between"> <Row className="align-center mb-4 justify-between">
<div className="mr-2 -ml-2 shrink-0 text-3xl sm:-ml-0 sm:text-4xl"> <div className="mr-2 -ml-2 shrink-0 text-3xl sm:-ml-0">Predict</div>
Trade
</div>
{!hideToggle && ( {!hideToggle && (
<Row className="mt-1 ml-1 items-center gap-1.5 sm:ml-0 sm:gap-2"> <Row className="mt-1 ml-1 items-center gap-1.5 sm:ml-0 sm:gap-2">
<PillButton <PillButton

View File

@ -13,8 +13,8 @@ export function PillButton(props: {
return ( return (
<button <button
className={clsx( className={clsx(
'cursor-pointer select-none whitespace-nowrap rounded-full px-3 py-1.5 sm:text-sm', 'cursor-pointer select-none whitespace-nowrap rounded-full px-3 py-1.5 text-sm',
xs ? 'text-xs' : 'text-sm', xs ? 'text-xs' : '',
selected selected
? ['text-white', color ?? 'bg-greyscale-6'] ? ['text-white', color ?? 'bg-greyscale-6']
: 'bg-greyscale-2 hover:bg-greyscale-3' : 'bg-greyscale-2 hover:bg-greyscale-3'

View File

@ -4,7 +4,6 @@ import clsx from 'clsx'
import { User } from 'common/user' import { User } from 'common/user'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { useWindowSize } from 'web/hooks/use-window-size'
import { MAX_COMMENT_LENGTH } from 'web/lib/firebase/comments' import { MAX_COMMENT_LENGTH } from 'web/lib/firebase/comments'
import { Avatar } from './avatar' import { Avatar } from './avatar'
import { TextEditor, useTextEditor } from './editor' import { TextEditor, useTextEditor } from './editor'
@ -80,7 +79,6 @@ export function CommentInputTextArea(props: {
upload: Parameters<typeof TextEditor>[0]['upload'] upload: Parameters<typeof TextEditor>[0]['upload']
submitComment: (id?: string) => void submitComment: (id?: string) => void
isSubmitting: boolean isSubmitting: boolean
submitOnEnter?: boolean
presetId?: string presetId?: string
}) { }) {
const { const {
@ -90,11 +88,8 @@ export function CommentInputTextArea(props: {
submitComment, submitComment,
presetId, presetId,
isSubmitting, isSubmitting,
submitOnEnter,
replyToUser, replyToUser,
} = props } = props
const isMobile = (useWindowSize().width ?? 0) < 768 // TODO: base off input device (keybord vs touch)
useEffect(() => { useEffect(() => {
editor?.setEditable(!isSubmitting) editor?.setEditable(!isSubmitting)
}, [isSubmitting, editor]) }, [isSubmitting, editor])
@ -108,15 +103,14 @@ export function CommentInputTextArea(props: {
if (!editor) { if (!editor) {
return return
} }
// submit on Enter key // Submit on ctrl+enter or mod+enter key
editor.setOptions({ editor.setOptions({
editorProps: { editorProps: {
handleKeyDown: (view, event) => { handleKeyDown: (view, event) => {
if ( if (
submitOnEnter &&
event.key === 'Enter' && event.key === 'Enter' &&
!event.shiftKey && !event.shiftKey &&
(!isMobile || event.ctrlKey || event.metaKey) && (event.ctrlKey || event.metaKey) &&
// mention list is closed // mention list is closed
!(view.state as any).mention$.active !(view.state as any).mention$.active
) { ) {

View File

@ -47,13 +47,13 @@ export function ConfirmationButton(props: {
{children} {children}
<Row className="gap-4"> <Row className="gap-4">
<div <div
className={clsx('btn normal-case', cancelBtn?.className)} className={clsx('btn', cancelBtn?.className)}
onClick={() => updateOpen(false)} onClick={() => updateOpen(false)}
> >
{cancelBtn?.label ?? 'Cancel'} {cancelBtn?.label ?? 'Cancel'}
</div> </div>
<div <div
className={clsx('btn normal-case', submitBtn?.className)} className={clsx('btn', submitBtn?.className)}
onClick={ onClick={
onSubmitWithSuccess onSubmitWithSuccess
? () => ? () =>
@ -69,7 +69,7 @@ export function ConfirmationButton(props: {
</Col> </Col>
</Modal> </Modal>
<div <div
className={clsx('btn normal-case', openModalBtn.className)} className={clsx('btn', openModalBtn.className)}
onClick={() => updateOpen(true)} onClick={() => updateOpen(true)}
> >
{openModalBtn.icon} {openModalBtn.icon}

View File

@ -13,7 +13,6 @@ import { Tabs } from '../layout/tabs'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { tradingAllowed } from 'web/lib/firebase/contracts' import { tradingAllowed } from 'web/lib/firebase/contracts'
import { CommentTipMap } from 'web/hooks/use-tip-txns' import { CommentTipMap } from 'web/hooks/use-tip-txns'
import { useBets } from 'web/hooks/use-bets'
import { useComments } from 'web/hooks/use-comments' import { useComments } from 'web/hooks/use-comments'
import { useLiquidity } from 'web/hooks/use-liquidity' import { useLiquidity } from 'web/hooks/use-liquidity'
import { BetSignUpPrompt } from '../sign-up-prompt' import { BetSignUpPrompt } from '../sign-up-prompt'
@ -27,24 +26,23 @@ export function ContractTabs(props: {
comments: ContractComment[] comments: ContractComment[]
tips: CommentTipMap tips: CommentTipMap
}) { }) {
const { contract, user, tips } = props const { contract, user, bets, tips } = props
const { outcomeType } = contract const { outcomeType } = contract
const bets = useBets(contract.id) ?? props.bets const lps = useLiquidity(contract.id)
const lps = useLiquidity(contract.id) ?? []
const userBets = const userBets =
user && bets.filter((bet) => !bet.isAnte && bet.userId === user.id) user && bets.filter((bet) => !bet.isAnte && bet.userId === user.id)
const visibleBets = bets.filter( const visibleBets = bets.filter(
(bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0 (bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0
) )
const visibleLps = lps.filter((l) => !l.isAnte && l.amount > 0) const visibleLps = lps?.filter((l) => !l.isAnte && l.amount > 0)
// Load comments here, so the badge count will be correct // Load comments here, so the badge count will be correct
const updatedComments = useComments(contract.id) const updatedComments = useComments(contract.id)
const comments = updatedComments ?? props.comments const comments = updatedComments ?? props.comments
const betActivity = ( const betActivity = visibleLps && (
<ContractBetsActivity <ContractBetsActivity
contract={contract} contract={contract}
bets={visibleBets} bets={visibleBets}

View File

@ -114,6 +114,7 @@ export function CreatorContractsList(props: {
additionalFilter={{ additionalFilter={{
creatorId: creator.id, creatorId: creator.id,
}} }}
persistPrefix={`user-${creator.id}`}
/> />
) )
} }

View File

@ -18,7 +18,6 @@ import { uploadImage } from 'web/lib/firebase/storage'
import { useMutation } from 'react-query' import { useMutation } from 'react-query'
import { FileUploadButton } from './file-upload-button' import { FileUploadButton } from './file-upload-button'
import { linkClass } from './site-link' import { linkClass } from './site-link'
import { useUsers } from 'web/hooks/use-users'
import { mentionSuggestion } from './editor/mention-suggestion' import { mentionSuggestion } from './editor/mention-suggestion'
import { DisplayMention } from './editor/mention' import { DisplayMention } from './editor/mention'
import Iframe from 'common/util/tiptap-iframe' import Iframe from 'common/util/tiptap-iframe'
@ -68,8 +67,6 @@ export function useTextEditor(props: {
}) { }) {
const { placeholder, max, defaultValue = '', disabled, simple } = props const { placeholder, max, defaultValue = '', disabled, simple } = props
const users = useUsers()
const editorClass = clsx( const editorClass = clsx(
proseClass, proseClass,
!simple && 'min-h-[6em]', !simple && 'min-h-[6em]',
@ -78,32 +75,27 @@ export function useTextEditor(props: {
'[&_.ProseMirror-selectednode]:outline-dotted [&_*]:outline-indigo-300' // selected img, emebeds '[&_.ProseMirror-selectednode]:outline-dotted [&_*]:outline-indigo-300' // selected img, emebeds
) )
const editor = useEditor( const editor = useEditor({
{ editorProps: { attributes: { class: editorClass } },
editorProps: { attributes: { class: editorClass } }, extensions: [
extensions: [ StarterKit.configure({
StarterKit.configure({ heading: simple ? false : { levels: [1, 2, 3] },
heading: simple ? false : { levels: [1, 2, 3] }, horizontalRule: simple ? false : {},
horizontalRule: simple ? false : {}, }),
}), Placeholder.configure({
Placeholder.configure({ placeholder,
placeholder, emptyEditorClass:
emptyEditorClass: 'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0 cursor-text',
'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0 cursor-text', }),
}), CharacterCount.configure({ limit: max }),
CharacterCount.configure({ limit: max }), simple ? DisplayImage : Image,
simple ? DisplayImage : Image, DisplayLink,
DisplayLink, DisplayMention.configure({ suggestion: mentionSuggestion }),
DisplayMention.configure({ Iframe,
suggestion: mentionSuggestion(users), TiptapTweet,
}), ],
Iframe, content: defaultValue,
TiptapTweet, })
],
content: defaultValue,
},
[!users.length] // passed as useEffect dependency. (re-render editor when users load, to update mention menu)
)
const upload = useUploadMutation(editor) const upload = useUploadMutation(editor)

View File

@ -1,9 +1,9 @@
import type { MentionOptions } from '@tiptap/extension-mention' import type { MentionOptions } from '@tiptap/extension-mention'
import { ReactRenderer } from '@tiptap/react' import { ReactRenderer } from '@tiptap/react'
import { User } from 'common/user'
import { searchInAny } from 'common/util/parse' import { searchInAny } from 'common/util/parse'
import { orderBy } from 'lodash' import { orderBy } from 'lodash'
import tippy from 'tippy.js' import tippy from 'tippy.js'
import { getCachedUsers } from 'web/hooks/use-users'
import { MentionList } from './mention-list' import { MentionList } from './mention-list'
type Suggestion = MentionOptions['suggestion'] type Suggestion = MentionOptions['suggestion']
@ -12,10 +12,12 @@ const beginsWith = (text: string, query: string) =>
text.toLocaleLowerCase().startsWith(query.toLocaleLowerCase()) text.toLocaleLowerCase().startsWith(query.toLocaleLowerCase())
// copied from https://tiptap.dev/api/nodes/mention#usage // copied from https://tiptap.dev/api/nodes/mention#usage
export const mentionSuggestion = (users: User[]): Suggestion => ({ export const mentionSuggestion: Suggestion = {
items: ({ query }) => items: async ({ query }) =>
orderBy( orderBy(
users.filter((u) => searchInAny(query, u.username, u.name)), (await getCachedUsers()).filter((u) =>
searchInAny(query, u.username, u.name)
),
[ [
(u) => [u.name, u.username].some((s) => beginsWith(s, query)), (u) => [u.name, u.username].some((s) => beginsWith(s, query)),
'followerCountCached', 'followerCountCached',
@ -38,7 +40,7 @@ export const mentionSuggestion = (users: User[]): Suggestion => ({
popup = tippy('body', { popup = tippy('body', {
getReferenceClientRect: props.clientRect as any, getReferenceClientRect: props.clientRect as any,
appendTo: () => document.body, appendTo: () => document.body,
content: component.element, content: component?.element,
showOnCreate: true, showOnCreate: true,
interactive: true, interactive: true,
trigger: 'manual', trigger: 'manual',
@ -46,27 +48,27 @@ export const mentionSuggestion = (users: User[]): Suggestion => ({
}) })
}, },
onUpdate(props) { onUpdate(props) {
component.updateProps(props) component?.updateProps(props)
if (!props.clientRect) { if (!props.clientRect) {
return return
} }
popup[0].setProps({ popup?.[0].setProps({
getReferenceClientRect: props.clientRect as any, getReferenceClientRect: props.clientRect as any,
}) })
}, },
onKeyDown(props) { onKeyDown(props) {
if (props.event.key === 'Escape') { if (props.event.key === 'Escape') {
popup[0].hide() popup?.[0].hide()
return true return true
} }
return (component.ref as any)?.onKeyDown(props) return (component?.ref as any)?.onKeyDown(props)
}, },
onExit() { onExit() {
popup[0].destroy() popup?.[0].destroy()
component.destroy() component?.destroy()
}, },
} }
}, },
}) }

View File

@ -20,7 +20,6 @@ import { getProbability } from 'common/calculate'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { Tipper } from '../tipper' import { Tipper } from '../tipper'
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
import { Content } from '../editor' import { Content } from '../editor'
import { Editor } from '@tiptap/react' import { Editor } from '@tiptap/react'
import { UserLink } from 'web/components/user-link' import { UserLink } from 'web/components/user-link'

View File

@ -22,7 +22,7 @@ export function GroupAboutPost(props: {
const post = usePost(group.aboutPostId) ?? props.post const post = usePost(group.aboutPostId) ?? props.post
return ( return (
<div className="rounded-md bg-white p-4"> <div className="rounded-md bg-white p-4 ">
{isEditable ? ( {isEditable ? (
<RichEditGroupAboutPost group={group} post={post} /> <RichEditGroupAboutPost group={group} post={post} />
) : ( ) : (

View File

@ -8,9 +8,10 @@ export function Modal(props: {
open: boolean open: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
size?: 'sm' | 'md' | 'lg' | 'xl' size?: 'sm' | 'md' | 'lg' | 'xl'
position?: 'center' | 'top' | 'bottom'
className?: string className?: string
}) { }) {
const { children, open, setOpen, size = 'md', className } = props const { children, position, open, setOpen, size = 'md', className } = props
const sizeClass = { const sizeClass = {
sm: 'max-w-sm', sm: 'max-w-sm',
@ -19,6 +20,12 @@ export function Modal(props: {
xl: 'max-w-5xl', xl: 'max-w-5xl',
}[size] }[size]
const positionClass = {
center: 'items-center',
top: 'items-start',
bottom: 'items-end',
}[position ?? 'bottom']
return ( return (
<Transition.Root show={open} as={Fragment}> <Transition.Root show={open} as={Fragment}>
<Dialog <Dialog
@ -26,7 +33,12 @@ export function Modal(props: {
className="fixed inset-0 z-50 overflow-y-auto" className="fixed inset-0 z-50 overflow-y-auto"
onClose={setOpen} onClose={setOpen}
> >
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:p-0"> <div
className={clsx(
'flex min-h-screen justify-center px-4 pt-4 pb-20 text-center sm:p-0',
positionClass
)}
>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"

View File

@ -19,7 +19,7 @@ export function BetSignUpPrompt(props: {
size={size} size={size}
color="gradient" color="gradient"
> >
{label ?? 'Sign up to trade!'} {label ?? 'Sign up to predict!'}
</Button> </Button>
) : null ) : null
} }

View File

@ -0,0 +1,74 @@
import clsx from 'clsx'
import React from 'react'
import { Row } from './layout/row'
import { ConfirmationButton } from './confirmation-button'
import { ExclamationIcon } from '@heroicons/react/solid'
export function WarningConfirmationButton(props: {
warning?: string
onSubmit: () => void
disabled?: boolean
isSubmitting: boolean
openModalButtonClass?: string
submitButtonClassName?: string
}) {
const {
onSubmit,
warning,
disabled,
isSubmitting,
openModalButtonClass,
submitButtonClassName,
} = props
if (!warning) {
return (
<button
className={clsx(
openModalButtonClass,
isSubmitting ? 'loading' : '',
disabled && 'btn-disabled'
)}
onClick={onSubmit}
disabled={disabled}
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)
}
return (
<ConfirmationButton
openModalBtn={{
className: clsx(
openModalButtonClass,
isSubmitting && 'btn-disabled loading'
),
label: 'Submit',
}}
cancelBtn={{
label: 'Cancel',
className: 'btn-warning',
}}
submitBtn={{
label: 'Submit',
className: clsx(
'border-none btn-sm btn-ghost self-center',
submitButtonClassName
),
}}
onSubmit={onSubmit}
>
<Row className="items-center text-xl">
<ExclamationIcon
className="h-16 w-16 text-yellow-400"
aria-hidden="true"
/>
Whoa, there!
</Row>
<p>{warning}</p>
</ConfirmationButton>
)
}

View File

@ -13,6 +13,7 @@ import {
getUserBetContractsQuery, getUserBetContractsQuery,
} from 'web/lib/firebase/contracts' } from 'web/lib/firebase/contracts'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import { MINUTE_MS } from 'common/util/time'
export const useContracts = () => { export const useContracts = () => {
const [contracts, setContracts] = useState<Contract[] | undefined>() const [contracts, setContracts] = useState<Contract[] | undefined>()
@ -96,8 +97,10 @@ export const useUpdatedContracts = (contracts: Contract[] | undefined) => {
export const usePrefetchUserBetContracts = (userId: string) => { export const usePrefetchUserBetContracts = (userId: string) => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return queryClient.prefetchQuery(['contracts', 'bets', userId], () => return queryClient.prefetchQuery(
getUserBetContracts(userId) ['contracts', 'bets', userId],
() => getUserBetContracts(userId),
{ staleTime: 5 * MINUTE_MS }
) )
} }

View File

@ -1,6 +1,6 @@
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import { useFirestoreQueryData } from '@react-query-firebase/firestore' import { useFirestoreQueryData } from '@react-query-firebase/firestore'
import { DAY_MS, HOUR_MS } from 'common/util/time' import { DAY_MS, HOUR_MS, MINUTE_MS } from 'common/util/time'
import { import {
getPortfolioHistory, getPortfolioHistory,
getPortfolioHistoryQuery, getPortfolioHistoryQuery,
@ -15,8 +15,10 @@ const getCutoff = (period: Period) => {
export const usePrefetchPortfolioHistory = (userId: string, period: Period) => { export const usePrefetchPortfolioHistory = (userId: string, period: Period) => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const cutoff = getCutoff(period) const cutoff = getCutoff(period)
return queryClient.prefetchQuery(['portfolio-history', userId, cutoff], () => return queryClient.prefetchQuery(
getPortfolioHistory(userId, cutoff) ['portfolio-history', userId, cutoff],
() => getPortfolioHistory(userId, cutoff),
{ staleTime: 15 * MINUTE_MS }
) )
} }
@ -24,7 +26,9 @@ export const usePortfolioHistory = (userId: string, period: Period) => {
const cutoff = getCutoff(period) const cutoff = getCutoff(period)
const result = useFirestoreQueryData( const result = useFirestoreQueryData(
['portfolio-history', userId, cutoff], ['portfolio-history', userId, cutoff],
getPortfolioHistoryQuery(userId, cutoff) getPortfolioHistoryQuery(userId, cutoff),
{},
{ staleTime: 15 * MINUTE_MS }
) )
return result.data return result.data
} }

View File

@ -7,10 +7,15 @@ import {
getUserBetsQuery, getUserBetsQuery,
listenForUserContractBets, listenForUserContractBets,
} from 'web/lib/firebase/bets' } from 'web/lib/firebase/bets'
import { MINUTE_MS } from 'common/util/time'
export const usePrefetchUserBets = (userId: string) => { export const usePrefetchUserBets = (userId: string) => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return queryClient.prefetchQuery(['bets', userId], () => getUserBets(userId)) return queryClient.prefetchQuery(
['bets', userId],
() => getUserBets(userId),
{ staleTime: MINUTE_MS }
)
} }
export const useUserBets = (userId: string) => { export const useUserBets = (userId: string) => {

View File

@ -6,7 +6,8 @@ import { useFollows } from './use-follows'
import { useUser } from './use-user' import { useUser } from './use-user'
import { useFirestoreQueryData } from '@react-query-firebase/firestore' import { useFirestoreQueryData } from '@react-query-firebase/firestore'
import { DocumentData } from 'firebase/firestore' import { DocumentData } from 'firebase/firestore'
import { users, privateUsers } from 'web/lib/firebase/users' import { users, privateUsers, getUsers } from 'web/lib/firebase/users'
import { QueryClient } from 'react-query'
export const useUsers = () => { export const useUsers = () => {
const result = useFirestoreQueryData<DocumentData, User[]>(['users'], users, { const result = useFirestoreQueryData<DocumentData, User[]>(['users'], users, {
@ -16,6 +17,10 @@ export const useUsers = () => {
return result.data ?? [] return result.data ?? []
} }
const q = new QueryClient()
export const getCachedUsers = async () =>
q.fetchQuery(['users'], getUsers, { staleTime: Infinity })
export const usePrivateUsers = () => { export const usePrivateUsers = () => {
const result = useFirestoreQueryData<DocumentData, PrivateUser[]>( const result = useFirestoreQueryData<DocumentData, PrivateUser[]>(
['private users'], ['private users'],

View File

@ -12,3 +12,7 @@ export function isIOS() {
(navigator.userAgent.includes('Mac') && 'ontouchend' in document) (navigator.userAgent.includes('Mac') && 'ontouchend' in document)
) )
} }
export function isAndroid() {
return navigator.userAgent.includes('Android')
}

View File

@ -5,7 +5,7 @@ import { Comment } from 'common/comment'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { User } from 'common/user' import { User } from 'common/user'
import { removeUndefinedProps } from 'common/util/object' import { removeUndefinedProps } from 'common/util/object'
import { ENV_CONFIG } from 'common/envs/constants' import { DOMAIN, ENV_CONFIG } from 'common/envs/constants'
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
import { richTextToString } from 'common/util/parse' import { richTextToString } from 'common/util/parse'
@ -121,7 +121,7 @@ export function toLiteMarket(contract: Contract): LiteMarket {
: closeTime, : closeTime,
question, question,
tags, tags,
url: `https://manifold.markets/${creatorUsername}/${slug}`, url: `https://${DOMAIN}/${creatorUsername}/${slug}`,
pool, pool,
probability, probability,
p, p,

View File

@ -178,7 +178,7 @@ export default function Charity(props: {
className="input input-bordered mb-6 w-full" className="input input-bordered mb-6 w-full"
/> />
</Col> </Col>
<div className="grid max-w-xl grid-flow-row grid-cols-1 gap-4 lg:max-w-full lg:grid-cols-2 xl:grid-cols-3"> <div className="grid max-w-xl grid-flow-row grid-cols-1 gap-4 self-center lg:max-w-full lg:grid-cols-2 xl:grid-cols-3">
{filterCharities.map((charity) => ( {filterCharities.map((charity) => (
<CharityCard <CharityCard
charity={charity} charity={charity}
@ -203,18 +203,26 @@ export default function Charity(props: {
></iframe> ></iframe>
</div> </div>
<div className="mt-10 text-gray-500"> <div className="prose mt-10 max-w-none text-gray-500">
<span className="font-semibold">Notes</span> <span className="text-lg font-semibold">Notes</span>
<br /> <ul>
- Don't see your favorite charity? Recommend it by emailing <li>
charity@manifold.markets! Don't see your favorite charity? Recommend it by emailing{' '}
<br /> <a href="mailto:charity@manifold.markets?subject=Add%20Charity">
- Manifold is not affiliated with non-Featured charities; we're just charity@manifold.markets
fans of their work. </a>
<br /> !
- As Manifold itself is a for-profit entity, your contributions will </li>
not be tax deductible. <li>
<br />- Donations + matches are wired once each quarter. Manifold is not affiliated with non-Featured charities; we're just
fans of their work.
</li>
<li>
As Manifold itself is a for-profit entity, your contributions will
not be tax deductible.
</li>
<li>Donations + matches are wired once each quarter.</li>
</ul>
</div> </div>
</Col> </Col>
</Page> </Page>

View File

@ -91,7 +91,7 @@ const Home = () => {
function SearchSection(props: { function SearchSection(props: {
label: string label: string
user: User | null | undefined user: User | null | undefined | undefined
sort: Sort sort: Sort
yourBets?: boolean yourBets?: boolean
followed?: boolean followed?: boolean
@ -126,7 +126,10 @@ function SearchSection(props: {
) )
} }
function GroupSection(props: { group: Group; user: User | null | undefined }) { function GroupSection(props: {
group: Group
user: User | null | undefined | undefined
}) {
const { group, user } = props const { group, user } = props
return ( return (

View File

@ -1,12 +1,12 @@
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { postPath, getPostBySlug } from 'web/lib/firebase/posts' import { postPath, getPostBySlug, updatePost } from 'web/lib/firebase/posts'
import { Post } from 'common/post' import { Post } from 'common/post'
import { Title } from 'web/components/title' import { Title } from 'web/components/title'
import { Spacer } from 'web/components/layout/spacer' import { Spacer } from 'web/components/layout/spacer'
import { Content } from 'web/components/editor' import { Content, TextEditor, useTextEditor } from 'web/components/editor'
import { getUser, User } from 'web/lib/firebase/users' import { getUser, User } from 'web/lib/firebase/users'
import { ShareIcon } from '@heroicons/react/solid' import { PencilIcon, ShareIcon } from '@heroicons/react/solid'
import clsx from 'clsx' import clsx from 'clsx'
import { Button } from 'web/components/button' import { Button } from 'web/components/button'
import { useState } from 'react' import { useState } from 'react'
@ -22,6 +22,8 @@ import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
import { groupBy, sortBy } from 'lodash' import { groupBy, sortBy } from 'lodash'
import { PostCommentInput, PostCommentThread } from 'web/posts/post-comments' import { PostCommentInput, PostCommentThread } from 'web/posts/post-comments'
import { useCommentsOnPost } from 'web/hooks/use-comments' import { useCommentsOnPost } from 'web/hooks/use-comments'
import { useUser } from 'web/hooks/use-user'
import { usePost } from 'web/hooks/use-post'
export async function getStaticProps(props: { params: { slugs: string[] } }) { export async function getStaticProps(props: { params: { slugs: string[] } }) {
const { slugs } = props.params const { slugs } = props.params
@ -51,12 +53,14 @@ export default function PostPage(props: {
comments: PostComment[] comments: PostComment[]
}) { }) {
const [isShareOpen, setShareOpen] = useState(false) const [isShareOpen, setShareOpen] = useState(false)
const { post, creator } = props const { creator } = props
const post = usePost(props.post.id) ?? props.post
const tips = useTipTxns({ postId: post.id }) const tips = useTipTxns({ postId: post.id })
const shareUrl = `https://${ENV_CONFIG.domain}${postPath(post.slug)}` const shareUrl = `https://${ENV_CONFIG.domain}${postPath(post.slug)}`
const updatedComments = useCommentsOnPost(post.id) const updatedComments = useCommentsOnPost(post.id)
const comments = updatedComments ?? props.comments const comments = updatedComments ?? props.comments
const user = useUser()
if (post == null) { if (post == null) {
return <Custom404 /> return <Custom404 />
@ -65,10 +69,9 @@ export default function PostPage(props: {
return ( return (
<Page> <Page>
<div className="mx-auto w-full max-w-3xl "> <div className="mx-auto w-full max-w-3xl ">
<Spacer h={1} /> <Title className="!mt-0 py-4 px-2" text={post.title} />
<Title className="!mt-0" text={post.title} />
<Row> <Row>
<Col className="flex-1"> <Col className="flex-1 px-2">
<div className={'inline-flex'}> <div className={'inline-flex'}>
<div className="mr-1 text-gray-500">Created by</div> <div className="mr-1 text-gray-500">Created by</div>
<UserLink <UserLink
@ -78,7 +81,7 @@ export default function PostPage(props: {
/> />
</div> </div>
</Col> </Col>
<Col> <Col className="px-2">
<Button <Button
size="lg" size="lg"
color="gray-white" color="gray-white"
@ -104,11 +107,15 @@ export default function PostPage(props: {
<Spacer h={2} /> <Spacer h={2} />
<div className="rounded-lg bg-white px-6 py-4 sm:py-0"> <div className="rounded-lg bg-white px-6 py-4 sm:py-0">
<div className="form-control w-full py-2"> <div className="form-control w-full py-2">
<Content content={post.content} /> {user && user.id === post.creatorId ? (
<RichEditPost post={post} />
) : (
<Content content={post.content} />
)}
</div> </div>
</div> </div>
<Spacer h={2} /> <Spacer h={4} />
<div className="rounded-lg bg-white px-6 py-4 sm:py-0"> <div className="rounded-lg bg-white px-6 py-4 sm:py-0">
<PostCommentsActivity <PostCommentsActivity
post={post} post={post}
@ -137,7 +144,7 @@ export function PostCommentsActivity(props: {
) )
return ( return (
<> <Col className="p-2">
<PostCommentInput post={post} /> <PostCommentInput post={post} />
{topLevelComments.map((parent) => ( {topLevelComments.map((parent) => (
<PostCommentThread <PostCommentThread
@ -153,6 +160,68 @@ export function PostCommentsActivity(props: {
commentsByUserId={commentsByUserId} commentsByUserId={commentsByUserId}
/> />
))} ))}
</Col>
)
}
function RichEditPost(props: { post: Post }) {
const { post } = props
const [editing, setEditing] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({
defaultValue: post.content,
disabled: isSubmitting,
})
async function savePost() {
if (!editor) return
await updatePost(post, {
content: editor.getJSON(),
})
}
return editing ? (
<>
<TextEditor editor={editor} upload={upload} />
<Spacer h={2} />
<Row className="gap-2">
<Button
onClick={async () => {
setIsSubmitting(true)
await savePost()
setEditing(false)
setIsSubmitting(false)
}}
>
Save
</Button>
<Button color="gray" onClick={() => setEditing(false)}>
Cancel
</Button>
</Row>
</>
) : (
<>
<div className="relative">
<div className="absolute top-0 right-0 z-10 space-x-2">
<Button
color="gray"
size="xs"
onClick={() => {
setEditing(true)
editor?.commands.focus('end')
}}
>
<PencilIcon className="inline h-4 w-4" />
Edit
</Button>
</div>
<Content content={post.content} />
<Spacer h={2} />
</div>
</> </>
) )
} }

View File

@ -77,13 +77,21 @@ const Salem = {
const tourneys: Tourney[] = [ const tourneys: Tourney[] = [
{ {
title: 'Cause Exploration Prizes', title: 'Manifold F2P Tournament',
blurb: blurb:
'Which new charity ideas will Open Philanthropy find most promising?', 'Who can amass the most mana starting from a free-to-play (F2P) account?',
award: 'M$100k', award: 'Poem',
endTime: toDate('Sep 9, 2022'), endTime: toDate('Sep 15, 2022'),
groupId: 'cMcpBQ2p452jEcJD2SFw', groupId: '6rrIja7tVW00lUVwtsYS',
}, },
// {
// title: 'Cause Exploration Prizes',
// blurb:
// 'Which new charity ideas will Open Philanthropy find most promising?',
// award: 'M$100k',
// endTime: toDate('Sep 9, 2022'),
// groupId: 'cMcpBQ2p452jEcJD2SFw',
// },
{ {
title: 'Fantasy Football Stock Exchange', title: 'Fantasy Football Stock Exchange',
blurb: 'How many points will each NFL player score this season?', blurb: 'How many points will each NFL player score this season?',
@ -91,13 +99,6 @@ const tourneys: Tourney[] = [
endTime: toDate('Jan 6, 2023'), endTime: toDate('Jan 6, 2023'),
groupId: 'SxGRqXRpV3RAQKudbcNb', groupId: 'SxGRqXRpV3RAQKudbcNb',
}, },
{
title: 'SF 2022 Ballot',
blurb: 'Which ballot initiatives will pass this year in SF and CA?',
award: '',
endTime: toDate('Nov 8, 2022'),
groupId: 'VkWZyS5yxs8XWUJrX9eq',
},
// { // {
// title: 'Clearer Thinking Regrant Project', // title: 'Clearer Thinking Regrant Project',
// blurb: 'Something amazing', // blurb: 'Something amazing',
@ -105,6 +106,27 @@ const tourneys: Tourney[] = [
// endTime: toDate('Sep 22, 2022'), // endTime: toDate('Sep 22, 2022'),
// groupId: '2VsVVFGhKtIdJnQRAXVb', // groupId: '2VsVVFGhKtIdJnQRAXVb',
// }, // },
// Tournaments without awards get featured belows
{
title: 'SF 2022 Ballot',
blurb: 'Which ballot initiatives will pass this year in SF and CA?',
endTime: toDate('Nov 8, 2022'),
groupId: 'VkWZyS5yxs8XWUJrX9eq',
},
{
title: '2024 Democratic Nominees',
blurb: 'How would different Democratic candidates fare in 2024?',
endTime: toDate('Nov 2, 2024'),
groupId: 'gFhjgFVrnYeFYfxhoLNn',
},
{
title: 'Private Tech Companies',
blurb: 'What will these companies exit for?',
endTime: toDate('Dec 31, 2030'),
groupId: 'faNUnphw6Eoq7OJBRJds',
},
] ]
type SectionInfo = { type SectionInfo = {
@ -135,20 +157,23 @@ export default function TournamentPage(props: { sections: SectionInfo[] }) {
title="Tournaments" title="Tournaments"
description="Win money by betting in forecasting touraments on current events, sports, science, and more" description="Win money by betting in forecasting touraments on current events, sports, science, and more"
/> />
<Col className="mx-4 mt-4 gap-10 sm:mx-10 xl:w-[125%]"> <Col className="m-4 gap-10 sm:mx-10 sm:gap-24 xl:w-[125%]">
{sections.map(({ tourney, slug, numPeople }) => ( {sections.map(
<div key={slug}> ({ tourney, slug, numPeople }) =>
<SectionHeader tourney.award && (
url={groupPath(slug)} <div key={slug}>
title={tourney.title} <SectionHeader
ppl={numPeople} url={groupPath(slug, 'about')}
award={tourney.award} title={tourney.title}
endTime={tourney.endTime} ppl={numPeople}
/> award={tourney.award}
<span>{tourney.blurb}</span> endTime={tourney.endTime}
<MarketCarousel slug={slug} /> />
</div> <span className="text-gray-500">{tourney.blurb}</span>
))} <MarketCarousel slug={slug} />
</div>
)
)}
<div> <div>
<SectionHeader <SectionHeader
url={Salem.url} url={Salem.url}
@ -156,9 +181,52 @@ export default function TournamentPage(props: { sections: SectionInfo[] }) {
award={Salem.award} award={Salem.award}
endTime={Salem.endTime} endTime={Salem.endTime}
/> />
<span>{Salem.blurb}</span> <span className="text-gray-500">{Salem.blurb}</span>
<ImageCarousel url={Salem.url} images={Salem.images} /> <ImageCarousel url={Salem.url} images={Salem.images} />
</div> </div>
{/* Title break */}
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center">
<span className="bg-gray-50 px-3 text-lg font-medium text-gray-900">
Featured Groups
</span>
</div>
</div>
{sections.map(
({ tourney, slug, numPeople }) =>
!tourney.award && (
<div key={slug}>
<SectionHeader
url={groupPath(slug, 'about')}
title={tourney.title}
ppl={numPeople}
award={tourney.award}
endTime={tourney.endTime}
/>
<span className="text-gray-500">{tourney.blurb}</span>
<MarketCarousel slug={slug} />
</div>
)
)}
<p className="pb-10 italic text-gray-500">
We'd love to sponsor more tournaments and groups. Have an idea? Ping{' '}
<SiteLink
className="font-semibold"
href="https://discord.com/invite/eHQBNBqXuh"
>
Austin on Discord
</SiteLink>
!
</p>
</Col> </Col>
</Page> </Page>
) )
@ -175,9 +243,7 @@ const SectionHeader = (props: {
return ( return (
<Link href={url}> <Link href={url}>
<a className="group mb-3 flex flex-wrap justify-between"> <a className="group mb-3 flex flex-wrap justify-between">
<h2 className="text-xl font-semibold group-hover:underline md:text-3xl"> <h2 className="text-xl group-hover:underline md:text-3xl">{title}</h2>
{title}
</h2>
<Row className="my-2 items-center gap-4 whitespace-nowrap rounded-full bg-gray-200 px-6"> <Row className="my-2 items-center gap-4 whitespace-nowrap rounded-full bg-gray-200 px-6">
{!!award && <span className="flex items-center">🏆 {award}</span>} {!!award && <span className="flex items-center">🏆 {award}</span>}
{!!ppl && ( {!!ppl && (

View File

@ -64,6 +64,8 @@ function putIntoMapAndFetch(data) {
document.getElementById('guess-type').innerText = 'Finding Fantastic Beasts' document.getElementById('guess-type').innerText = 'Finding Fantastic Beasts'
} else if (whichGuesser === 'basic') { } else if (whichGuesser === 'basic') {
document.getElementById('guess-type').innerText = 'How Basic' document.getElementById('guess-type').innerText = 'How Basic'
} else if (whichGuesser === 'commander') {
document.getElementById('guess-type').innerText = 'General Knowledge'
} }
setUpNewGame() setUpNewGame()
} }
@ -156,8 +158,8 @@ function determineIfSkip(card) {
if (card.flavor_name) { if (card.flavor_name) {
return true return true
} }
// don't include racist cards
return card.content_warning return false
} }
function putIntoMap(data) { function putIntoMap(data) {

View File

@ -3,16 +3,16 @@ import requests
import json import json
# add category name here # add category name here
allCategories = ['counterspell', 'beast', 'burn'] #, 'terror', 'wrath'] allCategories = ['counterspell', 'beast', 'burn', 'commander', 'artist'] #, 'terror', 'wrath', 'zombie', 'artifact']
specialCategories = ['set', 'basic'] specialCategories = ['set', 'basic']
def generate_initial_query(category): def generate_initial_query(category):
string_query = 'https://api.scryfall.com/cards/search?q=' string_query = 'https://api.scryfall.com/cards/search?q='
if category == 'counterspell': if category == 'counterspell':
string_query += 'otag%3Acounterspell+t%3Ainstant+not%3Aadventure' string_query += 'otag%3Acounterspell+t%3Ainstant+not%3Aadventure+not%3Adfc'
elif category == 'beast': elif category == 'beast':
string_query += '-type%3Alegendary+type%3Abeast+-type%3Atoken' string_query += '-type%3Alegendary+type%3Abeast+-type%3Atoken+not%3Adfc'
# elif category == 'terror': # elif category == 'terror':
# string_query += 'otag%3Acreature-removal+o%3A%2Fdestroy+target.%2A+%28creature%7Cpermanent%29%2F+%28t' \ # string_query += 'otag%3Acreature-removal+o%3A%2Fdestroy+target.%2A+%28creature%7Cpermanent%29%2F+%28t' \
# '%3Ainstant+or+t%3Asorcery%29+o%3Atarget+not%3Aadventure' # '%3Ainstant+or+t%3Asorcery%29+o%3Atarget+not%3Aadventure'
@ -22,11 +22,19 @@ def generate_initial_query(category):
string_query += '%28c>%3Dr+or+mana>%3Dr%29+%28o%3A%2Fdamage+to+them%2F+or+%28o%3Adeals+o%3Adamage+o%3A' \ string_query += '%28c>%3Dr+or+mana>%3Dr%29+%28o%3A%2Fdamage+to+them%2F+or+%28o%3Adeals+o%3Adamage+o%3A' \
'%2Fcontroller%28%5C.%7C+%29%2F%29+or+o%3A%2F~+deals+%28.%7C..%29+damage+to+%28any+target%7C' \ '%2Fcontroller%28%5C.%7C+%29%2F%29+or+o%3A%2F~+deals+%28.%7C..%29+damage+to+%28any+target%7C' \
'.*player%28%5C.%7C+or+planeswalker%29%7C.*opponent%28%5C.%7C+or+planeswalker%29%29%2F%29' \ '.*player%28%5C.%7C+or+planeswalker%29%7C.*opponent%28%5C.%7C+or+planeswalker%29%29%2F%29' \
'+%28type%3Ainstant+or+type%3Asorcery%29+not%3Aadventure' '+%28type%3Ainstant+or+type%3Asorcery%29+not%3Aadventure+not%3Adfc'
elif category == 'commander':
string_query += 'is%3Acommander+%28not%3Adigital+-banned%3Acommander+or+is%3Adigital+legal%3Ahistoricbrawl+or+legal%3Acommander+or+legal%3Abrawl%29'
# elif category == 'zombie':
# string_query += '-type%3Alegendary+type%3Azombie+-type%3Atoken'
# elif category == 'artifact':
# string_query += 't%3Aartifact&order=released&dir=asc&unique=prints&page='
# elif category == 'artist':
# string_query+= 'a%3A"Wylie+Beckert"+or+a%3A“Ernanda+Souza”+or+a%3A"randy+gallegos"+or+a%3A“Amy+Weber”+or+a%3A“Dan+Frazier”+or+a%3A“Thomas+M.+Baxa”+or+a%3A“Phil+Foglio”+or+a%3A“DiTerlizzi”+or+a%3A"steve+argyle"+or+a%3A"Veronique+Meignaud"+or+a%3A"Magali+Villeneuve"+or+a%3A"Michael+Sutfin"+or+a%3A“Volkan+Baǵa”+or+a%3A“Franz+Vohwinkel”+or+a%3A"Nils+Hamm"+or+a%3A"Mark+Poole"+or+a%3A"Carl+Critchlow"+or+a%3A"rob+alexander"+or+a%3A"igor+kieryluk"+or+a%3A“Victor+Adame+Minguez”+or+a%3A"johannes+voss"+or+a%3A"Svetlin+Velinov"+or+a%3A"ron+spencer"+or+a%3A"rk+post"+or+a%3A"kev+walker"+or+a%3A"rebecca+guay"+or+a%3A"seb+mckinnon"+or+a%3A"pete+venters"+or+a%3A"greg+staples"+or+a%3A"Christopher+Moeller"+or+a%3A"christopher+rush"+or+a%3A"Mark+Tedin"'
# add category string query here # add category string query here
string_query += '+-%28set%3Asld+%28%28cn>%3D231+cn<%3D233%29+or+%28cn>%3D321+cn<%3D324%29+or+%28cn>%3D185+cn' \ string_query += '+-%28set%3Asld+%28%28cn>%3D231+cn<%3D233%29+or+%28cn>%3D321+cn<%3D324%29+or+%28cn>%3D185+cn' \
'<%3D189%29+or+%28cn>%3D138+cn<%3D142%29+or+%28cn>%3D364+cn<%3D368%29+or+cn%3A669+or+cn%3A670%29' \ '<%3D189%29+or+%28cn>%3D138+cn<%3D142%29+or+%28cn>%3D364+cn<%3D368%29+or+cn%3A669+or+cn%3A670%29' \
'%29+-name%3A%2F%5EA-%2F+not%3Adfc+not%3Asplit+-set%3Acmb2+-set%3Acmb1+-set%3Aplist+-set%3Adbl' \ '%29+-name%3A%2F%5EA-%2F+not%3Asplit+-set%3Acmb2+-set%3Acmb1+-set%3Aplist+-st%3Amemorabilia' \
'+language%3Aenglish&order=released&dir=asc&unique=prints&page=' '+language%3Aenglish&order=released&dir=asc&unique=prints&page='
print(string_query) print(string_query)
return string_query return string_query
@ -89,22 +97,35 @@ def fetch_special(query):
def to_compact_write_form(smallJson, art_names, response, category): def to_compact_write_form(smallJson, art_names, response, category):
fieldsInCard = ['name', 'image_uris', 'content_warning', 'flavor_name', 'reprint', 'frame_effects', 'digital', fieldsInCard = ['name', 'image_uris', 'flavor_name', 'reprint', 'frame_effects', 'digital', 'set_type']
'set_type']
data = [] data = []
# write all fields needed in card # write all fields needed in card
for card in response['data']: for card in response['data']:
# do not include racist cards
if 'content_warning' in card and card['content_warning'] == True:
continue
# do not repeat art # do not repeat art
if 'illustration_id' not in card or card['illustration_id'] in art_names: if 'card_faces' in card:
card_face = card['card_faces'][0]
if 'illustration_id' not in card_face or card_face['illustration_id'] in art_names:
continue
else:
art_names.add(card_face['illustration_id'])
elif 'illustration_id' not in card or card['illustration_id'] in art_names:
continue continue
else: else:
art_names.add(card['illustration_id']) art_names.add(card['illustration_id'])
write_card = dict() write_card = dict()
for field in fieldsInCard: for field in fieldsInCard:
# if field == 'name' and category == 'artifact':
# write_card['name'] = card['released_at'].split('-')[0]
if field == 'name' and 'card_faces' in card: if field == 'name' and 'card_faces' in card:
write_card['name'] = card['card_faces'][0]['name'] write_card['name'] = card['card_faces'][0]['name']
elif field == 'image_uris': elif field == 'image_uris':
write_card['image_uris'] = write_image_uris(card['image_uris']) if 'card_faces' in card and 'image_uris' in card['card_faces'][0]:
write_card['image_uris'] = write_image_uris(card['card_faces'][0]['image_uris'])
else:
write_card['image_uris'] = write_image_uris(card['image_uris'])
elif field in card: elif field in card:
write_card[field] = card[field] write_card[field] = card[field]
data.append(write_card) data.append(write_card)
@ -115,6 +136,9 @@ def to_compact_write_form_special(smallJson, art_names, response, category):
data = [] data = []
# write all fields needed in card # write all fields needed in card
for card in response['data']: for card in response['data']:
# do not include racist cards
if 'content_warning' in card and card['content_warning'] == True:
continue
if category == 'basic': if category == 'basic':
write_card = dict() write_card = dict()
# do not repeat art # do not repeat art
@ -152,9 +176,9 @@ def write_image_uris(card_image_uris):
if __name__ == "__main__": if __name__ == "__main__":
# for category in allCategories: for category in allCategories:
# print(category) print(category)
# fetch_and_write_all(category, generate_initial_query(category)) fetch_and_write_all(category, generate_initial_query(category))
for category in specialCategories: for category in specialCategories:
print(category) print(category)
fetch_and_write_all_special(category, generate_initial_special_query(category)) fetch_and_write_all_special(category, generate_initial_special_query(category))

View File

@ -17,6 +17,14 @@
f.parentNode.insertBefore(j, f) f.parentNode.insertBefore(j, f)
})(window, document, 'script', 'dataLayer', 'GTM-M3MBVGG') })(window, document, 'script', 'dataLayer', 'GTM-M3MBVGG')
</script> </script>
<script>
function updateSettingDefault(digital, un, original) {
window.console.log(digital, un, original)
document.getElementById('digital').checked = digital
document.getElementById('un').checked = un
document.getElementById('original').checked = original
}
</script>
<!-- End Google Tag Manager --> <!-- End Google Tag Manager -->
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<style type="text/css"> <style type="text/css">
@ -105,6 +113,18 @@
list-style: none; list-style: none;
text-align: right; text-align: right;
} }
.option-row {
display: flex;
align-items: flex-end;
padding-left: 65px;
}
.level-badge {
display: block;
width: 65px;
padding-left: 8px;
padding-bottom: 2px;
}
</style> </style>
</head> </head>
<body> <body>
@ -125,48 +145,115 @@
action="guess.html" action="guess.html"
style="display: flex; flex-direction: column; align-items: center" style="display: flex; flex-direction: column; align-items: center"
> >
<input <div class="option-row">
type="radio" <input
id="counterspell" type="radio"
name="whichguesser" id="counterspell"
value="counterspell" name="whichguesser"
checked value="counterspell"
/> onchange="updateSettingDefault(true, true, false)"
<label class="radio-label" for="counterspell"> checked
<img
class="thumbnail"
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/71cfcba5-1571-48b8-a3db-55dca135506e.jpg?1562843855"
/> />
<h3>Counterspell Guesser</h3></label <label class="radio-label" for="counterspell">
><br /> <img
class="thumbnail"
<input type="radio" id="burn" name="whichguesser" value="burn" /> src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/71cfcba5-1571-48b8-a3db-55dca135506e.jpg?1562843855"
<label class="radio-label" for="burn"> />
<h3>Counterspell Guesser</h3></label
>
<img <img
class="thumbnail" class="level-badge"
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60b2fae1-242b-45e0-a757-b1adc02c06f3.jpg?1562760596" src="https://static.wikia.nocookie.net/mtgsalvation_gamepedia/images/a/a8/Advanced_level.jpg"
/> />
<h3>Match With Hot Singles</h3></label </div>
><br />
<input type="radio" id="beast" name="whichguesser" value="beast" />
<label class="radio-label" for="beast">
<img
class="thumbnail"
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33f7e788-8fc7-49f3-804b-2d7f96852d4b.jpg?1562905469"
/>
<h3>Finding Fantastic Beasts</h3></label
>
<br /> <br />
<input type="radio" id="basic" name="whichguesser" value="basic" /> <div class="option-row">
<label class="radio-label" for="basic"> <input
<img type="radio"
class="thumbnail" id="burn"
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/03683fbb-9843-4c14-bb95-387150e97c90.jpg?1642161346" name="whichguesser"
value="burn"
onchange="updateSettingDefault(true, true, false)"
/> />
<h3>How Basic</h3></label <label class="radio-label" for="burn">
> <img
class="thumbnail"
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60b2fae1-242b-45e0-a757-b1adc02c06f3.jpg?1562760596"
/>
<h3>Match With Hot Singles</h3></label
>
<img
class="level-badge"
src="https://static.wikia.nocookie.net/mtgsalvation_gamepedia/images/a/a8/Advanced_level.jpg"
/>
</div>
<br />
<div class="option-row">
<input
type="radio"
id="beast"
name="whichguesser"
value="beast"
onchange="updateSettingDefault(true, true, false)"
/>
<label class="radio-label" for="beast">
<img
class="thumbnail"
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/3/3/33f7e788-8fc7-49f3-804b-2d7f96852d4b.jpg?1562905469"
/>
<h3>Finding Fantastic Beasts</h3></label
>
<img
class="level-badge"
src="https://static.wikia.nocookie.net/mtgsalvation_gamepedia/images/a/a8/Advanced_level.jpg"
/>
</div>
<br />
<div class="option-row">
<input
type="radio"
id="basic"
name="whichguesser"
value="basic"
onchange="updateSettingDefault(true, true, true)"
/>
<label class="radio-label" for="basic">
<img
class="thumbnail"
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/e/5/e52ed647-bd30-40a5-b648-0b98d1a3fd4a.jpg?1562949575"
/>
<h3>How Basic</h3></label
>
<img
class="level-badge"
src="https://static.wikia.nocookie.net/mtgsalvation_gamepedia/images/a/af/Expert_level.jpg"
/>
</div>
<br />
<div class="option-row">
<input
type="radio"
id="commander"
name="whichguesser"
value="commander"
onchange="updateSettingDefault(false, false, false)"
/>
<label class="radio-label" for="commander">
<img
class="thumbnail"
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/d/9/d9631cb2-d53b-4401-b53b-29d27bdefc44.jpg?1562770627"
/>
<h3>General Knowledge</h3></label
>
<img
class="level-badge"
src="https://static.wikia.nocookie.net/mtgsalvation_gamepedia/images/0/00/Starter_level.jpg"
/>
</div>
<br /> <br />
<details id="addl-options"> <details id="addl-options">

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -60,6 +60,18 @@ module.exports = {
'overflow-wrap': 'anywhere', 'overflow-wrap': 'anywhere',
'word-break': 'break-word', // for Safari 'word-break': 'break-word', // for Safari
}, },
'.only-thumb': {
'pointer-events': 'none',
'&::-webkit-slider-thumb': {
'pointer-events': 'auto !important',
},
'&::-moz-range-thumb': {
'pointer-events': 'auto !important',
},
'&::-ms-thumb': {
'pointer-events': 'auto !important',
},
},
}) })
}), }),
], ],