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
|
'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',
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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`
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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 />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -114,6 +114,7 @@ export function CreatorContractsList(props: {
|
||||||
additionalFilter={{
|
additionalFilter={{
|
||||||
creatorId: creator.id,
|
creatorId: creator.id,
|
||||||
}}
|
}}
|
||||||
|
persistPrefix={`user-${creator.id}`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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} />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
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,
|
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 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -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')
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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">
|
||||||
|
|
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',
|
'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',
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in New Issue
Block a user