Merge branch 'main' into new-home
This commit is contained in:
commit
299f2f24c6
|
@ -73,6 +73,7 @@ export const PROD_CONFIG: EnvConfig = {
|
|||
'manticmarkets@gmail.com', // Manifold
|
||||
'iansphilips@gmail.com', // Ian
|
||||
'd4vidchee@gmail.com', // D4vid
|
||||
'federicoruizcassarino@gmail.com', // Fede
|
||||
],
|
||||
visibility: 'PUBLIC',
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import { addObjects } from './util/object'
|
|||
export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => {
|
||||
const { pool } = contract
|
||||
const poolTotal = sum(Object.values(pool))
|
||||
console.log('resolved N/A, pool M$', poolTotal)
|
||||
|
||||
const betSum = sumBy(bets, (b) => b.amount)
|
||||
|
||||
|
@ -58,17 +57,6 @@ export const getDpmStandardPayouts = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved',
|
||||
outcome,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
@ -110,17 +98,6 @@ export const getNumericDpmPayouts = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved numeric bucket: ',
|
||||
outcome,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
@ -163,17 +140,6 @@ export const getDpmMktPayouts = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved MKT',
|
||||
p,
|
||||
'pool',
|
||||
pool,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
@ -216,16 +182,6 @@ export const getPayoutsMultiOutcome = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved',
|
||||
resolutions,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { sum } from 'lodash'
|
||||
|
||||
import { Bet } from './bet'
|
||||
import { getProbability } from './calculate'
|
||||
|
@ -43,18 +42,6 @@ export const getStandardFixedPayouts = (
|
|||
|
||||
const { collectedFees } = contract
|
||||
const creatorPayout = collectedFees.creatorFee
|
||||
|
||||
console.log(
|
||||
'resolved',
|
||||
outcome,
|
||||
'pool',
|
||||
contract.pool[outcome],
|
||||
'payouts',
|
||||
sum(payouts),
|
||||
'creator fee',
|
||||
creatorPayout
|
||||
)
|
||||
|
||||
const liquidityPayouts = getLiquidityPoolPayouts(
|
||||
contract,
|
||||
outcome,
|
||||
|
@ -98,18 +85,6 @@ export const getMktFixedPayouts = (
|
|||
|
||||
const { collectedFees } = contract
|
||||
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)
|
||||
|
||||
return { payouts, creatorPayout, liquidityPayouts, collectedFees }
|
||||
|
|
|
@ -13,7 +13,10 @@ export const getRedeemableAmount = (bets: RedeemableBet[]) => {
|
|||
const yesShares = sumBy(yesBets, (b) => b.shares)
|
||||
const noShares = sumBy(noBets, (b) => b.shares)
|
||||
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 loanPayment = loanAmount * soldFrac
|
||||
const netAmount = shares - loanPayment
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { groupBy, sumBy, mapValues, partition } from 'lodash'
|
||||
import { groupBy, sumBy, mapValues } from 'lodash'
|
||||
|
||||
import { Bet } from './bet'
|
||||
import { getContractBetMetrics } from './calculate'
|
||||
import { Contract } from './contract'
|
||||
import { getPayouts } from './payouts'
|
||||
|
||||
export function scoreCreators(contracts: Contract[]) {
|
||||
const creatorScore = mapValues(
|
||||
|
@ -30,46 +30,8 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
|
|||
}
|
||||
|
||||
export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
||||
const { resolution } = contract
|
||||
const resolutionProb =
|
||||
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
|
||||
const betsByUser = groupBy(bets, bet => bet.userId)
|
||||
return mapValues(betsByUser, bets => getContractBetMetrics(contract, bets).profit)
|
||||
}
|
||||
|
||||
export function addUserScores(
|
||||
|
|
|
@ -60,23 +60,27 @@ Parameters:
|
|||
|
||||
Requires no authorization.
|
||||
|
||||
### `GET /v0/groups/[slug]`
|
||||
### `GET /v0/group/[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]`
|
||||
|
||||
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`
|
||||
|
||||
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`
|
||||
|
||||
|
|
|
@ -12,7 +12,9 @@ service cloud.firestore {
|
|||
'taowell@gmail.com',
|
||||
'abc.sinclair@gmail.com',
|
||||
'manticmarkets@gmail.com',
|
||||
'iansphilips@gmail.com'
|
||||
'iansphilips@gmail.com',
|
||||
'd4vidchee@gmail.com',
|
||||
'federicoruizcassarino@gmail.com'
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,45 @@ export const changeUser = async (
|
|||
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) => {
|
||||
if (update.username) {
|
||||
update.username = cleanUsername(update.username)
|
||||
|
@ -58,42 +97,7 @@ export const changeUser = async (
|
|||
|
||||
const userRef = firestore.collection('users').doc(user.id)
|
||||
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)
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -186,8 +186,9 @@
|
|||
font-family: Readex Pro, Arial, Helvetica,
|
||||
sans-serif;
|
||||
font-size: 17px;
|
||||
">Did you know you create your own prediction market on <a class="link-build-content"
|
||||
style="color: #55575d" target="_blank" href="https://manifold.markets">Manifold</a> for
|
||||
">Did you know you can create your own prediction market on <a
|
||||
class="link-build-content" style="color: #55575d" target="_blank"
|
||||
href="https://manifold.markets">Manifold</a> on
|
||||
any question you care about?</span>
|
||||
</p>
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<AmountInput
|
||||
|
@ -138,10 +150,10 @@ export function BuyAmountInput(props: {
|
|||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="200"
|
||||
value={amount ?? 0}
|
||||
onChange={(e) => onAmountChange(parseInt(e.target.value))}
|
||||
className="range range-lg z-40 mb-2 xl:hidden"
|
||||
max="205"
|
||||
value={getRaw(amount ?? 0)}
|
||||
onChange={(e) => onAmountChange(parseRaw(parseInt(e.target.value)))}
|
||||
className="range range-lg only-thumb z-40 mb-2 xl:hidden"
|
||||
step="5"
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import clsx from 'clsx'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { XIcon } from '@heroicons/react/solid'
|
||||
|
||||
import { Answer } from 'common/answer'
|
||||
|
@ -25,8 +25,7 @@ import {
|
|||
import { Bet } from 'common/bet'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { BetSignUpPrompt } from '../sign-up-prompt'
|
||||
import { isIOS } from 'web/lib/util/device'
|
||||
import { AlertBox } from '../alert-box'
|
||||
import { WarningConfirmationButton } from '../warning-confirmation-button'
|
||||
|
||||
export function AnswerBetPanel(props: {
|
||||
answer: Answer
|
||||
|
@ -44,12 +43,6 @@ export function AnswerBetPanel(props: {
|
|||
const [error, setError] = useState<string | undefined>()
|
||||
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() {
|
||||
if (!user || !betAmount) return
|
||||
|
||||
|
@ -116,6 +109,15 @@ export function AnswerBetPanel(props: {
|
|||
|
||||
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 (
|
||||
<Col className={clsx('px-2 pb-2 pt-4 sm:pt-0', className)}>
|
||||
<Row className="items-center justify-between self-stretch">
|
||||
|
@ -144,25 +146,9 @@ export function AnswerBetPanel(props: {
|
|||
error={error}
|
||||
setError={setError}
|
||||
disabled={isSubmitting}
|
||||
inputRef={inputRef}
|
||||
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">
|
||||
<Row className="items-center justify-between text-sm">
|
||||
<div className="text-gray-500">Probability</div>
|
||||
|
@ -198,16 +184,17 @@ export function AnswerBetPanel(props: {
|
|||
<Spacer h={6} />
|
||||
|
||||
{user ? (
|
||||
<button
|
||||
className={clsx(
|
||||
<WarningConfirmationButton
|
||||
warning={warning}
|
||||
onSubmit={submitBet}
|
||||
isSubmitting={isSubmitting}
|
||||
disabled={!!betDisabled}
|
||||
openModalButtonClass={clsx(
|
||||
'btn self-stretch',
|
||||
betDisabled ? 'btn-disabled' : 'btn-primary',
|
||||
isSubmitting ? 'loading' : ''
|
||||
)}
|
||||
onClick={betDisabled ? undefined : submitBet}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
/>
|
||||
) : (
|
||||
<BetSignUpPrompt />
|
||||
)}
|
||||
|
|
|
@ -194,7 +194,7 @@ function OpenAnswer(props: {
|
|||
|
||||
return (
|
||||
<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
|
||||
answer={answer}
|
||||
contract={contract}
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function BetButton(props: {
|
|||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Trade
|
||||
Predict
|
||||
</Button>
|
||||
) : (
|
||||
<BetSignUpPrompt />
|
||||
|
@ -60,7 +60,7 @@ export default function BetButton(props: {
|
|||
)}
|
||||
</Col>
|
||||
|
||||
<Modal open={open} setOpen={setOpen}>
|
||||
<Modal open={open} setOpen={setOpen} position="center">
|
||||
<SimpleBetPanel
|
||||
className={betPanelClassName}
|
||||
contract={contract}
|
||||
|
|
|
@ -40,7 +40,8 @@ import { LimitBets } from './limit-bets'
|
|||
import { PillButton } from './buttons/pill-button'
|
||||
import { YesNoSelector } from './yes-no-selector'
|
||||
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: {
|
||||
contract: CPMMBinaryContract | PseudoNumericContract
|
||||
|
@ -184,17 +185,13 @@ function BuyPanel(props: {
|
|||
|
||||
const [inputRef, focusAmountInput] = useFocus()
|
||||
|
||||
// useEffect(() => {
|
||||
// if (selected) {
|
||||
// if (isIOS()) window.scrollTo(0, window.scrollY + 200)
|
||||
// focusAmountInput()
|
||||
// }
|
||||
// }, [selected, focusAmountInput])
|
||||
|
||||
function onBetChoice(choice: 'YES' | 'NO') {
|
||||
setOutcome(choice)
|
||||
setWasSubmitted(false)
|
||||
focusAmountInput()
|
||||
|
||||
if (!isIOS() && !isAndroid()) {
|
||||
focusAmountInput()
|
||||
}
|
||||
}
|
||||
|
||||
function onBetChange(newAmount: number | undefined) {
|
||||
|
@ -274,30 +271,20 @@ function BuyPanel(props: {
|
|||
const bankrollFraction = (betAmount ?? 0) / (user?.balance ?? 1e9)
|
||||
|
||||
const warning =
|
||||
(betAmount ?? 0) > 10 &&
|
||||
bankrollFraction >= 0.5 &&
|
||||
bankrollFraction <= 1 ? (
|
||||
<AlertBox
|
||||
title="Whoa, there!"
|
||||
text={`You might not want to spend ${formatPercent(
|
||||
(betAmount ?? 0) >= 100 && bankrollFraction >= 0.5 && bankrollFraction <= 1
|
||||
? `You might not want to spend ${formatPercent(
|
||||
bankrollFraction
|
||||
)} of your balance on a single trade. \n\nCurrent balance: ${formatMoney(
|
||||
user?.balance ?? 0
|
||||
)}`}
|
||||
/>
|
||||
) : (betAmount ?? 0) > 10 && probChange >= 0.3 && bankrollFraction <= 1 ? (
|
||||
<AlertBox
|
||||
title="Whoa, there!"
|
||||
text={`Are you sure you want to move the market by ${displayedDifference}?`}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
)}`
|
||||
: (betAmount ?? 0) > 10 && probChange >= 0.3 && bankrollFraction <= 1
|
||||
? `Are you sure you want to move the market by ${displayedDifference}?`
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Col className={hidden ? 'hidden' : ''}>
|
||||
<div className="my-3 text-left text-sm text-gray-500">
|
||||
{isPseudoNumeric ? 'Direction' : 'Buy'}
|
||||
{isPseudoNumeric ? 'Direction' : 'Outcome'}
|
||||
</div>
|
||||
<YesNoSelector
|
||||
className="mb-4"
|
||||
|
@ -325,8 +312,6 @@ function BuyPanel(props: {
|
|||
showSliderOnMobile
|
||||
/>
|
||||
|
||||
{warning}
|
||||
|
||||
<Col className="mt-3 w-full gap-3">
|
||||
<Row className="items-center justify-between text-sm">
|
||||
<div className="text-gray-500">
|
||||
|
@ -367,20 +352,20 @@ function BuyPanel(props: {
|
|||
<Spacer h={8} />
|
||||
|
||||
{user && (
|
||||
<button
|
||||
className={clsx(
|
||||
<WarningConfirmationButton
|
||||
warning={warning}
|
||||
onSubmit={submitBet}
|
||||
isSubmitting={isSubmitting}
|
||||
disabled={!!betDisabled}
|
||||
openModalButtonClass={clsx(
|
||||
'btn mb-2 flex-1',
|
||||
betDisabled
|
||||
? 'btn-disabled'
|
||||
: outcome === 'YES'
|
||||
? 'btn-primary'
|
||||
: 'border-none bg-red-400 hover:bg-red-500',
|
||||
isSubmitting ? 'loading' : ''
|
||||
: 'border-none bg-red-400 hover:bg-red-500'
|
||||
)}
|
||||
onClick={betDisabled ? undefined : submitBet}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
|
||||
{wasSubmitted && <div className="mt-4">Trade submitted!</div>}
|
||||
|
@ -750,9 +735,7 @@ function QuickOrLimitBet(props: {
|
|||
|
||||
return (
|
||||
<Row className="align-center mb-4 justify-between">
|
||||
<div className="mr-2 -ml-2 shrink-0 text-3xl sm:-ml-0 sm:text-4xl">
|
||||
Trade
|
||||
</div>
|
||||
<div className="mr-2 -ml-2 shrink-0 text-3xl sm:-ml-0">Predict</div>
|
||||
{!hideToggle && (
|
||||
<Row className="mt-1 ml-1 items-center gap-1.5 sm:ml-0 sm:gap-2">
|
||||
<PillButton
|
||||
|
|
|
@ -13,8 +13,8 @@ export function PillButton(props: {
|
|||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'cursor-pointer select-none whitespace-nowrap rounded-full px-3 py-1.5 sm:text-sm',
|
||||
xs ? 'text-xs' : 'text-sm',
|
||||
'cursor-pointer select-none whitespace-nowrap rounded-full px-3 py-1.5 text-sm',
|
||||
xs ? 'text-xs' : '',
|
||||
selected
|
||||
? ['text-white', color ?? 'bg-greyscale-6']
|
||||
: 'bg-greyscale-2 hover:bg-greyscale-3'
|
||||
|
|
|
@ -4,7 +4,6 @@ import clsx from 'clsx'
|
|||
import { User } from 'common/user'
|
||||
import { useEffect, useState } from 'react'
|
||||
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 { Avatar } from './avatar'
|
||||
import { TextEditor, useTextEditor } from './editor'
|
||||
|
@ -80,7 +79,6 @@ export function CommentInputTextArea(props: {
|
|||
upload: Parameters<typeof TextEditor>[0]['upload']
|
||||
submitComment: (id?: string) => void
|
||||
isSubmitting: boolean
|
||||
submitOnEnter?: boolean
|
||||
presetId?: string
|
||||
}) {
|
||||
const {
|
||||
|
@ -90,11 +88,8 @@ export function CommentInputTextArea(props: {
|
|||
submitComment,
|
||||
presetId,
|
||||
isSubmitting,
|
||||
submitOnEnter,
|
||||
replyToUser,
|
||||
} = props
|
||||
const isMobile = (useWindowSize().width ?? 0) < 768 // TODO: base off input device (keybord vs touch)
|
||||
|
||||
useEffect(() => {
|
||||
editor?.setEditable(!isSubmitting)
|
||||
}, [isSubmitting, editor])
|
||||
|
@ -108,15 +103,14 @@ export function CommentInputTextArea(props: {
|
|||
if (!editor) {
|
||||
return
|
||||
}
|
||||
// submit on Enter key
|
||||
// Submit on ctrl+enter or mod+enter key
|
||||
editor.setOptions({
|
||||
editorProps: {
|
||||
handleKeyDown: (view, event) => {
|
||||
if (
|
||||
submitOnEnter &&
|
||||
event.key === 'Enter' &&
|
||||
!event.shiftKey &&
|
||||
(!isMobile || event.ctrlKey || event.metaKey) &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
// mention list is closed
|
||||
!(view.state as any).mention$.active
|
||||
) {
|
||||
|
|
|
@ -47,13 +47,13 @@ export function ConfirmationButton(props: {
|
|||
{children}
|
||||
<Row className="gap-4">
|
||||
<div
|
||||
className={clsx('btn normal-case', cancelBtn?.className)}
|
||||
className={clsx('btn', cancelBtn?.className)}
|
||||
onClick={() => updateOpen(false)}
|
||||
>
|
||||
{cancelBtn?.label ?? 'Cancel'}
|
||||
</div>
|
||||
<div
|
||||
className={clsx('btn normal-case', submitBtn?.className)}
|
||||
className={clsx('btn', submitBtn?.className)}
|
||||
onClick={
|
||||
onSubmitWithSuccess
|
||||
? () =>
|
||||
|
@ -69,7 +69,7 @@ export function ConfirmationButton(props: {
|
|||
</Col>
|
||||
</Modal>
|
||||
<div
|
||||
className={clsx('btn normal-case', openModalBtn.className)}
|
||||
className={clsx('btn', openModalBtn.className)}
|
||||
onClick={() => updateOpen(true)}
|
||||
>
|
||||
{openModalBtn.icon}
|
||||
|
|
|
@ -13,7 +13,6 @@ import { Tabs } from '../layout/tabs'
|
|||
import { Col } from '../layout/col'
|
||||
import { tradingAllowed } from 'web/lib/firebase/contracts'
|
||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||
import { useBets } from 'web/hooks/use-bets'
|
||||
import { useComments } from 'web/hooks/use-comments'
|
||||
import { useLiquidity } from 'web/hooks/use-liquidity'
|
||||
import { BetSignUpPrompt } from '../sign-up-prompt'
|
||||
|
@ -27,24 +26,23 @@ export function ContractTabs(props: {
|
|||
comments: ContractComment[]
|
||||
tips: CommentTipMap
|
||||
}) {
|
||||
const { contract, user, tips } = props
|
||||
const { contract, user, bets, tips } = props
|
||||
const { outcomeType } = contract
|
||||
|
||||
const bets = useBets(contract.id) ?? props.bets
|
||||
const lps = useLiquidity(contract.id) ?? []
|
||||
const lps = useLiquidity(contract.id)
|
||||
|
||||
const userBets =
|
||||
user && bets.filter((bet) => !bet.isAnte && bet.userId === user.id)
|
||||
const visibleBets = bets.filter(
|
||||
(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
|
||||
const updatedComments = useComments(contract.id)
|
||||
const comments = updatedComments ?? props.comments
|
||||
|
||||
const betActivity = (
|
||||
const betActivity = visibleLps && (
|
||||
<ContractBetsActivity
|
||||
contract={contract}
|
||||
bets={visibleBets}
|
||||
|
|
|
@ -114,6 +114,7 @@ export function CreatorContractsList(props: {
|
|||
additionalFilter={{
|
||||
creatorId: creator.id,
|
||||
}}
|
||||
persistPrefix={`user-${creator.id}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import { uploadImage } from 'web/lib/firebase/storage'
|
|||
import { useMutation } from 'react-query'
|
||||
import { FileUploadButton } from './file-upload-button'
|
||||
import { linkClass } from './site-link'
|
||||
import { useUsers } from 'web/hooks/use-users'
|
||||
import { mentionSuggestion } from './editor/mention-suggestion'
|
||||
import { DisplayMention } from './editor/mention'
|
||||
import Iframe from 'common/util/tiptap-iframe'
|
||||
|
@ -68,8 +67,6 @@ export function useTextEditor(props: {
|
|||
}) {
|
||||
const { placeholder, max, defaultValue = '', disabled, simple } = props
|
||||
|
||||
const users = useUsers()
|
||||
|
||||
const editorClass = clsx(
|
||||
proseClass,
|
||||
!simple && 'min-h-[6em]',
|
||||
|
@ -78,32 +75,27 @@ export function useTextEditor(props: {
|
|||
'[&_.ProseMirror-selectednode]:outline-dotted [&_*]:outline-indigo-300' // selected img, emebeds
|
||||
)
|
||||
|
||||
const editor = useEditor(
|
||||
{
|
||||
editorProps: { attributes: { class: editorClass } },
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: simple ? false : { levels: [1, 2, 3] },
|
||||
horizontalRule: simple ? false : {},
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
emptyEditorClass:
|
||||
'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0 cursor-text',
|
||||
}),
|
||||
CharacterCount.configure({ limit: max }),
|
||||
simple ? DisplayImage : Image,
|
||||
DisplayLink,
|
||||
DisplayMention.configure({
|
||||
suggestion: mentionSuggestion(users),
|
||||
}),
|
||||
Iframe,
|
||||
TiptapTweet,
|
||||
],
|
||||
content: defaultValue,
|
||||
},
|
||||
[!users.length] // passed as useEffect dependency. (re-render editor when users load, to update mention menu)
|
||||
)
|
||||
const editor = useEditor({
|
||||
editorProps: { attributes: { class: editorClass } },
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: simple ? false : { levels: [1, 2, 3] },
|
||||
horizontalRule: simple ? false : {},
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
emptyEditorClass:
|
||||
'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0 cursor-text',
|
||||
}),
|
||||
CharacterCount.configure({ limit: max }),
|
||||
simple ? DisplayImage : Image,
|
||||
DisplayLink,
|
||||
DisplayMention.configure({ suggestion: mentionSuggestion }),
|
||||
Iframe,
|
||||
TiptapTweet,
|
||||
],
|
||||
content: defaultValue,
|
||||
})
|
||||
|
||||
const upload = useUploadMutation(editor)
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { MentionOptions } from '@tiptap/extension-mention'
|
||||
import { ReactRenderer } from '@tiptap/react'
|
||||
import { User } from 'common/user'
|
||||
import { searchInAny } from 'common/util/parse'
|
||||
import { orderBy } from 'lodash'
|
||||
import tippy from 'tippy.js'
|
||||
import { getCachedUsers } from 'web/hooks/use-users'
|
||||
import { MentionList } from './mention-list'
|
||||
|
||||
type Suggestion = MentionOptions['suggestion']
|
||||
|
@ -12,10 +12,12 @@ const beginsWith = (text: string, query: string) =>
|
|||
text.toLocaleLowerCase().startsWith(query.toLocaleLowerCase())
|
||||
|
||||
// copied from https://tiptap.dev/api/nodes/mention#usage
|
||||
export const mentionSuggestion = (users: User[]): Suggestion => ({
|
||||
items: ({ query }) =>
|
||||
export const mentionSuggestion: Suggestion = {
|
||||
items: async ({ query }) =>
|
||||
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)),
|
||||
'followerCountCached',
|
||||
|
@ -38,7 +40,7 @@ export const mentionSuggestion = (users: User[]): Suggestion => ({
|
|||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
content: component?.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
|
@ -46,27 +48,27 @@ export const mentionSuggestion = (users: User[]): Suggestion => ({
|
|||
})
|
||||
},
|
||||
onUpdate(props) {
|
||||
component.updateProps(props)
|
||||
component?.updateProps(props)
|
||||
|
||||
if (!props.clientRect) {
|
||||
return
|
||||
}
|
||||
|
||||
popup[0].setProps({
|
||||
popup?.[0].setProps({
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide()
|
||||
popup?.[0].hide()
|
||||
return true
|
||||
}
|
||||
return (component.ref as any)?.onKeyDown(props)
|
||||
return (component?.ref as any)?.onKeyDown(props)
|
||||
},
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
component.destroy()
|
||||
popup?.[0].destroy()
|
||||
component?.destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import { getProbability } from 'common/calculate'
|
|||
import { track } from 'web/lib/service/analytics'
|
||||
import { Tipper } from '../tipper'
|
||||
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
|
||||
|
||||
import { Content } from '../editor'
|
||||
import { Editor } from '@tiptap/react'
|
||||
import { UserLink } from 'web/components/user-link'
|
||||
|
|
|
@ -22,7 +22,7 @@ export function GroupAboutPost(props: {
|
|||
const post = usePost(group.aboutPostId) ?? props.post
|
||||
|
||||
return (
|
||||
<div className="rounded-md bg-white p-4">
|
||||
<div className="rounded-md bg-white p-4 ">
|
||||
{isEditable ? (
|
||||
<RichEditGroupAboutPost group={group} post={post} />
|
||||
) : (
|
||||
|
|
|
@ -8,9 +8,10 @@ export function Modal(props: {
|
|||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||
position?: 'center' | 'top' | 'bottom'
|
||||
className?: string
|
||||
}) {
|
||||
const { children, open, setOpen, size = 'md', className } = props
|
||||
const { children, position, open, setOpen, size = 'md', className } = props
|
||||
|
||||
const sizeClass = {
|
||||
sm: 'max-w-sm',
|
||||
|
@ -19,6 +20,12 @@ export function Modal(props: {
|
|||
xl: 'max-w-5xl',
|
||||
}[size]
|
||||
|
||||
const positionClass = {
|
||||
center: 'items-center',
|
||||
top: 'items-start',
|
||||
bottom: 'items-end',
|
||||
}[position ?? 'bottom']
|
||||
|
||||
return (
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog
|
||||
|
@ -26,7 +33,12 @@ export function Modal(props: {
|
|||
className="fixed inset-0 z-50 overflow-y-auto"
|
||||
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
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
|
|
|
@ -19,7 +19,7 @@ export function BetSignUpPrompt(props: {
|
|||
size={size}
|
||||
color="gradient"
|
||||
>
|
||||
{label ?? 'Sign up to trade!'}
|
||||
{label ?? 'Sign up to predict!'}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
|
|
74
web/components/warning-confirmation-button.tsx
Normal file
74
web/components/warning-confirmation-button.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -13,6 +13,7 @@ import {
|
|||
getUserBetContractsQuery,
|
||||
} from 'web/lib/firebase/contracts'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { MINUTE_MS } from 'common/util/time'
|
||||
|
||||
export const useContracts = () => {
|
||||
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
||||
|
@ -96,8 +97,10 @@ export const useUpdatedContracts = (contracts: Contract[] | undefined) => {
|
|||
|
||||
export const usePrefetchUserBetContracts = (userId: string) => {
|
||||
const queryClient = useQueryClient()
|
||||
return queryClient.prefetchQuery(['contracts', 'bets', userId], () =>
|
||||
getUserBetContracts(userId)
|
||||
return queryClient.prefetchQuery(
|
||||
['contracts', 'bets', userId],
|
||||
() => getUserBetContracts(userId),
|
||||
{ staleTime: 5 * MINUTE_MS }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useQueryClient } from 'react-query'
|
||||
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 {
|
||||
getPortfolioHistory,
|
||||
getPortfolioHistoryQuery,
|
||||
|
@ -15,8 +15,10 @@ const getCutoff = (period: Period) => {
|
|||
export const usePrefetchPortfolioHistory = (userId: string, period: Period) => {
|
||||
const queryClient = useQueryClient()
|
||||
const cutoff = getCutoff(period)
|
||||
return queryClient.prefetchQuery(['portfolio-history', userId, cutoff], () =>
|
||||
getPortfolioHistory(userId, cutoff)
|
||||
return queryClient.prefetchQuery(
|
||||
['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 result = useFirestoreQueryData(
|
||||
['portfolio-history', userId, cutoff],
|
||||
getPortfolioHistoryQuery(userId, cutoff)
|
||||
getPortfolioHistoryQuery(userId, cutoff),
|
||||
{},
|
||||
{ staleTime: 15 * MINUTE_MS }
|
||||
)
|
||||
return result.data
|
||||
}
|
||||
|
|
|
@ -7,10 +7,15 @@ import {
|
|||
getUserBetsQuery,
|
||||
listenForUserContractBets,
|
||||
} from 'web/lib/firebase/bets'
|
||||
import { MINUTE_MS } from 'common/util/time'
|
||||
|
||||
export const usePrefetchUserBets = (userId: string) => {
|
||||
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) => {
|
||||
|
|
|
@ -6,7 +6,8 @@ import { useFollows } from './use-follows'
|
|||
import { useUser } from './use-user'
|
||||
import { useFirestoreQueryData } from '@react-query-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 = () => {
|
||||
const result = useFirestoreQueryData<DocumentData, User[]>(['users'], users, {
|
||||
|
@ -16,6 +17,10 @@ export const useUsers = () => {
|
|||
return result.data ?? []
|
||||
}
|
||||
|
||||
const q = new QueryClient()
|
||||
export const getCachedUsers = async () =>
|
||||
q.fetchQuery(['users'], getUsers, { staleTime: Infinity })
|
||||
|
||||
export const usePrivateUsers = () => {
|
||||
const result = useFirestoreQueryData<DocumentData, PrivateUser[]>(
|
||||
['private users'],
|
||||
|
|
|
@ -12,3 +12,7 @@ export function isIOS() {
|
|||
(navigator.userAgent.includes('Mac') && 'ontouchend' in document)
|
||||
)
|
||||
}
|
||||
|
||||
export function isAndroid() {
|
||||
return navigator.userAgent.includes('Android')
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Comment } from 'common/comment'
|
|||
import { Contract } from 'common/contract'
|
||||
import { User } from 'common/user'
|
||||
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 { richTextToString } from 'common/util/parse'
|
||||
|
||||
|
@ -121,7 +121,7 @@ export function toLiteMarket(contract: Contract): LiteMarket {
|
|||
: closeTime,
|
||||
question,
|
||||
tags,
|
||||
url: `https://manifold.markets/${creatorUsername}/${slug}`,
|
||||
url: `https://${DOMAIN}/${creatorUsername}/${slug}`,
|
||||
pool,
|
||||
probability,
|
||||
p,
|
||||
|
|
|
@ -178,7 +178,7 @@ export default function Charity(props: {
|
|||
className="input input-bordered mb-6 w-full"
|
||||
/>
|
||||
</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) => (
|
||||
<CharityCard
|
||||
charity={charity}
|
||||
|
@ -203,18 +203,26 @@ export default function Charity(props: {
|
|||
></iframe>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 text-gray-500">
|
||||
<span className="font-semibold">Notes</span>
|
||||
<br />
|
||||
- Don't see your favorite charity? Recommend it by emailing
|
||||
charity@manifold.markets!
|
||||
<br />
|
||||
- Manifold is not affiliated with non-Featured charities; we're just
|
||||
fans of their work.
|
||||
<br />
|
||||
- As Manifold itself is a for-profit entity, your contributions will
|
||||
not be tax deductible.
|
||||
<br />- Donations + matches are wired once each quarter.
|
||||
<div className="prose mt-10 max-w-none text-gray-500">
|
||||
<span className="text-lg font-semibold">Notes</span>
|
||||
<ul>
|
||||
<li>
|
||||
Don't see your favorite charity? Recommend it by emailing{' '}
|
||||
<a href="mailto:charity@manifold.markets?subject=Add%20Charity">
|
||||
charity@manifold.markets
|
||||
</a>
|
||||
!
|
||||
</li>
|
||||
<li>
|
||||
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>
|
||||
</Col>
|
||||
</Page>
|
||||
|
|
|
@ -91,7 +91,7 @@ const Home = () => {
|
|||
|
||||
function SearchSection(props: {
|
||||
label: string
|
||||
user: User | null | undefined
|
||||
user: User | null | undefined | undefined
|
||||
sort: Sort
|
||||
yourBets?: 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
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
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 { Title } from 'web/components/title'
|
||||
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 { ShareIcon } from '@heroicons/react/solid'
|
||||
import { PencilIcon, ShareIcon } from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import { Button } from 'web/components/button'
|
||||
import { useState } from 'react'
|
||||
|
@ -22,6 +22,8 @@ import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
|||
import { groupBy, sortBy } from 'lodash'
|
||||
import { PostCommentInput, PostCommentThread } from 'web/posts/post-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[] } }) {
|
||||
const { slugs } = props.params
|
||||
|
@ -51,12 +53,14 @@ export default function PostPage(props: {
|
|||
comments: PostComment[]
|
||||
}) {
|
||||
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 shareUrl = `https://${ENV_CONFIG.domain}${postPath(post.slug)}`
|
||||
const updatedComments = useCommentsOnPost(post.id)
|
||||
const comments = updatedComments ?? props.comments
|
||||
const user = useUser()
|
||||
|
||||
if (post == null) {
|
||||
return <Custom404 />
|
||||
|
@ -65,10 +69,9 @@ export default function PostPage(props: {
|
|||
return (
|
||||
<Page>
|
||||
<div className="mx-auto w-full max-w-3xl ">
|
||||
<Spacer h={1} />
|
||||
<Title className="!mt-0" text={post.title} />
|
||||
<Title className="!mt-0 py-4 px-2" text={post.title} />
|
||||
<Row>
|
||||
<Col className="flex-1">
|
||||
<Col className="flex-1 px-2">
|
||||
<div className={'inline-flex'}>
|
||||
<div className="mr-1 text-gray-500">Created by</div>
|
||||
<UserLink
|
||||
|
@ -78,7 +81,7 @@ export default function PostPage(props: {
|
|||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<Col className="px-2">
|
||||
<Button
|
||||
size="lg"
|
||||
color="gray-white"
|
||||
|
@ -104,11 +107,15 @@ export default function PostPage(props: {
|
|||
<Spacer h={2} />
|
||||
<div className="rounded-lg bg-white px-6 py-4 sm:py-0">
|
||||
<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>
|
||||
|
||||
<Spacer h={2} />
|
||||
<Spacer h={4} />
|
||||
<div className="rounded-lg bg-white px-6 py-4 sm:py-0">
|
||||
<PostCommentsActivity
|
||||
post={post}
|
||||
|
@ -137,7 +144,7 @@ export function PostCommentsActivity(props: {
|
|||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col className="p-2">
|
||||
<PostCommentInput post={post} />
|
||||
{topLevelComments.map((parent) => (
|
||||
<PostCommentThread
|
||||
|
@ -153,6 +160,68 @@ export function PostCommentsActivity(props: {
|
|||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -77,13 +77,21 @@ const Salem = {
|
|||
|
||||
const tourneys: Tourney[] = [
|
||||
{
|
||||
title: 'Cause Exploration Prizes',
|
||||
title: 'Manifold F2P Tournament',
|
||||
blurb:
|
||||
'Which new charity ideas will Open Philanthropy find most promising?',
|
||||
award: 'M$100k',
|
||||
endTime: toDate('Sep 9, 2022'),
|
||||
groupId: 'cMcpBQ2p452jEcJD2SFw',
|
||||
'Who can amass the most mana starting from a free-to-play (F2P) account?',
|
||||
award: 'Poem',
|
||||
endTime: toDate('Sep 15, 2022'),
|
||||
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',
|
||||
blurb: 'How many points will each NFL player score this season?',
|
||||
|
@ -91,13 +99,6 @@ const tourneys: Tourney[] = [
|
|||
endTime: toDate('Jan 6, 2023'),
|
||||
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',
|
||||
// blurb: 'Something amazing',
|
||||
|
@ -105,6 +106,27 @@ const tourneys: Tourney[] = [
|
|||
// endTime: toDate('Sep 22, 2022'),
|
||||
// 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 = {
|
||||
|
@ -135,20 +157,23 @@ export default function TournamentPage(props: { sections: SectionInfo[] }) {
|
|||
title="Tournaments"
|
||||
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%]">
|
||||
{sections.map(({ tourney, slug, numPeople }) => (
|
||||
<div key={slug}>
|
||||
<SectionHeader
|
||||
url={groupPath(slug)}
|
||||
title={tourney.title}
|
||||
ppl={numPeople}
|
||||
award={tourney.award}
|
||||
endTime={tourney.endTime}
|
||||
/>
|
||||
<span>{tourney.blurb}</span>
|
||||
<MarketCarousel slug={slug} />
|
||||
</div>
|
||||
))}
|
||||
<Col className="m-4 gap-10 sm:mx-10 sm:gap-24 xl:w-[125%]">
|
||||
{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>
|
||||
)
|
||||
)}
|
||||
<div>
|
||||
<SectionHeader
|
||||
url={Salem.url}
|
||||
|
@ -156,9 +181,52 @@ export default function TournamentPage(props: { sections: SectionInfo[] }) {
|
|||
award={Salem.award}
|
||||
endTime={Salem.endTime}
|
||||
/>
|
||||
<span>{Salem.blurb}</span>
|
||||
<span className="text-gray-500">{Salem.blurb}</span>
|
||||
<ImageCarousel url={Salem.url} images={Salem.images} />
|
||||
</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>
|
||||
</Page>
|
||||
)
|
||||
|
@ -175,9 +243,7 @@ const SectionHeader = (props: {
|
|||
return (
|
||||
<Link href={url}>
|
||||
<a className="group mb-3 flex flex-wrap justify-between">
|
||||
<h2 className="text-xl font-semibold group-hover:underline md:text-3xl">
|
||||
{title}
|
||||
</h2>
|
||||
<h2 className="text-xl group-hover:underline md:text-3xl">{title}</h2>
|
||||
<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>}
|
||||
{!!ppl && (
|
||||
|
|
|
@ -64,6 +64,8 @@ function putIntoMapAndFetch(data) {
|
|||
document.getElementById('guess-type').innerText = 'Finding Fantastic Beasts'
|
||||
} else if (whichGuesser === 'basic') {
|
||||
document.getElementById('guess-type').innerText = 'How Basic'
|
||||
} else if (whichGuesser === 'commander') {
|
||||
document.getElementById('guess-type').innerText = 'General Knowledge'
|
||||
}
|
||||
setUpNewGame()
|
||||
}
|
||||
|
@ -156,8 +158,8 @@ function determineIfSkip(card) {
|
|||
if (card.flavor_name) {
|
||||
return true
|
||||
}
|
||||
// don't include racist cards
|
||||
return card.content_warning
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function putIntoMap(data) {
|
||||
|
|
|
@ -3,16 +3,16 @@ import requests
|
|||
import json
|
||||
|
||||
# add category name here
|
||||
allCategories = ['counterspell', 'beast', 'burn'] #, 'terror', 'wrath']
|
||||
allCategories = ['counterspell', 'beast', 'burn', 'commander', 'artist'] #, 'terror', 'wrath', 'zombie', 'artifact']
|
||||
specialCategories = ['set', 'basic']
|
||||
|
||||
|
||||
def generate_initial_query(category):
|
||||
string_query = 'https://api.scryfall.com/cards/search?q='
|
||||
if category == 'counterspell':
|
||||
string_query += 'otag%3Acounterspell+t%3Ainstant+not%3Aadventure'
|
||||
string_query += 'otag%3Acounterspell+t%3Ainstant+not%3Aadventure+not%3Adfc'
|
||||
elif category == 'beast':
|
||||
string_query += '-type%3Alegendary+type%3Abeast+-type%3Atoken'
|
||||
string_query += '-type%3Alegendary+type%3Abeast+-type%3Atoken+not%3Adfc'
|
||||
# elif category == 'terror':
|
||||
# string_query += 'otag%3Acreature-removal+o%3A%2Fdestroy+target.%2A+%28creature%7Cpermanent%29%2F+%28t' \
|
||||
# '%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' \
|
||||
'%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' \
|
||||
'+%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
|
||||
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' \
|
||||
'%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='
|
||||
print(string_query)
|
||||
return string_query
|
||||
|
@ -89,22 +97,35 @@ def fetch_special(query):
|
|||
|
||||
|
||||
def to_compact_write_form(smallJson, art_names, response, category):
|
||||
fieldsInCard = ['name', 'image_uris', 'content_warning', 'flavor_name', 'reprint', 'frame_effects', 'digital',
|
||||
'set_type']
|
||||
fieldsInCard = ['name', 'image_uris', 'flavor_name', 'reprint', 'frame_effects', 'digital', 'set_type']
|
||||
data = []
|
||||
# write all fields needed in card
|
||||
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
|
||||
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
|
||||
else:
|
||||
art_names.add(card['illustration_id'])
|
||||
write_card = dict()
|
||||
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:
|
||||
write_card['name'] = card['card_faces'][0]['name']
|
||||
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:
|
||||
write_card[field] = card[field]
|
||||
data.append(write_card)
|
||||
|
@ -115,6 +136,9 @@ def to_compact_write_form_special(smallJson, art_names, response, category):
|
|||
data = []
|
||||
# write all fields needed in card
|
||||
for card in response['data']:
|
||||
# do not include racist cards
|
||||
if 'content_warning' in card and card['content_warning'] == True:
|
||||
continue
|
||||
if category == 'basic':
|
||||
write_card = dict()
|
||||
# do not repeat art
|
||||
|
@ -152,9 +176,9 @@ def write_image_uris(card_image_uris):
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# for category in allCategories:
|
||||
# print(category)
|
||||
# fetch_and_write_all(category, generate_initial_query(category))
|
||||
for category in allCategories:
|
||||
print(category)
|
||||
fetch_and_write_all(category, generate_initial_query(category))
|
||||
for category in specialCategories:
|
||||
print(category)
|
||||
fetch_and_write_all_special(category, generate_initial_special_query(category))
|
||||
|
|
|
@ -17,6 +17,14 @@
|
|||
f.parentNode.insertBefore(j, f)
|
||||
})(window, document, 'script', 'dataLayer', 'GTM-M3MBVGG')
|
||||
</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 -->
|
||||
<meta charset="UTF-8" />
|
||||
<style type="text/css">
|
||||
|
@ -105,6 +113,18 @@
|
|||
list-style: none;
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -125,48 +145,115 @@
|
|||
action="guess.html"
|
||||
style="display: flex; flex-direction: column; align-items: center"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="counterspell"
|
||||
name="whichguesser"
|
||||
value="counterspell"
|
||||
checked
|
||||
/>
|
||||
<label class="radio-label" for="counterspell">
|
||||
<img
|
||||
class="thumbnail"
|
||||
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/71cfcba5-1571-48b8-a3db-55dca135506e.jpg?1562843855"
|
||||
<div class="option-row">
|
||||
<input
|
||||
type="radio"
|
||||
id="counterspell"
|
||||
name="whichguesser"
|
||||
value="counterspell"
|
||||
onchange="updateSettingDefault(true, true, false)"
|
||||
checked
|
||||
/>
|
||||
<h3>Counterspell Guesser</h3></label
|
||||
><br />
|
||||
|
||||
<input type="radio" id="burn" name="whichguesser" value="burn" />
|
||||
<label class="radio-label" for="burn">
|
||||
<label class="radio-label" for="counterspell">
|
||||
<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
|
||||
>
|
||||
<img
|
||||
class="thumbnail"
|
||||
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60b2fae1-242b-45e0-a757-b1adc02c06f3.jpg?1562760596"
|
||||
class="level-badge"
|
||||
src="https://static.wikia.nocookie.net/mtgsalvation_gamepedia/images/a/a8/Advanced_level.jpg"
|
||||
/>
|
||||
<h3>Match With Hot Singles</h3></label
|
||||
><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
|
||||
>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<input type="radio" id="basic" name="whichguesser" value="basic" />
|
||||
<label class="radio-label" for="basic">
|
||||
<img
|
||||
class="thumbnail"
|
||||
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/03683fbb-9843-4c14-bb95-387150e97c90.jpg?1642161346"
|
||||
<div class="option-row">
|
||||
<input
|
||||
type="radio"
|
||||
id="burn"
|
||||
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 />
|
||||
|
||||
<details id="addl-options">
|
||||
|
|
1
web/public/mtg/jsons/artist.json
Normal file
1
web/public/mtg/jsons/artist.json
Normal file
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
1
web/public/mtg/jsons/commander.json
Normal file
1
web/public/mtg/jsons/commander.json
Normal file
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
|
@ -60,6 +60,18 @@ module.exports = {
|
|||
'overflow-wrap': 'anywhere',
|
||||
'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',
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue
Block a user