Merge remote-tracking branch 'origin/main' into fix-large-groups
This commit is contained in:
commit
82772cb733
|
@ -116,12 +116,12 @@ const calculateProfitForPeriod = (
|
||||||
return currentProfit
|
return currentProfit
|
||||||
}
|
}
|
||||||
|
|
||||||
const startingProfit = calculateTotalProfit(startingPortfolio)
|
const startingProfit = calculatePortfolioProfit(startingPortfolio)
|
||||||
|
|
||||||
return currentProfit - startingProfit
|
return currentProfit - startingProfit
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateTotalProfit = (portfolio: PortfolioMetrics) => {
|
export const calculatePortfolioProfit = (portfolio: PortfolioMetrics) => {
|
||||||
return portfolio.investmentValue + portfolio.balance - portfolio.totalDeposits
|
return portfolio.investmentValue + portfolio.balance - portfolio.totalDeposits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ export const calculateNewProfit = (
|
||||||
portfolioHistory: PortfolioMetrics[],
|
portfolioHistory: PortfolioMetrics[],
|
||||||
newPortfolio: PortfolioMetrics
|
newPortfolio: PortfolioMetrics
|
||||||
) => {
|
) => {
|
||||||
const allTimeProfit = calculateTotalProfit(newPortfolio)
|
const allTimeProfit = calculatePortfolioProfit(newPortfolio)
|
||||||
const descendingPortfolio = sortBy(
|
const descendingPortfolio = sortBy(
|
||||||
portfolioHistory,
|
portfolioHistory,
|
||||||
(p) => p.timestamp
|
(p) => p.timestamp
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { JSONContent } from '@tiptap/core'
|
import type { JSONContent } from '@tiptap/core'
|
||||||
|
|
||||||
export type AnyCommentType = OnContract | OnGroup
|
export type AnyCommentType = OnContract | OnGroup | OnPost
|
||||||
|
|
||||||
// Currently, comments are created after the bet, not atomically with the bet.
|
// Currently, comments are created after the bet, not atomically with the bet.
|
||||||
// They're uniquely identified by the pair contractId/betId.
|
// They're uniquely identified by the pair contractId/betId.
|
||||||
|
@ -20,7 +20,7 @@ export type Comment<T extends AnyCommentType = AnyCommentType> = {
|
||||||
userAvatarUrl?: string
|
userAvatarUrl?: string
|
||||||
} & T
|
} & T
|
||||||
|
|
||||||
type OnContract = {
|
export type OnContract = {
|
||||||
commentType: 'contract'
|
commentType: 'contract'
|
||||||
contractId: string
|
contractId: string
|
||||||
answerOutcome?: string
|
answerOutcome?: string
|
||||||
|
@ -35,10 +35,16 @@ type OnContract = {
|
||||||
betOutcome?: string
|
betOutcome?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type OnGroup = {
|
export type OnGroup = {
|
||||||
commentType: 'group'
|
commentType: 'group'
|
||||||
groupId: string
|
groupId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OnPost = {
|
||||||
|
commentType: 'post'
|
||||||
|
postId: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ContractComment = Comment<OnContract>
|
export type ContractComment = Comment<OnContract>
|
||||||
export type GroupComment = Comment<OnGroup>
|
export type GroupComment = Comment<OnGroup>
|
||||||
|
export type PostComment = Comment<OnPost>
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -34,7 +34,7 @@ export type User = {
|
||||||
followerCountCached: number
|
followerCountCached: number
|
||||||
|
|
||||||
followedCategories?: string[]
|
followedCategories?: string[]
|
||||||
homeSections?: { visible: string[]; hidden: string[] }
|
homeSections?: string[]
|
||||||
|
|
||||||
referredByUserId?: string
|
referredByUserId?: string
|
||||||
referredByContractId?: string
|
referredByContractId?: string
|
||||||
|
|
|
@ -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'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,6 +205,10 @@ service cloud.firestore {
|
||||||
.affectedKeys()
|
.affectedKeys()
|
||||||
.hasOnly(['name', 'content']);
|
.hasOnly(['name', 'content']);
|
||||||
allow delete: if isAdmin() || request.auth.uid == resource.data.creatorId;
|
allow delete: if isAdmin() || request.auth.uid == resource.data.creatorId;
|
||||||
|
match /comments/{commentId} {
|
||||||
|
allow read;
|
||||||
|
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) ;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -135,7 +135,7 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
!isFinite(newP) ||
|
!isFinite(newP) ||
|
||||||
Math.min(...Object.values(newPool ?? {})) < CPMM_MIN_POOL_QTY)
|
Math.min(...Object.values(newPool ?? {})) < CPMM_MIN_POOL_QTY)
|
||||||
) {
|
) {
|
||||||
throw new APIError(400, 'Bet too large for current liquidity pool.')
|
throw new APIError(400, 'Trade too large for current liquidity pool.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const betDoc = contractDoc.collection('bets').doc()
|
const betDoc = contractDoc.collection('bets').doc()
|
||||||
|
|
|
@ -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,11 +109,20 @@ 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">
|
||||||
<div className="text-xl">
|
<div className="text-xl">
|
||||||
Bet on {isModal ? `"${answer.text}"` : 'this answer'}
|
Buy answer: {isModal ? `"${answer.text}"` : 'this answer'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isModal && (
|
{!isModal && (
|
||||||
|
@ -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 trade'}
|
|
||||||
</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}
|
||||||
|
|
|
@ -12,47 +12,33 @@ import { User } from 'common/user'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
|
|
||||||
export function ArrangeHome(props: {
|
export function ArrangeHome(props: {
|
||||||
user: User | null
|
user: User | null | undefined
|
||||||
homeSections: { visible: string[]; hidden: string[] }
|
homeSections: string[]
|
||||||
setHomeSections: (homeSections: {
|
setHomeSections: (sections: string[]) => void
|
||||||
visible: string[]
|
|
||||||
hidden: string[]
|
|
||||||
}) => void
|
|
||||||
}) {
|
}) {
|
||||||
const { user, homeSections, setHomeSections } = props
|
const { user, homeSections, setHomeSections } = props
|
||||||
|
|
||||||
const groups = useMemberGroups(user?.id) ?? []
|
const groups = useMemberGroups(user?.id) ?? []
|
||||||
const { itemsById, visibleItems, hiddenItems } = getHomeItems(
|
const { itemsById, sections } = getHomeItems(groups, homeSections)
|
||||||
groups,
|
|
||||||
homeSections
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext
|
<DragDropContext
|
||||||
onDragEnd={(e) => {
|
onDragEnd={(e) => {
|
||||||
console.log('drag end', e)
|
|
||||||
const { destination, source, draggableId } = e
|
const { destination, source, draggableId } = e
|
||||||
if (!destination) return
|
if (!destination) return
|
||||||
|
|
||||||
const item = itemsById[draggableId]
|
const item = itemsById[draggableId]
|
||||||
|
|
||||||
const newHomeSections = {
|
const newHomeSections = sections.map((section) => section.id)
|
||||||
visible: visibleItems.map((item) => item.id),
|
|
||||||
hidden: hiddenItems.map((item) => item.id),
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceSection = source.droppableId as 'visible' | 'hidden'
|
newHomeSections.splice(source.index, 1)
|
||||||
newHomeSections[sourceSection].splice(source.index, 1)
|
newHomeSections.splice(destination.index, 0, item.id)
|
||||||
|
|
||||||
const destSection = destination.droppableId as 'visible' | 'hidden'
|
|
||||||
newHomeSections[destSection].splice(destination.index, 0, item.id)
|
|
||||||
|
|
||||||
setHomeSections(newHomeSections)
|
setHomeSections(newHomeSections)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Row className="relative max-w-lg gap-4">
|
<Row className="relative max-w-md gap-4">
|
||||||
<DraggableList items={visibleItems} title="Visible" />
|
<DraggableList items={sections} title="Sections" />
|
||||||
<DraggableList items={hiddenItems} title="Hidden" />
|
|
||||||
</Row>
|
</Row>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
)
|
)
|
||||||
|
@ -65,16 +51,13 @@ function DraggableList(props: {
|
||||||
const { title, items } = props
|
const { title, items } = props
|
||||||
return (
|
return (
|
||||||
<Droppable droppableId={title.toLowerCase()}>
|
<Droppable droppableId={title.toLowerCase()}>
|
||||||
{(provided, snapshot) => (
|
{(provided) => (
|
||||||
<Col
|
<Col
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
className={clsx(
|
className={clsx('flex-1 items-stretch gap-1 rounded bg-gray-100 p-4')}
|
||||||
'width-[220px] flex-1 items-start rounded bg-gray-50 p-2',
|
|
||||||
snapshot.isDraggingOver && 'bg-gray-100'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Subtitle text={title} className="mx-2 !my-2" />
|
<Subtitle text={title} className="mx-2 !mt-0 !mb-4" />
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<Draggable key={item.id} draggableId={item.id} index={index}>
|
<Draggable key={item.id} draggableId={item.id} index={index}>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
|
@ -83,16 +66,13 @@ function DraggableList(props: {
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
style={provided.draggableProps.style}
|
style={provided.draggableProps.style}
|
||||||
className={clsx(
|
|
||||||
'flex flex-row items-center gap-4 rounded bg-gray-50 p-2',
|
|
||||||
snapshot.isDragging && 'z-[9000] bg-gray-300'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<MenuIcon
|
<SectionItem
|
||||||
className="h-5 w-5 flex-shrink-0 text-gray-500"
|
className={clsx(
|
||||||
aria-hidden="true"
|
snapshot.isDragging && 'z-[9000] bg-gray-200'
|
||||||
/>{' '}
|
)}
|
||||||
{item.label}
|
item={item}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
|
@ -104,15 +84,33 @@ function DraggableList(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getHomeItems = (
|
const SectionItem = (props: {
|
||||||
groups: Group[],
|
item: { id: string; label: string }
|
||||||
homeSections: { visible: string[]; hidden: string[] }
|
className?: string
|
||||||
) => {
|
}) => {
|
||||||
|
const { item, className } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
'flex flex-row items-center gap-4 rounded bg-gray-50 p-2'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MenuIcon
|
||||||
|
className="h-5 w-5 flex-shrink-0 text-gray-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>{' '}
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHomeItems = (groups: Group[], sections: string[]) => {
|
||||||
const items = [
|
const items = [
|
||||||
|
{ label: 'Daily movers', id: 'daily-movers' },
|
||||||
{ label: 'Trending', id: 'score' },
|
{ label: 'Trending', id: 'score' },
|
||||||
{ label: 'Newest', id: 'newest' },
|
{ label: 'New for you', id: 'newest' },
|
||||||
{ label: 'Close date', id: 'close-date' },
|
|
||||||
{ label: 'Your bets', id: 'your-bets' },
|
|
||||||
...groups.map((g) => ({
|
...groups.map((g) => ({
|
||||||
label: g.name,
|
label: g.name,
|
||||||
id: g.id,
|
id: g.id,
|
||||||
|
@ -120,23 +118,13 @@ export const getHomeItems = (
|
||||||
]
|
]
|
||||||
const itemsById = keyBy(items, 'id')
|
const itemsById = keyBy(items, 'id')
|
||||||
|
|
||||||
const { visible, hidden } = homeSections
|
const sectionItems = filterDefined(sections.map((id) => itemsById[id]))
|
||||||
|
|
||||||
const [visibleItems, hiddenItems] = [
|
// Add unmentioned items to the end.
|
||||||
filterDefined(visible.map((id) => itemsById[id])),
|
sectionItems.push(...items.filter((item) => !sectionItems.includes(item)))
|
||||||
filterDefined(hidden.map((id) => itemsById[id])),
|
|
||||||
]
|
|
||||||
|
|
||||||
// Add unmentioned items to the visible list.
|
|
||||||
visibleItems.push(
|
|
||||||
...items.filter(
|
|
||||||
(item) => !visibleItems.includes(item) && !hiddenItems.includes(item)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
visibleItems,
|
sections: sectionItems,
|
||||||
hiddenItems,
|
|
||||||
itemsById,
|
itemsById,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Router from 'next/router'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { MouseEvent, useEffect, useState } from 'react'
|
import { MouseEvent, useEffect, useState } from 'react'
|
||||||
import { UserCircleIcon, UserIcon, UsersIcon } from '@heroicons/react/solid'
|
import { UserCircleIcon, UserIcon, UsersIcon } from '@heroicons/react/solid'
|
||||||
|
import Image from 'next/future/image'
|
||||||
|
|
||||||
export function Avatar(props: {
|
export function Avatar(props: {
|
||||||
username?: string
|
username?: string
|
||||||
|
@ -14,6 +15,7 @@ export function Avatar(props: {
|
||||||
const [avatarUrl, setAvatarUrl] = useState(props.avatarUrl)
|
const [avatarUrl, setAvatarUrl] = useState(props.avatarUrl)
|
||||||
useEffect(() => setAvatarUrl(props.avatarUrl), [props.avatarUrl])
|
useEffect(() => setAvatarUrl(props.avatarUrl), [props.avatarUrl])
|
||||||
const s = size == 'xs' ? 6 : size === 'sm' ? 8 : size || 10
|
const s = size == 'xs' ? 6 : size === 'sm' ? 8 : size || 10
|
||||||
|
const sizeInPx = s * 4
|
||||||
|
|
||||||
const onClick =
|
const onClick =
|
||||||
noLink && username
|
noLink && username
|
||||||
|
@ -26,7 +28,9 @@ export function Avatar(props: {
|
||||||
// there can be no avatar URL or username in the feed, we show a "submit comment"
|
// there can be no avatar URL or username in the feed, we show a "submit comment"
|
||||||
// item with a fake grey user circle guy even if you aren't signed in
|
// item with a fake grey user circle guy even if you aren't signed in
|
||||||
return avatarUrl ? (
|
return avatarUrl ? (
|
||||||
<img
|
<Image
|
||||||
|
width={sizeInPx}
|
||||||
|
height={sizeInPx}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-shrink-0 rounded-full bg-white object-cover',
|
'flex-shrink-0 rounded-full bg-white object-cover',
|
||||||
`w-${s} h-${s}`,
|
`w-${s} h-${s}`,
|
||||||
|
|
|
@ -35,10 +35,13 @@ export default function BetButton(props: {
|
||||||
{user ? (
|
{user ? (
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
className={clsx('my-auto inline-flex min-w-[75px] ', btnClassName)}
|
className={clsx(
|
||||||
|
'my-auto inline-flex min-w-[75px] whitespace-nowrap',
|
||||||
|
btnClassName
|
||||||
|
)}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
Bet
|
Predict
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<BetSignUpPrompt />
|
<BetSignUpPrompt />
|
||||||
|
@ -57,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,18 +185,14 @@ 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)
|
||||||
|
|
||||||
|
if (!isIOS() && !isAndroid()) {
|
||||||
focusAmountInput()
|
focusAmountInput()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onBetChange(newAmount: number | undefined) {
|
function onBetChange(newAmount: number | undefined) {
|
||||||
setWasSubmitted(false)
|
setWasSubmitted(false)
|
||||||
|
@ -274,25 +271,15 @@ 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 bet. \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' : ''}>
|
||||||
|
@ -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,23 +352,23 @@ 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 bet'}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{wasSubmitted && <div className="mt-4">Bet submitted!</div>}
|
{wasSubmitted && <div className="mt-4">Trade submitted!</div>}
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -569,7 +554,7 @@ function LimitOrderPanel(props: {
|
||||||
<Row className="mt-1 items-center gap-4">
|
<Row className="mt-1 items-center gap-4">
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<div className="relative ml-1 text-sm text-gray-500">
|
<div className="relative ml-1 text-sm text-gray-500">
|
||||||
Bet {isPseudoNumeric ? <HigherLabel /> : <YesLabel />} up to
|
Buy {isPseudoNumeric ? <HigherLabel /> : <YesLabel />} up to
|
||||||
</div>
|
</div>
|
||||||
<ProbabilityOrNumericInput
|
<ProbabilityOrNumericInput
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -580,7 +565,7 @@ function LimitOrderPanel(props: {
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<div className="ml-1 text-sm text-gray-500">
|
<div className="ml-1 text-sm text-gray-500">
|
||||||
Bet {isPseudoNumeric ? <LowerLabel /> : <NoLabel />} down to
|
Buy {isPseudoNumeric ? <LowerLabel /> : <NoLabel />} down to
|
||||||
</div>
|
</div>
|
||||||
<ProbabilityOrNumericInput
|
<ProbabilityOrNumericInput
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -750,15 +735,16 @@ function QuickOrLimitBet(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="align-center mb-4 justify-between">
|
<Row className="align-center mb-4 justify-between">
|
||||||
<div className="text-4xl">Bet</div>
|
<div className="mr-2 -ml-2 shrink-0 text-3xl sm:-ml-0">Predict</div>
|
||||||
{!hideToggle && (
|
{!hideToggle && (
|
||||||
<Row className="mt-1 items-center gap-2">
|
<Row className="mt-1 ml-1 items-center gap-1.5 sm:ml-0 sm:gap-2">
|
||||||
<PillButton
|
<PillButton
|
||||||
selected={!isLimitOrder}
|
selected={!isLimitOrder}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setIsLimitOrder(false)
|
setIsLimitOrder(false)
|
||||||
track('select quick order')
|
track('select quick order')
|
||||||
}}
|
}}
|
||||||
|
xs={true}
|
||||||
>
|
>
|
||||||
Quick
|
Quick
|
||||||
</PillButton>
|
</PillButton>
|
||||||
|
@ -768,6 +754,7 @@ function QuickOrLimitBet(props: {
|
||||||
setIsLimitOrder(true)
|
setIsLimitOrder(true)
|
||||||
track('select limit order')
|
track('select limit order')
|
||||||
}}
|
}}
|
||||||
|
xs={true}
|
||||||
>
|
>
|
||||||
Limit
|
Limit
|
||||||
</PillButton>
|
</PillButton>
|
||||||
|
|
|
@ -5,19 +5,19 @@ export function PillButton(props: {
|
||||||
selected: boolean
|
selected: boolean
|
||||||
onSelect: () => void
|
onSelect: () => void
|
||||||
color?: string
|
color?: string
|
||||||
big?: boolean
|
xs?: boolean
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const { children, selected, onSelect, color, big } = props
|
const { children, selected, onSelect, color, xs } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'cursor-pointer select-none whitespace-nowrap rounded-full',
|
'cursor-pointer select-none whitespace-nowrap rounded-full px-3 py-1.5 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'
|
||||||
big ? 'px-8 py-2' : 'px-3 py-1.5 text-sm'
|
|
||||||
)}
|
)}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
|
|
|
@ -38,7 +38,7 @@ export function Carousel(props: {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('relative', className)}>
|
<div className={clsx('relative', className)}>
|
||||||
<Row
|
<Row
|
||||||
className="scrollbar-hide w-full gap-4 overflow-x-auto scroll-smooth"
|
className="scrollbar-hide w-full snap-x gap-4 overflow-x-auto scroll-smooth"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
>
|
>
|
||||||
|
|
|
@ -27,7 +27,8 @@ export function AcceptChallengeButton(props: {
|
||||||
setErrorText('')
|
setErrorText('')
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
if (!user) return <BetSignUpPrompt label="Accept this bet" className="mt-4" />
|
if (!user)
|
||||||
|
return <BetSignUpPrompt label="Sign up to accept" className="mt-4" />
|
||||||
|
|
||||||
const iAcceptChallenge = () => {
|
const iAcceptChallenge = () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
169
web/components/comment-input.tsx
Normal file
169
web/components/comment-input.tsx
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
import { PaperAirplaneIcon } from '@heroicons/react/solid'
|
||||||
|
import { Editor } from '@tiptap/react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { User } from 'common/user'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { MAX_COMMENT_LENGTH } from 'web/lib/firebase/comments'
|
||||||
|
import { Avatar } from './avatar'
|
||||||
|
import { TextEditor, useTextEditor } from './editor'
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
import { LoadingIndicator } from './loading-indicator'
|
||||||
|
|
||||||
|
export function CommentInput(props: {
|
||||||
|
replyToUser?: { id: string; username: string }
|
||||||
|
// Reply to a free response answer
|
||||||
|
parentAnswerOutcome?: string
|
||||||
|
// Reply to another comment
|
||||||
|
parentCommentId?: string
|
||||||
|
onSubmitComment?: (editor: Editor, betId: string | undefined) => void
|
||||||
|
className?: string
|
||||||
|
presetId?: string
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
parentAnswerOutcome,
|
||||||
|
parentCommentId,
|
||||||
|
replyToUser,
|
||||||
|
onSubmitComment,
|
||||||
|
presetId,
|
||||||
|
} = props
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
const { editor, upload } = useTextEditor({
|
||||||
|
simple: true,
|
||||||
|
max: MAX_COMMENT_LENGTH,
|
||||||
|
placeholder:
|
||||||
|
!!parentCommentId || !!parentAnswerOutcome
|
||||||
|
? 'Write a reply...'
|
||||||
|
: 'Write a comment...',
|
||||||
|
})
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
async function submitComment(betId: string | undefined) {
|
||||||
|
if (!editor || editor.isEmpty || isSubmitting) return
|
||||||
|
setIsSubmitting(true)
|
||||||
|
onSubmitComment?.(editor, betId)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user?.isBannedFromPosting) return <></>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row className={clsx(props.className, 'mb-2 gap-1 sm:gap-2')}>
|
||||||
|
<Avatar
|
||||||
|
avatarUrl={user?.avatarUrl}
|
||||||
|
username={user?.username}
|
||||||
|
size="sm"
|
||||||
|
className="mt-2"
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 flex-1 pl-0.5 text-sm">
|
||||||
|
<CommentInputTextArea
|
||||||
|
editor={editor}
|
||||||
|
upload={upload}
|
||||||
|
replyToUser={replyToUser}
|
||||||
|
user={user}
|
||||||
|
submitComment={submitComment}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
presetId={presetId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommentInputTextArea(props: {
|
||||||
|
user: User | undefined | null
|
||||||
|
replyToUser?: { id: string; username: string }
|
||||||
|
editor: Editor | null
|
||||||
|
upload: Parameters<typeof TextEditor>[0]['upload']
|
||||||
|
submitComment: (id?: string) => void
|
||||||
|
isSubmitting: boolean
|
||||||
|
presetId?: string
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
user,
|
||||||
|
editor,
|
||||||
|
upload,
|
||||||
|
submitComment,
|
||||||
|
presetId,
|
||||||
|
isSubmitting,
|
||||||
|
replyToUser,
|
||||||
|
} = props
|
||||||
|
useEffect(() => {
|
||||||
|
editor?.setEditable(!isSubmitting)
|
||||||
|
}, [isSubmitting, editor])
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
submitComment(presetId)
|
||||||
|
editor?.commands?.clearContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Submit on ctrl+enter or mod+enter key
|
||||||
|
editor.setOptions({
|
||||||
|
editorProps: {
|
||||||
|
handleKeyDown: (view, event) => {
|
||||||
|
if (
|
||||||
|
event.key === 'Enter' &&
|
||||||
|
!event.shiftKey &&
|
||||||
|
(event.ctrlKey || event.metaKey) &&
|
||||||
|
// mention list is closed
|
||||||
|
!(view.state as any).mention$.active
|
||||||
|
) {
|
||||||
|
submit()
|
||||||
|
event.preventDefault()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// insert at mention and focus
|
||||||
|
if (replyToUser) {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.setContent({
|
||||||
|
type: 'mention',
|
||||||
|
attrs: { label: replyToUser.username, id: replyToUser.id },
|
||||||
|
})
|
||||||
|
.insertContent(' ')
|
||||||
|
.focus()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextEditor editor={editor} upload={upload}>
|
||||||
|
{user && !isSubmitting && (
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300"
|
||||||
|
disabled={!editor || editor.isEmpty}
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
<PaperAirplaneIcon className="m-0 h-[25px] min-w-[22px] rotate-90 p-0" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSubmitting && (
|
||||||
|
<LoadingIndicator spinnerClassName={'border-gray-500'} />
|
||||||
|
)}
|
||||||
|
</TextEditor>
|
||||||
|
<Row>
|
||||||
|
{!user && (
|
||||||
|
<button
|
||||||
|
className={'btn btn-outline btn-sm mt-2 normal-case'}
|
||||||
|
onClick={() => submitComment(presetId)}
|
||||||
|
>
|
||||||
|
Add my comment
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
|
@ -69,6 +69,7 @@ type AdditionalFilter = {
|
||||||
excludeContractIds?: string[]
|
excludeContractIds?: string[]
|
||||||
groupSlug?: string
|
groupSlug?: string
|
||||||
yourBets?: boolean
|
yourBets?: boolean
|
||||||
|
followed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContractSearch(props: {
|
export function ContractSearch(props: {
|
||||||
|
@ -88,6 +89,7 @@ export function ContractSearch(props: {
|
||||||
useQueryUrlParam?: boolean
|
useQueryUrlParam?: boolean
|
||||||
isWholePage?: boolean
|
isWholePage?: boolean
|
||||||
noControls?: boolean
|
noControls?: boolean
|
||||||
|
maxResults?: number
|
||||||
renderContracts?: (
|
renderContracts?: (
|
||||||
contracts: Contract[] | undefined,
|
contracts: Contract[] | undefined,
|
||||||
loadMore: () => void
|
loadMore: () => void
|
||||||
|
@ -107,6 +109,7 @@ export function ContractSearch(props: {
|
||||||
useQueryUrlParam,
|
useQueryUrlParam,
|
||||||
isWholePage,
|
isWholePage,
|
||||||
noControls,
|
noControls,
|
||||||
|
maxResults,
|
||||||
renderContracts,
|
renderContracts,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
@ -189,7 +192,8 @@ export function ContractSearch(props: {
|
||||||
const contracts = state.pages
|
const contracts = state.pages
|
||||||
.flat()
|
.flat()
|
||||||
.filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id))
|
.filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id))
|
||||||
const renderedContracts = state.pages.length === 0 ? undefined : contracts
|
const renderedContracts =
|
||||||
|
state.pages.length === 0 ? undefined : contracts.slice(0, maxResults)
|
||||||
|
|
||||||
if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
|
if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
|
||||||
return <ContractSearchFirestore additionalFilter={additionalFilter} />
|
return <ContractSearchFirestore additionalFilter={additionalFilter} />
|
||||||
|
@ -292,6 +296,19 @@ function ContractSearchControls(props: {
|
||||||
const pillGroups: { name: string; slug: string }[] =
|
const pillGroups: { name: string; slug: string }[] =
|
||||||
memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS
|
memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS
|
||||||
|
|
||||||
|
const personalFilters = user
|
||||||
|
? [
|
||||||
|
// Show contracts in groups that the user is a member of.
|
||||||
|
memberGroupSlugs
|
||||||
|
.map((slug) => `groupLinks.slug:${slug}`)
|
||||||
|
// Or, show contracts created by users the user follows
|
||||||
|
.concat(follows?.map((followId) => `creatorId:${followId}`) ?? []),
|
||||||
|
|
||||||
|
// Subtract contracts you bet on, to show new ones.
|
||||||
|
`uniqueBettorIds:-${user.id}`,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
|
||||||
const additionalFilters = [
|
const additionalFilters = [
|
||||||
additionalFilter?.creatorId
|
additionalFilter?.creatorId
|
||||||
? `creatorId:${additionalFilter.creatorId}`
|
? `creatorId:${additionalFilter.creatorId}`
|
||||||
|
@ -304,6 +321,7 @@ function ContractSearchControls(props: {
|
||||||
? // Show contracts bet on by the user
|
? // Show contracts bet on by the user
|
||||||
`uniqueBettorIds:${user.id}`
|
`uniqueBettorIds:${user.id}`
|
||||||
: '',
|
: '',
|
||||||
|
...(additionalFilter?.followed ? personalFilters : []),
|
||||||
]
|
]
|
||||||
const facetFilters = query
|
const facetFilters = query
|
||||||
? additionalFilters
|
? additionalFilters
|
||||||
|
@ -320,17 +338,7 @@ function ContractSearchControls(props: {
|
||||||
state.pillFilter !== 'your-bets'
|
state.pillFilter !== 'your-bets'
|
||||||
? `groupLinks.slug:${state.pillFilter}`
|
? `groupLinks.slug:${state.pillFilter}`
|
||||||
: '',
|
: '',
|
||||||
state.pillFilter === 'personal'
|
...(state.pillFilter === 'personal' ? personalFilters : []),
|
||||||
? // Show contracts in groups that the user is a member of
|
|
||||||
memberGroupSlugs
|
|
||||||
.map((slug) => `groupLinks.slug:${slug}`)
|
|
||||||
// Show contracts created by users the user follows
|
|
||||||
.concat(follows?.map((followId) => `creatorId:${followId}`) ?? [])
|
|
||||||
: '',
|
|
||||||
// Subtract contracts you bet on from For you.
|
|
||||||
state.pillFilter === 'personal' && user
|
|
||||||
? `uniqueBettorIds:-${user.id}`
|
|
||||||
: '',
|
|
||||||
state.pillFilter === 'your-bets' && user
|
state.pillFilter === 'your-bets' && user
|
||||||
? // Show contracts bet on by the user
|
? // Show contracts bet on by the user
|
||||||
`uniqueBettorIds:${user.id}`
|
`uniqueBettorIds:${user.id}`
|
||||||
|
@ -441,7 +449,7 @@ function ContractSearchControls(props: {
|
||||||
selected={state.pillFilter === 'your-bets'}
|
selected={state.pillFilter === 'your-bets'}
|
||||||
onSelect={selectPill('your-bets')}
|
onSelect={selectPill('your-bets')}
|
||||||
>
|
>
|
||||||
Your bets
|
Your trades
|
||||||
</PillButton>
|
</PillButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -294,7 +294,7 @@ export function ExtraMobileContractDetails(props: {
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={`${formatMoney(
|
text={`${formatMoney(
|
||||||
volume
|
volume
|
||||||
)} bet - ${uniqueBettors} unique bettors`}
|
)} bet - ${uniqueBettors} unique traders`}
|
||||||
>
|
>
|
||||||
{volumeTranslation}
|
{volumeTranslation}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -135,7 +135,7 @@ export function ContractInfoDialog(props: {
|
||||||
</tr> */}
|
</tr> */}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Bettors</td>
|
<td>Traders</td>
|
||||||
<td>{bettorsCount}</td>
|
<td>{bettorsCount}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ export function ContractLeaderboard(props: {
|
||||||
|
|
||||||
return users && users.length > 0 ? (
|
return users && users.length > 0 ? (
|
||||||
<Leaderboard
|
<Leaderboard
|
||||||
title="🏅 Top bettors"
|
title="🏅 Top traders"
|
||||||
users={users || []}
|
users={users || []}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
|
|
|
@ -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}
|
||||||
|
@ -116,13 +114,13 @@ export function ContractTabs(props: {
|
||||||
badge: `${comments.length}`,
|
badge: `${comments.length}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Bets',
|
title: 'Trades',
|
||||||
content: betActivity,
|
content: betActivity,
|
||||||
badge: `${visibleBets.length}`,
|
badge: `${visibleBets.length}`,
|
||||||
},
|
},
|
||||||
...(!user || !userBets?.length
|
...(!user || !userBets?.length
|
||||||
? []
|
? []
|
||||||
: [{ title: 'Your bets', content: yourTrades }]),
|
: [{ title: 'Your trades', content: yourTrades }]),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{!user ? (
|
{!user ? (
|
||||||
|
|
|
@ -114,6 +114,7 @@ export function CreatorContractsList(props: {
|
||||||
additionalFilter={{
|
additionalFilter={{
|
||||||
creatorId: creator.id,
|
creatorId: creator.id,
|
||||||
}}
|
}}
|
||||||
|
persistPrefix={`user-${creator.id}`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { Col } from 'web/components/layout/col'
|
||||||
import { withTracking } from 'web/lib/service/analytics'
|
import { withTracking } from 'web/lib/service/analytics'
|
||||||
import { CreateChallengeModal } from 'web/components/challenges/create-challenge-modal'
|
import { CreateChallengeModal } from 'web/components/challenges/create-challenge-modal'
|
||||||
import { CHALLENGES_ENABLED } from 'common/challenge'
|
import { CHALLENGES_ENABLED } from 'common/challenge'
|
||||||
|
import ChallengeIcon from 'web/lib/icons/challenge-icon'
|
||||||
|
|
||||||
export function ExtraContractActionsRow(props: { contract: Contract }) {
|
export function ExtraContractActionsRow(props: { contract: Contract }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
|
@ -61,9 +62,7 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Col className="items-center sm:flex-row">
|
<Col className="items-center sm:flex-row">
|
||||||
<span className="h-[24px] w-5 sm:mr-2" aria-hidden="true">
|
<ChallengeIcon className="mx-auto h-[24px] w-5 text-gray-500 sm:mr-2" />
|
||||||
⚔️
|
|
||||||
</span>
|
|
||||||
<span>Challenge</span>
|
<span>Challenge</span>
|
||||||
</Col>
|
</Col>
|
||||||
<CreateChallengeModal
|
<CreateChallengeModal
|
||||||
|
|
|
@ -39,14 +39,14 @@ export function LikeMarketButton(props: {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
size={'lg'}
|
size={'lg'}
|
||||||
className={'mb-1'}
|
className={'max-w-xs self-center'}
|
||||||
color={'gray-white'}
|
color={'gray-white'}
|
||||||
onClick={onLike}
|
onClick={onLike}
|
||||||
>
|
>
|
||||||
<Col className={'items-center sm:flex-row sm:gap-x-2'}>
|
<Col className={'items-center sm:flex-row'}>
|
||||||
<HeartIcon
|
<HeartIcon
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'h-6 w-6',
|
'h-[24px] w-5 sm:mr-2',
|
||||||
user &&
|
user &&
|
||||||
(userLikedContractIds?.includes(contract.id) ||
|
(userLikedContractIds?.includes(contract.id) ||
|
||||||
(!likes && contract.likedByUserIds?.includes(user.id)))
|
(!likes && contract.likedByUserIds?.includes(user.id)))
|
||||||
|
|
|
@ -2,53 +2,70 @@ import clsx from 'clsx'
|
||||||
import { contractPath } from 'web/lib/firebase/contracts'
|
import { contractPath } from 'web/lib/firebase/contracts'
|
||||||
import { CPMMContract } from 'common/contract'
|
import { CPMMContract } from 'common/contract'
|
||||||
import { formatPercent } from 'common/util/format'
|
import { formatPercent } from 'common/util/format'
|
||||||
import { useProbChanges } from 'web/hooks/use-prob-changes'
|
|
||||||
import { SiteLink } from '../site-link'
|
import { SiteLink } from '../site-link'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
|
import { LoadingIndicator } from '../loading-indicator'
|
||||||
|
|
||||||
export function ProbChangeTable(props: { userId: string | undefined }) {
|
export function ProbChangeTable(props: {
|
||||||
const { userId } = props
|
changes:
|
||||||
|
| { positiveChanges: CPMMContract[]; negativeChanges: CPMMContract[] }
|
||||||
|
| undefined
|
||||||
|
}) {
|
||||||
|
const { changes } = props
|
||||||
|
|
||||||
const changes = useProbChanges(userId ?? '')
|
if (!changes) return <LoadingIndicator />
|
||||||
|
|
||||||
if (!changes) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const { positiveChanges, negativeChanges } = changes
|
const { positiveChanges, negativeChanges } = changes
|
||||||
|
|
||||||
const count = 3
|
const threshold = 0.075
|
||||||
|
const countOverThreshold = Math.max(
|
||||||
|
positiveChanges.findIndex((c) => c.probChanges.day < threshold) + 1,
|
||||||
|
negativeChanges.findIndex((c) => c.probChanges.day > -threshold) + 1
|
||||||
|
)
|
||||||
|
const maxRows = Math.min(positiveChanges.length, negativeChanges.length)
|
||||||
|
const rows = Math.min(3, Math.min(maxRows, countOverThreshold))
|
||||||
|
|
||||||
|
const filteredPositiveChanges = positiveChanges.slice(0, rows)
|
||||||
|
const filteredNegativeChanges = negativeChanges.slice(0, rows)
|
||||||
|
|
||||||
|
if (rows === 0) return <div className="px-4 text-gray-500">None</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="w-full flex-wrap divide-x-2 rounded bg-white shadow-md">
|
<Col className="mb-4 w-full divide-x-2 divide-y rounded-lg bg-white shadow-md md:flex-row md:divide-y-0">
|
||||||
<Col className="min-w-[300px] flex-1 divide-y">
|
<Col className="flex-1 divide-y">
|
||||||
{positiveChanges.slice(0, count).map((contract) => (
|
{filteredPositiveChanges.map((contract) => (
|
||||||
<Row className="hover:bg-gray-100">
|
<Row className="items-center hover:bg-gray-100">
|
||||||
<ProbChange className="p-4 text-right" contract={contract} />
|
<ProbChange
|
||||||
|
className="p-4 text-right text-xl"
|
||||||
|
contract={contract}
|
||||||
|
/>
|
||||||
<SiteLink
|
<SiteLink
|
||||||
className="p-4 font-semibold text-indigo-700"
|
className="p-4 pl-2 font-semibold text-indigo-700"
|
||||||
href={contractPath(contract)}
|
href={contractPath(contract)}
|
||||||
>
|
>
|
||||||
{contract.question}
|
<span className="line-clamp-2">{contract.question}</span>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="justify-content-stretch min-w-[300px] flex-1 divide-y">
|
<Col className="flex-1 divide-y">
|
||||||
{negativeChanges.slice(0, count).map((contract) => (
|
{filteredNegativeChanges.map((contract) => (
|
||||||
<Row className="hover:bg-gray-100">
|
<Row className="items-center hover:bg-gray-100">
|
||||||
<ProbChange className="p-4 text-right" contract={contract} />
|
<ProbChange
|
||||||
|
className="p-4 text-right text-xl"
|
||||||
|
contract={contract}
|
||||||
|
/>
|
||||||
<SiteLink
|
<SiteLink
|
||||||
className="p-4 font-semibold text-indigo-700"
|
className="p-4 pl-2 font-semibold text-indigo-700"
|
||||||
href={contractPath(contract)}
|
href={contractPath(contract)}
|
||||||
>
|
>
|
||||||
{contract.question}
|
<span className="line-clamp-2">{contract.question}</span>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,9 +80,9 @@ export function ProbChange(props: {
|
||||||
|
|
||||||
const color =
|
const color =
|
||||||
change > 0
|
change > 0
|
||||||
? 'text-green-600'
|
? 'text-green-500'
|
||||||
: change < 0
|
: change < 0
|
||||||
? 'text-red-600'
|
? 'text-red-500'
|
||||||
: 'text-gray-600'
|
: 'text-gray-600'
|
||||||
|
|
||||||
const str =
|
const str =
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { Col } from 'web/components/layout/col'
|
||||||
|
|
||||||
export function DoubleCarousel(props: {
|
export function DoubleCarousel(props: {
|
||||||
contracts: Contract[]
|
contracts: Contract[]
|
||||||
seeMoreUrl?: string
|
|
||||||
showTime?: ShowTime
|
showTime?: ShowTime
|
||||||
loadMore?: () => void
|
loadMore?: () => void
|
||||||
}) {
|
}) {
|
||||||
|
@ -19,7 +18,7 @@ export function DoubleCarousel(props: {
|
||||||
? range(0, Math.floor(contracts.length / 2)).map((col) => {
|
? range(0, Math.floor(contracts.length / 2)).map((col) => {
|
||||||
const i = col * 2
|
const i = col * 2
|
||||||
return (
|
return (
|
||||||
<Col key={contracts[i].id}>
|
<Col className="snap-start scroll-m-4" key={contracts[i].id}>
|
||||||
<ContractCard
|
<ContractCard
|
||||||
contract={contracts[i]}
|
contract={contracts[i]}
|
||||||
className="mb-2 w-96 shrink-0"
|
className="mb-2 w-96 shrink-0"
|
||||||
|
|
|
@ -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,8 +75,7 @@ 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({
|
||||||
|
@ -94,16 +90,12 @@ export function useTextEditor(props: {
|
||||||
CharacterCount.configure({ limit: max }),
|
CharacterCount.configure({ limit: max }),
|
||||||
simple ? DisplayImage : Image,
|
simple ? DisplayImage : Image,
|
||||||
DisplayLink,
|
DisplayLink,
|
||||||
DisplayMention.configure({
|
DisplayMention.configure({ suggestion: mentionSuggestion }),
|
||||||
suggestion: mentionSuggestion(users),
|
|
||||||
}),
|
|
||||||
Iframe,
|
Iframe,
|
||||||
TiptapTweet,
|
TiptapTweet,
|
||||||
],
|
],
|
||||||
content: defaultValue,
|
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()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { getOutcomeProbability } from 'common/calculate'
|
||||||
import { FeedBet } from './feed-bets'
|
import { FeedBet } from './feed-bets'
|
||||||
import { FeedLiquidity } from './feed-liquidity'
|
import { FeedLiquidity } from './feed-liquidity'
|
||||||
import { FeedAnswerCommentGroup } from './feed-answer-comment-group'
|
import { FeedAnswerCommentGroup } from './feed-answer-comment-group'
|
||||||
import { FeedCommentThread, CommentInput } from './feed-comments'
|
import { FeedCommentThread, ContractCommentInput } from './feed-comments'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||||
import { LiquidityProvision } from 'common/liquidity-provision'
|
import { LiquidityProvision } from 'common/liquidity-provision'
|
||||||
|
@ -72,7 +72,7 @@ export function ContractCommentsActivity(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CommentInput
|
<ContractCommentInput
|
||||||
className="mb-5"
|
className="mb-5"
|
||||||
contract={contract}
|
contract={contract}
|
||||||
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
|
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Avatar } from 'web/components/avatar'
|
||||||
import { Linkify } from 'web/components/linkify'
|
import { Linkify } from 'web/components/linkify'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
CommentInput,
|
ContractCommentInput,
|
||||||
FeedComment,
|
FeedComment,
|
||||||
getMostRecentCommentableBet,
|
getMostRecentCommentableBet,
|
||||||
} from 'web/components/feed/feed-comments'
|
} from 'web/components/feed/feed-comments'
|
||||||
|
@ -177,7 +177,7 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
className="absolute -left-1 -ml-[1px] mt-[1.25rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
className="absolute -left-1 -ml-[1px] mt-[1.25rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<CommentInput
|
<ContractCommentInput
|
||||||
contract={contract}
|
contract={contract}
|
||||||
betsByCurrentUser={betsByCurrentUser}
|
betsByCurrentUser={betsByCurrentUser}
|
||||||
commentsByCurrentUser={commentsByCurrentUser}
|
commentsByCurrentUser={commentsByCurrentUser}
|
||||||
|
|
|
@ -13,22 +13,17 @@ import { Avatar } from 'web/components/avatar'
|
||||||
import { OutcomeLabel } from 'web/components/outcome-label'
|
import { OutcomeLabel } from 'web/components/outcome-label'
|
||||||
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
||||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||||
import {
|
import { createCommentOnContract } from 'web/lib/firebase/comments'
|
||||||
createCommentOnContract,
|
|
||||||
MAX_COMMENT_LENGTH,
|
|
||||||
} from 'web/lib/firebase/comments'
|
|
||||||
import { BetStatusText } from 'web/components/feed/feed-bets'
|
import { BetStatusText } from 'web/components/feed/feed-bets'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { getProbability } from 'common/calculate'
|
import { getProbability } from 'common/calculate'
|
||||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
|
||||||
import { PaperAirplaneIcon } from '@heroicons/react/outline'
|
|
||||||
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 { useWindowSize } from 'web/hooks/use-window-size'
|
import { Content } from '../editor'
|
||||||
import { Content, TextEditor, useTextEditor } 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'
|
||||||
|
import { CommentInput } from '../comment-input'
|
||||||
|
|
||||||
export function FeedCommentThread(props: {
|
export function FeedCommentThread(props: {
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
|
@ -90,14 +85,16 @@ export function FeedCommentThread(props: {
|
||||||
className="absolute -left-1 -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
className="absolute -left-1 -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<CommentInput
|
<ContractCommentInput
|
||||||
contract={contract}
|
contract={contract}
|
||||||
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
|
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
|
||||||
commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []}
|
commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []}
|
||||||
parentCommentId={parentComment.id}
|
parentCommentId={parentComment.id}
|
||||||
replyToUser={replyTo}
|
replyToUser={replyTo}
|
||||||
parentAnswerOutcome={parentComment.answerOutcome}
|
parentAnswerOutcome={parentComment.answerOutcome}
|
||||||
onSubmitComment={() => setShowReply(false)}
|
onSubmitComment={() => {
|
||||||
|
setShowReply(false)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
@ -267,67 +264,76 @@ function CommentStatus(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: move commentinput and comment input text area into their own files
|
export function ContractCommentInput(props: {
|
||||||
export function CommentInput(props: {
|
|
||||||
contract: Contract
|
contract: Contract
|
||||||
betsByCurrentUser: Bet[]
|
betsByCurrentUser: Bet[]
|
||||||
commentsByCurrentUser: ContractComment[]
|
commentsByCurrentUser: ContractComment[]
|
||||||
className?: string
|
className?: string
|
||||||
|
parentAnswerOutcome?: string | undefined
|
||||||
replyToUser?: { id: string; username: string }
|
replyToUser?: { id: string; username: string }
|
||||||
// Reply to a free response answer
|
|
||||||
parentAnswerOutcome?: string
|
|
||||||
// Reply to another comment
|
|
||||||
parentCommentId?: string
|
parentCommentId?: string
|
||||||
onSubmitComment?: () => void
|
onSubmitComment?: () => void
|
||||||
}) {
|
}) {
|
||||||
const {
|
|
||||||
contract,
|
|
||||||
betsByCurrentUser,
|
|
||||||
commentsByCurrentUser,
|
|
||||||
className,
|
|
||||||
parentAnswerOutcome,
|
|
||||||
parentCommentId,
|
|
||||||
replyToUser,
|
|
||||||
onSubmitComment,
|
|
||||||
} = props
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const { editor, upload } = useTextEditor({
|
async function onSubmitComment(editor: Editor, betId: string | undefined) {
|
||||||
simple: true,
|
|
||||||
max: MAX_COMMENT_LENGTH,
|
|
||||||
placeholder:
|
|
||||||
!!parentCommentId || !!parentAnswerOutcome
|
|
||||||
? 'Write a reply...'
|
|
||||||
: 'Write a comment...',
|
|
||||||
})
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
||||||
|
|
||||||
const mostRecentCommentableBet = getMostRecentCommentableBet(
|
|
||||||
betsByCurrentUser,
|
|
||||||
commentsByCurrentUser,
|
|
||||||
user,
|
|
||||||
parentAnswerOutcome
|
|
||||||
)
|
|
||||||
const { id } = mostRecentCommentableBet || { id: undefined }
|
|
||||||
|
|
||||||
async function submitComment(betId: string | undefined) {
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
track('sign in to comment')
|
track('sign in to comment')
|
||||||
return await firebaseLogin()
|
return await firebaseLogin()
|
||||||
}
|
}
|
||||||
if (!editor || editor.isEmpty || isSubmitting) return
|
|
||||||
setIsSubmitting(true)
|
|
||||||
await createCommentOnContract(
|
await createCommentOnContract(
|
||||||
contract.id,
|
props.contract.id,
|
||||||
editor.getJSON(),
|
editor.getJSON(),
|
||||||
user,
|
user,
|
||||||
betId,
|
betId,
|
||||||
parentAnswerOutcome,
|
props.parentAnswerOutcome,
|
||||||
parentCommentId
|
props.parentCommentId
|
||||||
)
|
)
|
||||||
onSubmitComment?.()
|
props.onSubmitComment?.()
|
||||||
setIsSubmitting(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mostRecentCommentableBet = getMostRecentCommentableBet(
|
||||||
|
props.betsByCurrentUser,
|
||||||
|
props.commentsByCurrentUser,
|
||||||
|
user,
|
||||||
|
props.parentAnswerOutcome
|
||||||
|
)
|
||||||
|
|
||||||
|
const { id } = mostRecentCommentableBet || { id: undefined }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col>
|
||||||
|
<CommentBetArea
|
||||||
|
betsByCurrentUser={props.betsByCurrentUser}
|
||||||
|
contract={props.contract}
|
||||||
|
commentsByCurrentUser={props.commentsByCurrentUser}
|
||||||
|
parentAnswerOutcome={props.parentAnswerOutcome}
|
||||||
|
user={useUser()}
|
||||||
|
className={props.className}
|
||||||
|
mostRecentCommentableBet={mostRecentCommentableBet}
|
||||||
|
/>
|
||||||
|
<CommentInput
|
||||||
|
replyToUser={props.replyToUser}
|
||||||
|
parentAnswerOutcome={props.parentAnswerOutcome}
|
||||||
|
parentCommentId={props.parentCommentId}
|
||||||
|
onSubmitComment={onSubmitComment}
|
||||||
|
className={props.className}
|
||||||
|
presetId={id}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommentBetArea(props: {
|
||||||
|
betsByCurrentUser: Bet[]
|
||||||
|
contract: Contract
|
||||||
|
commentsByCurrentUser: ContractComment[]
|
||||||
|
parentAnswerOutcome?: string
|
||||||
|
user?: User | null
|
||||||
|
className?: string
|
||||||
|
mostRecentCommentableBet?: Bet
|
||||||
|
}) {
|
||||||
|
const { betsByCurrentUser, contract, user, mostRecentCommentableBet } = props
|
||||||
|
|
||||||
const { userPosition, outcome } = getBettorsLargestPositionBeforeTime(
|
const { userPosition, outcome } = getBettorsLargestPositionBeforeTime(
|
||||||
contract,
|
contract,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
|
@ -336,26 +342,15 @@ export function CommentInput(props: {
|
||||||
|
|
||||||
const isNumeric = contract.outcomeType === 'NUMERIC'
|
const isNumeric = contract.outcomeType === 'NUMERIC'
|
||||||
|
|
||||||
if (user?.isBannedFromPosting) return <></>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={clsx(className, 'mb-2 gap-1 sm:gap-2')}>
|
<Row className={clsx(props.className, 'mb-2 gap-1 sm:gap-2')}>
|
||||||
<Avatar
|
|
||||||
avatarUrl={user?.avatarUrl}
|
|
||||||
username={user?.username}
|
|
||||||
size="sm"
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
<div className="min-w-0 flex-1 pl-0.5 text-sm">
|
|
||||||
<div className="mb-1 text-gray-500">
|
<div className="mb-1 text-gray-500">
|
||||||
{mostRecentCommentableBet && (
|
{mostRecentCommentableBet && (
|
||||||
<BetStatusText
|
<BetStatusText
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bet={mostRecentCommentableBet}
|
bet={mostRecentCommentableBet}
|
||||||
isSelf={true}
|
isSelf={true}
|
||||||
hideOutcome={
|
hideOutcome={isNumeric || contract.outcomeType === 'FREE_RESPONSE'}
|
||||||
isNumeric || contract.outcomeType === 'FREE_RESPONSE'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!mostRecentCommentableBet && user && userPosition > 0 && !isNumeric && (
|
{!mostRecentCommentableBet && user && userPosition > 0 && !isNumeric && (
|
||||||
|
@ -373,121 +368,10 @@ export function CommentInput(props: {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CommentInputTextArea
|
|
||||||
editor={editor}
|
|
||||||
upload={upload}
|
|
||||||
replyToUser={replyToUser}
|
|
||||||
user={user}
|
|
||||||
submitComment={submitComment}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
presetId={id}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommentInputTextArea(props: {
|
|
||||||
user: User | undefined | null
|
|
||||||
replyToUser?: { id: string; username: string }
|
|
||||||
editor: Editor | null
|
|
||||||
upload: Parameters<typeof TextEditor>[0]['upload']
|
|
||||||
submitComment: (id?: string) => void
|
|
||||||
isSubmitting: boolean
|
|
||||||
submitOnEnter?: boolean
|
|
||||||
presetId?: string
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
user,
|
|
||||||
editor,
|
|
||||||
upload,
|
|
||||||
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])
|
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
submitComment(presetId)
|
|
||||||
editor?.commands?.clearContent()
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// submit on Enter key
|
|
||||||
editor.setOptions({
|
|
||||||
editorProps: {
|
|
||||||
handleKeyDown: (view, event) => {
|
|
||||||
if (
|
|
||||||
submitOnEnter &&
|
|
||||||
event.key === 'Enter' &&
|
|
||||||
!event.shiftKey &&
|
|
||||||
(!isMobile || event.ctrlKey || event.metaKey) &&
|
|
||||||
// mention list is closed
|
|
||||||
!(view.state as any).mention$.active
|
|
||||||
) {
|
|
||||||
submit()
|
|
||||||
event.preventDefault()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// insert at mention and focus
|
|
||||||
if (replyToUser) {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.setContent({
|
|
||||||
type: 'mention',
|
|
||||||
attrs: { label: replyToUser.username, id: replyToUser.id },
|
|
||||||
})
|
|
||||||
.insertContent(' ')
|
|
||||||
.focus()
|
|
||||||
.run()
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [editor])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TextEditor editor={editor} upload={upload}>
|
|
||||||
{user && !isSubmitting && (
|
|
||||||
<button
|
|
||||||
className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300"
|
|
||||||
disabled={!editor || editor.isEmpty}
|
|
||||||
onClick={submit}
|
|
||||||
>
|
|
||||||
<PaperAirplaneIcon className="m-0 h-[25px] min-w-[22px] rotate-90 p-0" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSubmitting && (
|
|
||||||
<LoadingIndicator spinnerClassName={'border-gray-500'} />
|
|
||||||
)}
|
|
||||||
</TextEditor>
|
|
||||||
<Row>
|
|
||||||
{!user && (
|
|
||||||
<button
|
|
||||||
className={'btn btn-outline btn-sm mt-2 normal-case'}
|
|
||||||
onClick={() => submitComment(presetId)}
|
|
||||||
>
|
|
||||||
Add my comment
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBettorsLargestPositionBeforeTime(
|
function getBettorsLargestPositionBeforeTime(
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
createdTime: number,
|
createdTime: number,
|
||||||
|
|
|
@ -27,23 +27,18 @@ export function LandingPagePanel(props: { hotContracts: Contract[] }) {
|
||||||
<div className="m-4 max-w-[550px] self-center">
|
<div className="m-4 max-w-[550px] self-center">
|
||||||
<h1 className="text-3xl sm:text-6xl xl:text-6xl">
|
<h1 className="text-3xl sm:text-6xl xl:text-6xl">
|
||||||
<div className="font-semibold sm:mb-2">
|
<div className="font-semibold sm:mb-2">
|
||||||
Predict{' '}
|
A{' '}
|
||||||
<span className="bg-gradient-to-r from-indigo-500 to-blue-500 bg-clip-text font-bold text-transparent">
|
<span className="bg-gradient-to-r from-indigo-500 to-blue-500 bg-clip-text font-bold text-transparent">
|
||||||
anything!
|
market
|
||||||
</span>
|
</span>{' '}
|
||||||
|
for every question
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
<div className="mb-4 px-2 ">
|
<div className="mb-4 px-2 ">
|
||||||
Create a play-money prediction market on any topic you care about
|
Create a play-money prediction market on any topic you care about.
|
||||||
and bet with your friends on what will happen!
|
Trade with your friends to forecast the future.
|
||||||
<br />
|
<br />
|
||||||
{/* <br />
|
|
||||||
Sign up and get {formatMoney(1000)} - worth $10 to your{' '}
|
|
||||||
<SiteLink className="font-semibold" href="/charity">
|
|
||||||
favorite charity.
|
|
||||||
</SiteLink>
|
|
||||||
<br /> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -64,7 +64,7 @@ export function BottomNavBar() {
|
||||||
item={{
|
item={{
|
||||||
name: formatMoney(user.balance),
|
name: formatMoney(user.balance),
|
||||||
trackingEventName: 'profile',
|
trackingEventName: 'profile',
|
||||||
href: `/${user.username}?tab=bets`,
|
href: `/${user.username}?tab=trades`,
|
||||||
icon: () => (
|
icon: () => (
|
||||||
<Avatar
|
<Avatar
|
||||||
className="mx-auto my-1"
|
className="mx-auto my-1"
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { trackCallback } from 'web/lib/service/analytics'
|
||||||
export function ProfileSummary(props: { user: User }) {
|
export function ProfileSummary(props: { user: User }) {
|
||||||
const { user } = props
|
const { user } = props
|
||||||
return (
|
return (
|
||||||
<Link href={`/${user.username}?tab=bets`}>
|
<Link href={`/${user.username}?tab=trades`}>
|
||||||
<a
|
<a
|
||||||
onClick={trackCallback('sidebar: profile')}
|
onClick={trackCallback('sidebar: profile')}
|
||||||
className="group mb-3 flex flex-row items-center gap-4 truncate rounded-md py-3 text-gray-500 hover:bg-gray-100 hover:text-gray-700"
|
className="group mb-3 flex flex-row items-center gap-4 truncate rounded-md py-3 text-gray-500 hover:bg-gray-100 hover:text-gray-700"
|
||||||
|
|
|
@ -203,7 +203,7 @@ function NumericBuyPanel(props: {
|
||||||
)}
|
)}
|
||||||
onClick={betDisabled ? undefined : submitBet}
|
onClick={betDisabled ? undefined : submitBet}
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Submitting...' : 'Submit bet'}
|
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { InfoBox } from './info-box'
|
||||||
|
|
||||||
export const PlayMoneyDisclaimer = () => (
|
export const PlayMoneyDisclaimer = () => (
|
||||||
<InfoBox
|
<InfoBox
|
||||||
title="Play-money betting"
|
title="Play-money trading"
|
||||||
className="mt-4 max-w-md"
|
className="mt-4 max-w-md"
|
||||||
text="Mana (M$) is the play-money used by our platform to keep track of your bets. It's completely free for you and your friends to get started!"
|
text="Mana (M$) is the play-money used by our platform to keep track of your trades. It's completely free for you and your friends to get started!"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,7 +11,7 @@ export function LoansModal(props: {
|
||||||
<Modal open={isOpen} setOpen={setOpen}>
|
<Modal open={isOpen} setOpen={setOpen}>
|
||||||
<Col className="items-center gap-4 rounded-md bg-white px-8 py-6">
|
<Col className="items-center gap-4 rounded-md bg-white px-8 py-6">
|
||||||
<span className={'text-8xl'}>🏦</span>
|
<span className={'text-8xl'}>🏦</span>
|
||||||
<span className="text-xl">Daily loans on your bets</span>
|
<span className="text-xl">Daily loans on your trades</span>
|
||||||
<Col className={'gap-2'}>
|
<Col className={'gap-2'}>
|
||||||
<span className={'text-indigo-700'}>• What are daily loans?</span>
|
<span className={'text-indigo-700'}>• What are daily loans?</span>
|
||||||
<span className={'ml-2'}>
|
<span className={'ml-2'}>
|
||||||
|
|
|
@ -83,14 +83,14 @@ export function ResolutionPanel(props: {
|
||||||
<div>
|
<div>
|
||||||
{outcome === 'YES' ? (
|
{outcome === 'YES' ? (
|
||||||
<>
|
<>
|
||||||
Winnings will be paid out to YES bettors.
|
Winnings will be paid out to traders who bought YES.
|
||||||
{/* <br />
|
{/* <br />
|
||||||
<br />
|
<br />
|
||||||
You will earn {earnedFees}. */}
|
You will earn {earnedFees}. */}
|
||||||
</>
|
</>
|
||||||
) : outcome === 'NO' ? (
|
) : outcome === 'NO' ? (
|
||||||
<>
|
<>
|
||||||
Winnings will be paid out to NO bettors.
|
Winnings will be paid out to traders who bought NO.
|
||||||
{/* <br />
|
{/* <br />
|
||||||
<br />
|
<br />
|
||||||
You will earn {earnedFees}. */}
|
You will earn {earnedFees}. */}
|
||||||
|
|
|
@ -19,7 +19,7 @@ export function BetSignUpPrompt(props: {
|
||||||
size={size}
|
size={size}
|
||||||
color="gradient"
|
color="gradient"
|
||||||
>
|
>
|
||||||
{label ?? 'Sign up to bet!'}
|
{label ?? 'Sign up to predict!'}
|
||||||
</Button>
|
</Button>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
||||||
comment.commentType === 'contract' ? comment.contractId : undefined
|
comment.commentType === 'contract' ? comment.contractId : undefined
|
||||||
const groupId =
|
const groupId =
|
||||||
comment.commentType === 'group' ? comment.groupId : undefined
|
comment.commentType === 'group' ? comment.groupId : undefined
|
||||||
|
const postId = comment.commentType === 'post' ? comment.postId : undefined
|
||||||
await transact({
|
await transact({
|
||||||
amount: change,
|
amount: change,
|
||||||
fromId: user.id,
|
fromId: user.id,
|
||||||
|
@ -54,7 +55,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
||||||
toType: 'USER',
|
toType: 'USER',
|
||||||
token: 'M$',
|
token: 'M$',
|
||||||
category: 'TIP',
|
category: 'TIP',
|
||||||
data: { commentId: comment.id, contractId, groupId },
|
data: { commentId: comment.id, contractId, groupId, postId },
|
||||||
description: `${user.name} tipped M$ ${change} to ${comment.userName} for a comment`,
|
description: `${user.name} tipped M$ ${change} to ${comment.userName} for a comment`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -62,6 +63,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
||||||
commentId: comment.id,
|
commentId: comment.id,
|
||||||
contractId,
|
contractId,
|
||||||
groupId,
|
groupId,
|
||||||
|
postId,
|
||||||
amount: change,
|
amount: change,
|
||||||
fromId: user.id,
|
fromId: user.id,
|
||||||
toId: comment.userId,
|
toId: comment.userId,
|
||||||
|
|
|
@ -260,7 +260,7 @@ export function UserPage(props: { user: User }) {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Bets',
|
title: 'Trades',
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
<BetsList user={user} />
|
<BetsList user={user} />
|
||||||
|
|
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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -193,7 +193,7 @@ export function BuyButton(props: { className?: string; onClick?: () => void }) {
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
Bet
|
Buy
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Comment, ContractComment, GroupComment } from 'common/comment'
|
import {
|
||||||
|
Comment,
|
||||||
|
ContractComment,
|
||||||
|
GroupComment,
|
||||||
|
PostComment,
|
||||||
|
} from 'common/comment'
|
||||||
import {
|
import {
|
||||||
listenForCommentsOnContract,
|
listenForCommentsOnContract,
|
||||||
listenForCommentsOnGroup,
|
listenForCommentsOnGroup,
|
||||||
|
listenForCommentsOnPost,
|
||||||
listenForRecentComments,
|
listenForRecentComments,
|
||||||
} from 'web/lib/firebase/comments'
|
} from 'web/lib/firebase/comments'
|
||||||
|
|
||||||
|
@ -25,6 +31,16 @@ export const useCommentsOnGroup = (groupId: string | undefined) => {
|
||||||
return comments
|
return comments
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useCommentsOnPost = (postId: string | undefined) => {
|
||||||
|
const [comments, setComments] = useState<PostComment[] | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (postId) return listenForCommentsOnPost(postId, setComments)
|
||||||
|
}, [postId])
|
||||||
|
|
||||||
|
return comments
|
||||||
|
}
|
||||||
|
|
||||||
export const useRecentComments = () => {
|
export const useRecentComments = () => {
|
||||||
const [recentComments, setRecentComments] = useState<Comment[] | undefined>()
|
const [recentComments, setRecentComments] = useState<Comment[] | undefined>()
|
||||||
useEffect(() => listenForRecentComments(setRecentComments), [])
|
useEffect(() => listenForRecentComments(setRecentComments), [])
|
||||||
|
|
|
@ -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 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,13 @@ import { useEffect, useState } from 'react'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import {
|
import {
|
||||||
|
getMemberGroups,
|
||||||
GroupMemberDoc,
|
GroupMemberDoc,
|
||||||
groupMembers,
|
groupMembers,
|
||||||
listenForGroup,
|
listenForGroup,
|
||||||
listenForGroupContractDocs,
|
listenForGroupContractDocs,
|
||||||
listenForGroups,
|
listenForGroups,
|
||||||
listenForMemberGroupIds,
|
listenForMemberGroupIds,
|
||||||
listenForMemberGroups,
|
|
||||||
listenForOpenGroups,
|
listenForOpenGroups,
|
||||||
listGroups,
|
listGroups,
|
||||||
} from 'web/lib/firebase/groups'
|
} from 'web/lib/firebase/groups'
|
||||||
|
@ -17,6 +17,7 @@ import { filterDefined } from 'common/util/array'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { uniq } from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
import { listenForValues } from 'web/lib/firebase/utils'
|
import { listenForValues } from 'web/lib/firebase/utils'
|
||||||
|
import { useQuery } from 'react-query'
|
||||||
|
|
||||||
export const useGroup = (groupId: string | undefined) => {
|
export const useGroup = (groupId: string | undefined) => {
|
||||||
const [group, setGroup] = useState<Group | null | undefined>()
|
const [group, setGroup] = useState<Group | null | undefined>()
|
||||||
|
@ -49,12 +50,10 @@ export const useOpenGroups = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMemberGroups = (userId: string | null | undefined) => {
|
export const useMemberGroups = (userId: string | null | undefined) => {
|
||||||
const [memberGroups, setMemberGroups] = useState<Group[] | undefined>()
|
const result = useQuery(['member-groups', userId ?? ''], () =>
|
||||||
useEffect(() => {
|
getMemberGroups(userId ?? '')
|
||||||
if (userId)
|
)
|
||||||
return listenForMemberGroups(userId, (groups) => setMemberGroups(groups))
|
return result.data
|
||||||
}, [userId])
|
|
||||||
return memberGroups
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: We cache member group ids in localstorage to speed up the initial load
|
// Note: We cache member group ids in localstorage to speed up the initial load
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
listenForTipTxns,
|
listenForTipTxns,
|
||||||
listenForTipTxnsOnGroup,
|
listenForTipTxnsOnGroup,
|
||||||
|
listenForTipTxnsOnPost,
|
||||||
} from 'web/lib/firebase/txns'
|
} from 'web/lib/firebase/txns'
|
||||||
|
|
||||||
export type CommentTips = { [userId: string]: number }
|
export type CommentTips = { [userId: string]: number }
|
||||||
|
@ -12,14 +13,16 @@ export type CommentTipMap = { [commentId: string]: CommentTips }
|
||||||
export function useTipTxns(on: {
|
export function useTipTxns(on: {
|
||||||
contractId?: string
|
contractId?: string
|
||||||
groupId?: string
|
groupId?: string
|
||||||
|
postId?: string
|
||||||
}): CommentTipMap {
|
}): CommentTipMap {
|
||||||
const [txns, setTxns] = useState<TipTxn[]>([])
|
const [txns, setTxns] = useState<TipTxn[]>([])
|
||||||
const { contractId, groupId } = on
|
const { contractId, groupId, postId } = on
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contractId) return listenForTipTxns(contractId, setTxns)
|
if (contractId) return listenForTipTxns(contractId, setTxns)
|
||||||
if (groupId) return listenForTipTxnsOnGroup(groupId, setTxns)
|
if (groupId) return listenForTipTxnsOnGroup(groupId, setTxns)
|
||||||
}, [contractId, groupId, setTxns])
|
if (postId) return listenForTipTxnsOnPost(postId, setTxns)
|
||||||
|
}, [contractId, groupId, postId, setTxns])
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const byComment = groupBy(txns, 'data.commentId')
|
const byComment = groupBy(txns, 'data.commentId')
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -7,12 +7,22 @@ import {
|
||||||
query,
|
query,
|
||||||
setDoc,
|
setDoc,
|
||||||
where,
|
where,
|
||||||
|
DocumentData,
|
||||||
|
DocumentReference,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
|
|
||||||
import { getValues, listenForValues } from './utils'
|
import { getValues, listenForValues } from './utils'
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { Comment, ContractComment, GroupComment } from 'common/comment'
|
import {
|
||||||
|
Comment,
|
||||||
|
ContractComment,
|
||||||
|
GroupComment,
|
||||||
|
OnContract,
|
||||||
|
OnGroup,
|
||||||
|
OnPost,
|
||||||
|
PostComment,
|
||||||
|
} from 'common/comment'
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
import { JSONContent } from '@tiptap/react'
|
import { JSONContent } from '@tiptap/react'
|
||||||
|
@ -24,7 +34,7 @@ export const MAX_COMMENT_LENGTH = 10000
|
||||||
export async function createCommentOnContract(
|
export async function createCommentOnContract(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
content: JSONContent,
|
content: JSONContent,
|
||||||
commenter: User,
|
user: User,
|
||||||
betId?: string,
|
betId?: string,
|
||||||
answerOutcome?: string,
|
answerOutcome?: string,
|
||||||
replyToCommentId?: string
|
replyToCommentId?: string
|
||||||
|
@ -32,28 +42,20 @@ export async function createCommentOnContract(
|
||||||
const ref = betId
|
const ref = betId
|
||||||
? doc(getCommentsCollection(contractId), betId)
|
? doc(getCommentsCollection(contractId), betId)
|
||||||
: doc(getCommentsCollection(contractId))
|
: doc(getCommentsCollection(contractId))
|
||||||
// contract slug and question are set via trigger
|
const onContract = {
|
||||||
const comment = removeUndefinedProps({
|
|
||||||
id: ref.id,
|
|
||||||
commentType: 'contract',
|
commentType: 'contract',
|
||||||
contractId,
|
contractId,
|
||||||
userId: commenter.id,
|
betId,
|
||||||
content: content,
|
answerOutcome,
|
||||||
createdTime: Date.now(),
|
} as OnContract
|
||||||
userName: commenter.name,
|
return await createComment(
|
||||||
userUsername: commenter.username,
|
|
||||||
userAvatarUrl: commenter.avatarUrl,
|
|
||||||
betId: betId,
|
|
||||||
answerOutcome: answerOutcome,
|
|
||||||
replyToCommentId: replyToCommentId,
|
|
||||||
})
|
|
||||||
track('comment', {
|
|
||||||
contractId,
|
contractId,
|
||||||
commentId: ref.id,
|
onContract,
|
||||||
betId: betId,
|
content,
|
||||||
replyToCommentId: replyToCommentId,
|
user,
|
||||||
})
|
ref,
|
||||||
return await setDoc(ref, comment)
|
replyToCommentId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
export async function createCommentOnGroup(
|
export async function createCommentOnGroup(
|
||||||
groupId: string,
|
groupId: string,
|
||||||
|
@ -62,10 +64,45 @@ export async function createCommentOnGroup(
|
||||||
replyToCommentId?: string
|
replyToCommentId?: string
|
||||||
) {
|
) {
|
||||||
const ref = doc(getCommentsOnGroupCollection(groupId))
|
const ref = doc(getCommentsOnGroupCollection(groupId))
|
||||||
|
const onGroup = { commentType: 'group', groupId: groupId } as OnGroup
|
||||||
|
return await createComment(
|
||||||
|
groupId,
|
||||||
|
onGroup,
|
||||||
|
content,
|
||||||
|
user,
|
||||||
|
ref,
|
||||||
|
replyToCommentId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCommentOnPost(
|
||||||
|
postId: string,
|
||||||
|
content: JSONContent,
|
||||||
|
user: User,
|
||||||
|
replyToCommentId?: string
|
||||||
|
) {
|
||||||
|
const ref = doc(getCommentsOnPostCollection(postId))
|
||||||
|
const onPost = { postId: postId, commentType: 'post' } as OnPost
|
||||||
|
return await createComment(
|
||||||
|
postId,
|
||||||
|
onPost,
|
||||||
|
content,
|
||||||
|
user,
|
||||||
|
ref,
|
||||||
|
replyToCommentId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createComment(
|
||||||
|
surfaceId: string,
|
||||||
|
extraFields: OnContract | OnGroup | OnPost,
|
||||||
|
content: JSONContent,
|
||||||
|
user: User,
|
||||||
|
ref: DocumentReference<DocumentData>,
|
||||||
|
replyToCommentId?: string
|
||||||
|
) {
|
||||||
const comment = removeUndefinedProps({
|
const comment = removeUndefinedProps({
|
||||||
id: ref.id,
|
id: ref.id,
|
||||||
commentType: 'group',
|
|
||||||
groupId,
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
content: content,
|
content: content,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
|
@ -73,11 +110,13 @@ export async function createCommentOnGroup(
|
||||||
userUsername: user.username,
|
userUsername: user.username,
|
||||||
userAvatarUrl: user.avatarUrl,
|
userAvatarUrl: user.avatarUrl,
|
||||||
replyToCommentId: replyToCommentId,
|
replyToCommentId: replyToCommentId,
|
||||||
|
...extraFields,
|
||||||
})
|
})
|
||||||
track('group message', {
|
|
||||||
|
track(`${extraFields.commentType} message`, {
|
||||||
user,
|
user,
|
||||||
commentId: ref.id,
|
commentId: ref.id,
|
||||||
groupId,
|
surfaceId,
|
||||||
replyToCommentId: replyToCommentId,
|
replyToCommentId: replyToCommentId,
|
||||||
})
|
})
|
||||||
return await setDoc(ref, comment)
|
return await setDoc(ref, comment)
|
||||||
|
@ -91,6 +130,10 @@ function getCommentsOnGroupCollection(groupId: string) {
|
||||||
return collection(db, 'groups', groupId, 'comments')
|
return collection(db, 'groups', groupId, 'comments')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCommentsOnPostCollection(postId: string) {
|
||||||
|
return collection(db, 'posts', postId, 'comments')
|
||||||
|
}
|
||||||
|
|
||||||
export async function listAllComments(contractId: string) {
|
export async function listAllComments(contractId: string) {
|
||||||
return await getValues<Comment>(
|
return await getValues<Comment>(
|
||||||
query(getCommentsCollection(contractId), orderBy('createdTime', 'desc'))
|
query(getCommentsCollection(contractId), orderBy('createdTime', 'desc'))
|
||||||
|
@ -103,6 +146,12 @@ export async function listAllCommentsOnGroup(groupId: string) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listAllCommentsOnPost(postId: string) {
|
||||||
|
return await getValues<PostComment>(
|
||||||
|
query(getCommentsOnPostCollection(postId), orderBy('createdTime', 'desc'))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function listenForCommentsOnContract(
|
export function listenForCommentsOnContract(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
setComments: (comments: ContractComment[]) => void
|
setComments: (comments: ContractComment[]) => void
|
||||||
|
@ -126,6 +175,16 @@ export function listenForCommentsOnGroup(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listenForCommentsOnPost(
|
||||||
|
postId: string,
|
||||||
|
setComments: (comments: PostComment[]) => void
|
||||||
|
) {
|
||||||
|
return listenForValues<PostComment>(
|
||||||
|
query(getCommentsOnPostCollection(postId), orderBy('createdTime', 'desc')),
|
||||||
|
setComments
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
// Define "recent" as "<3 days ago" for now
|
// Define "recent" as "<3 days ago" for now
|
||||||
|
|
|
@ -31,7 +31,7 @@ export const groupMembers = (groupId: string) =>
|
||||||
export const groupContracts = (groupId: string) =>
|
export const groupContracts = (groupId: string) =>
|
||||||
collection(groups, groupId, 'groupContracts')
|
collection(groups, groupId, 'groupContracts')
|
||||||
const openGroupsQuery = query(groups, where('anyoneCanJoin', '==', true))
|
const openGroupsQuery = query(groups, where('anyoneCanJoin', '==', true))
|
||||||
const memberGroupsQuery = (userId: string) =>
|
export const memberGroupsQuery = (userId: string) =>
|
||||||
query(collectionGroup(db, 'groupMembers'), where('userId', '==', userId))
|
query(collectionGroup(db, 'groupMembers'), where('userId', '==', userId))
|
||||||
|
|
||||||
export function groupPath(
|
export function groupPath(
|
||||||
|
@ -112,6 +112,15 @@ export function listenForGroup(
|
||||||
return listenForValue(doc(groups, groupId), setGroup)
|
return listenForValue(doc(groups, groupId), setGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMemberGroups(userId: string) {
|
||||||
|
const snapshot = await getDocs(memberGroupsQuery(userId))
|
||||||
|
const groupIds = filterDefined(
|
||||||
|
snapshot.docs.map((doc) => doc.ref.parent.parent?.id)
|
||||||
|
)
|
||||||
|
const groups = await Promise.all(groupIds.map(getGroup))
|
||||||
|
return filterDefined(groups)
|
||||||
|
}
|
||||||
|
|
||||||
export function listenForMemberGroupIds(
|
export function listenForMemberGroupIds(
|
||||||
userId: string,
|
userId: string,
|
||||||
setGroupIds: (groupIds: string[]) => void
|
setGroupIds: (groupIds: string[]) => void
|
||||||
|
|
|
@ -41,6 +41,13 @@ const getTipsOnGroupQuery = (groupId: string) =>
|
||||||
where('data.groupId', '==', groupId)
|
where('data.groupId', '==', groupId)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const getTipsOnPostQuery = (postId: string) =>
|
||||||
|
query(
|
||||||
|
txns,
|
||||||
|
where('category', '==', 'TIP'),
|
||||||
|
where('data.postId', '==', postId)
|
||||||
|
)
|
||||||
|
|
||||||
export function listenForTipTxns(
|
export function listenForTipTxns(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
setTxns: (txns: TipTxn[]) => void
|
setTxns: (txns: TipTxn[]) => void
|
||||||
|
@ -54,6 +61,13 @@ export function listenForTipTxnsOnGroup(
|
||||||
return listenForValues<TipTxn>(getTipsOnGroupQuery(groupId), setTxns)
|
return listenForValues<TipTxn>(getTipsOnGroupQuery(groupId), setTxns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listenForTipTxnsOnPost(
|
||||||
|
postId: string,
|
||||||
|
setTxns: (txns: TipTxn[]) => void
|
||||||
|
) {
|
||||||
|
return listenForValues<TipTxn>(getTipsOnPostQuery(postId), setTxns)
|
||||||
|
}
|
||||||
|
|
||||||
// Find all manalink Txns that are from or to this user
|
// Find all manalink Txns that are from or to this user
|
||||||
export function useManalinkTxns(userId: string) {
|
export function useManalinkTxns(userId: string) {
|
||||||
const [fromTxns, setFromTxns] = useState<ManalinkTxn[]>([])
|
const [fromTxns, setFromTxns] = useState<ManalinkTxn[]>([])
|
||||||
|
|
19
web/lib/icons/challenge-icon.tsx
Normal file
19
web/lib/icons/challenge-icon.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export default function ChallengeIcon(props: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
className={props.className}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon points="18.63 15.11 15.37 18.49 3.39 6.44 1.82 1.05 7.02 2.68 18.63 15.11" />
|
||||||
|
<polygon points="21.16 13.73 22.26 14.87 19.51 17.72 23 21.35 21.41 23 17.91 19.37 15.16 22.23 14.07 21.09 21.16 13.73" />
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<polygon points="8.6 18.44 5.34 15.06 16.96 2.63 22.15 1 20.58 6.39 8.6 18.44" />
|
||||||
|
<polygon points="9.93 21.07 8.84 22.21 6.09 19.35 2.59 22.98 1 21.33 4.49 17.7 1.74 14.85 2.84 13.71 9.93 21.07" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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')
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,9 @@ module.exports = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
optimizeFonts: false,
|
optimizeFonts: false,
|
||||||
experimental: {
|
experimental: {
|
||||||
|
images: {
|
||||||
|
allowFutureImage: true,
|
||||||
|
},
|
||||||
scrollRestoration: true,
|
scrollRestoration: true,
|
||||||
externalDir: true,
|
externalDir: true,
|
||||||
modularizeImports: {
|
modularizeImports: {
|
||||||
|
@ -25,7 +28,12 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
domains: ['lh3.googleusercontent.com', 'i.imgur.com'],
|
domains: [
|
||||||
|
'manifold.markets',
|
||||||
|
'lh3.googleusercontent.com',
|
||||||
|
'i.imgur.com',
|
||||||
|
'firebasestorage.googleapis.com',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -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
|
||||||
|
</a>
|
||||||
|
!
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Manifold is not affiliated with non-Featured charities; we're just
|
||||||
fans of their work.
|
fans of their work.
|
||||||
<br />
|
</li>
|
||||||
- As Manifold itself is a for-profit entity, your contributions will
|
<li>
|
||||||
|
As Manifold itself is a for-profit entity, your contributions will
|
||||||
not be tax deductible.
|
not be tax deductible.
|
||||||
<br />- Donations + matches are wired once each quarter.
|
</li>
|
||||||
|
<li>Donations + matches are wired once each quarter.</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
55
web/pages/experimental/home/edit.tsx
Normal file
55
web/pages/experimental/home/edit.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ArrangeHome } from 'web/components/arrange-home'
|
||||||
|
import { Button } from 'web/components/button'
|
||||||
|
import { Col } from 'web/components/layout/col'
|
||||||
|
import { Row } from 'web/components/layout/row'
|
||||||
|
import { Page } from 'web/components/page'
|
||||||
|
import { SiteLink } from 'web/components/site-link'
|
||||||
|
import { Title } from 'web/components/title'
|
||||||
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { updateUser } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
useTracking('edit home')
|
||||||
|
|
||||||
|
const [homeSections, setHomeSections] = useState(user?.homeSections ?? [])
|
||||||
|
|
||||||
|
const updateHomeSections = (newHomeSections: string[]) => {
|
||||||
|
if (!user) return
|
||||||
|
updateUser(user.id, { homeSections: newHomeSections })
|
||||||
|
setHomeSections(newHomeSections)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<Col className="pm:mx-10 gap-4 px-4 pb-6 pt-2">
|
||||||
|
<Row className={'w-full items-center justify-between'}>
|
||||||
|
<Title text="Edit your home page" />
|
||||||
|
<DoneButton />
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<ArrangeHome
|
||||||
|
user={user}
|
||||||
|
homeSections={homeSections}
|
||||||
|
setHomeSections={updateHomeSections}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DoneButton(props: { className?: string }) {
|
||||||
|
const { className } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SiteLink href="/experimental/home">
|
||||||
|
<Button size="lg" color="blue" className={clsx(className, 'flex')}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</SiteLink>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,40 +1,37 @@
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
import Router from 'next/router'
|
import Router from 'next/router'
|
||||||
import { PencilIcon, PlusSmIcon } from '@heroicons/react/solid'
|
import {
|
||||||
|
PencilIcon,
|
||||||
|
PlusSmIcon,
|
||||||
|
ArrowSmRightIcon,
|
||||||
|
} from '@heroicons/react/solid'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { ContractSearch, SORTS } from 'web/components/contract-search'
|
import { ContractSearch, SORTS } from 'web/components/contract-search'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { getUserAndPrivateUser, updateUser } from 'web/lib/firebase/users'
|
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { authenticateOnServer } from 'web/lib/firebase/server-auth'
|
|
||||||
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
import { GetServerSideProps } from 'next'
|
|
||||||
import { Sort } from 'web/components/contract-search'
|
import { Sort } from 'web/components/contract-search'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
|
||||||
import { GroupLinkItem } from '../../groups'
|
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { useMemberGroups } from 'web/hooks/use-group'
|
import { useMemberGroups } from 'web/hooks/use-group'
|
||||||
import { DoubleCarousel } from '../../../components/double-carousel'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Button } from 'web/components/button'
|
import { Button } from 'web/components/button'
|
||||||
import { ArrangeHome, getHomeItems } from '../../../components/arrange-home'
|
import { getHomeItems } from '../../../components/arrange-home'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { ProbChangeTable } from 'web/components/contract/prob-change-table'
|
import { ProbChangeTable } from 'web/components/contract/prob-change-table'
|
||||||
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
|
import { usePortfolioHistory } from 'web/hooks/use-portfolio-history'
|
||||||
|
import { calculatePortfolioProfit } from 'common/calculate-metrics'
|
||||||
|
import { formatMoney } from 'common/util/format'
|
||||||
|
import { useProbChanges } from 'web/hooks/use-prob-changes'
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
const Home = () => {
|
||||||
const creds = await authenticateOnServer(ctx)
|
const user = useUser()
|
||||||
const auth = creds ? await getUserAndPrivateUser(creds.uid) : null
|
|
||||||
return { props: { auth } }
|
|
||||||
}
|
|
||||||
|
|
||||||
const Home = (props: { auth: { user: User } | null }) => {
|
|
||||||
const user = useUser() ?? props.auth?.user ?? null
|
|
||||||
|
|
||||||
useTracking('view home')
|
useTracking('view home')
|
||||||
|
|
||||||
|
@ -42,76 +39,41 @@ const Home = (props: { auth: { user: User } | null }) => {
|
||||||
|
|
||||||
const groups = useMemberGroups(user?.id) ?? []
|
const groups = useMemberGroups(user?.id) ?? []
|
||||||
|
|
||||||
const [homeSections, setHomeSections] = useState(
|
const { sections } = getHomeItems(groups, user?.homeSections ?? [])
|
||||||
user?.homeSections ?? { visible: [], hidden: [] }
|
|
||||||
)
|
|
||||||
const { visibleItems } = getHomeItems(groups, homeSections)
|
|
||||||
|
|
||||||
const updateHomeSections = (newHomeSections: {
|
|
||||||
visible: string[]
|
|
||||||
hidden: string[]
|
|
||||||
}) => {
|
|
||||||
if (!user) return
|
|
||||||
updateUser(user.id, { homeSections: newHomeSections })
|
|
||||||
setHomeSections(newHomeSections)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Col className="pm:mx-10 gap-4 px-4 pb-12 xl:w-[125%]">
|
<Col className="pm:mx-10 gap-4 px-4 pb-12">
|
||||||
<Row className={'w-full items-center justify-between'}>
|
<Row className={'w-full items-center justify-between'}>
|
||||||
<Title text={isEditing ? 'Edit your home page' : 'Home'} />
|
<Title className="!mb-0" text="Home" />
|
||||||
|
|
||||||
<EditDoneButton isEditing={isEditing} setIsEditing={setIsEditing} />
|
<EditButton />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{isEditing ? (
|
<DailyProfitAndBalance userId={user?.id} />
|
||||||
<>
|
|
||||||
<ArrangeHome
|
|
||||||
user={user}
|
|
||||||
homeSections={homeSections}
|
|
||||||
setHomeSections={updateHomeSections}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="text-xl text-gray-800">Daily movers</div>
|
|
||||||
<ProbChangeTable userId={user?.id} />
|
|
||||||
|
|
||||||
{visibleItems.map((item) => {
|
{sections.map((item) => {
|
||||||
const { id } = item
|
const { id } = item
|
||||||
if (id === 'your-bets') {
|
if (id === 'daily-movers') {
|
||||||
return (
|
return <DailyMoversSection key={id} userId={user?.id} />
|
||||||
<SearchSection
|
|
||||||
key={id}
|
|
||||||
label={'Your bets'}
|
|
||||||
sort={'prob-change-day'}
|
|
||||||
user={user}
|
|
||||||
yourBets
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
const sort = SORTS.find((sort) => sort.value === id)
|
const sort = SORTS.find((sort) => sort.value === id)
|
||||||
if (sort)
|
if (sort)
|
||||||
return (
|
return (
|
||||||
<SearchSection
|
<SearchSection
|
||||||
key={id}
|
key={id}
|
||||||
label={sort.label}
|
label={sort.value === 'newest' ? 'New for you' : sort.label}
|
||||||
sort={sort.value}
|
sort={sort.value}
|
||||||
|
followed={sort.value === 'newest'}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const group = groups.find((g) => g.id === id)
|
const group = groups.find((g) => g.id === id)
|
||||||
if (group)
|
if (group) return <GroupSection key={id} group={group} user={user} />
|
||||||
return <GroupSection key={id} group={group} user={user} />
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
})}
|
})}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -129,98 +91,129 @@ const Home = (props: { auth: { user: User } | null }) => {
|
||||||
|
|
||||||
function SearchSection(props: {
|
function SearchSection(props: {
|
||||||
label: string
|
label: string
|
||||||
user: User | null
|
user: User | null | undefined | undefined
|
||||||
sort: Sort
|
sort: Sort
|
||||||
yourBets?: boolean
|
yourBets?: boolean
|
||||||
|
followed?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { label, user, sort, yourBets } = props
|
const { label, user, sort, yourBets, followed } = props
|
||||||
const href = `/home?s=${sort}`
|
const href = `/home?s=${sort}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<SiteLink className="mb-2 text-xl" href={href}>
|
<SiteLink className="mb-2 text-xl" href={href}>
|
||||||
{label}
|
{label}{' '}
|
||||||
|
<ArrowSmRightIcon
|
||||||
|
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
<ContractSearch
|
<ContractSearch
|
||||||
user={user}
|
user={user}
|
||||||
defaultSort={sort}
|
defaultSort={sort}
|
||||||
additionalFilter={yourBets ? { yourBets: true } : undefined}
|
additionalFilter={
|
||||||
noControls
|
yourBets
|
||||||
// persistPrefix={`experimental-home-${sort}`}
|
? { yourBets: true }
|
||||||
renderContracts={(contracts, loadMore) =>
|
: followed
|
||||||
contracts ? (
|
? { followed: true }
|
||||||
<DoubleCarousel
|
|
||||||
contracts={contracts}
|
|
||||||
seeMoreUrl={href}
|
|
||||||
showTime={
|
|
||||||
sort === 'close-date' || sort === 'resolve-date'
|
|
||||||
? sort
|
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
loadMore={loadMore}
|
noControls
|
||||||
/>
|
maxResults={6}
|
||||||
) : (
|
persistPrefix={`experimental-home-${sort}`}
|
||||||
<LoadingIndicator />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupSection(props: { group: Group; user: User | null }) {
|
function GroupSection(props: {
|
||||||
|
group: Group
|
||||||
|
user: User | null | undefined | undefined
|
||||||
|
}) {
|
||||||
const { group, user } = props
|
const { group, user } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<GroupLinkItem className="mb-2 text-xl" group={group} />
|
<SiteLink className="mb-2 text-xl" href={groupPath(group.slug)}>
|
||||||
|
{group.name}{' '}
|
||||||
|
<ArrowSmRightIcon
|
||||||
|
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</SiteLink>
|
||||||
<ContractSearch
|
<ContractSearch
|
||||||
user={user}
|
user={user}
|
||||||
defaultSort={'score'}
|
defaultSort={'score'}
|
||||||
additionalFilter={{ groupSlug: group.slug }}
|
additionalFilter={{ groupSlug: group.slug }}
|
||||||
noControls
|
noControls
|
||||||
// persistPrefix={`experimental-home-${group.slug}`}
|
maxResults={6}
|
||||||
renderContracts={(contracts, loadMore) =>
|
persistPrefix={`experimental-home-${group.slug}`}
|
||||||
contracts ? (
|
|
||||||
contracts.length == 0 ? (
|
|
||||||
<div className="m-2 text-gray-500">No open markets</div>
|
|
||||||
) : (
|
|
||||||
<DoubleCarousel
|
|
||||||
contracts={contracts}
|
|
||||||
seeMoreUrl={`/group/${group.slug}`}
|
|
||||||
loadMore={loadMore}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<LoadingIndicator />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditDoneButton(props: {
|
function DailyMoversSection(props: { userId: string | null | undefined }) {
|
||||||
isEditing: boolean
|
const { userId } = props
|
||||||
setIsEditing: (isEditing: boolean) => void
|
const changes = useProbChanges(userId ?? '')
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const { isEditing, setIsEditing, className } = props
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Col className="gap-2">
|
||||||
size="lg"
|
<SiteLink className="text-xl" href={'/daily-movers'}>
|
||||||
color={isEditing ? 'blue' : 'gray-white'}
|
Daily movers{' '}
|
||||||
className={clsx(className, 'flex')}
|
<ArrowSmRightIcon
|
||||||
onClick={() => {
|
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
||||||
setIsEditing(!isEditing)
|
aria-hidden="true"
|
||||||
}}
|
/>
|
||||||
>
|
</SiteLink>
|
||||||
{!isEditing && (
|
<ProbChangeTable changes={changes} />
|
||||||
<PencilIcon className={clsx('mr-2 h-[24px] w-5')} aria-hidden="true" />
|
</Col>
|
||||||
)}
|
)
|
||||||
{isEditing ? 'Done' : 'Edit'}
|
}
|
||||||
|
|
||||||
|
function EditButton(props: { className?: string }) {
|
||||||
|
const { className } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SiteLink href="/experimental/home/edit">
|
||||||
|
<Button size="lg" color="gray-white" className={clsx(className, 'flex')}>
|
||||||
|
<PencilIcon className={clsx('mr-2 h-[24px] w-5')} aria-hidden="true" />{' '}
|
||||||
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</SiteLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DailyProfitAndBalance(props: {
|
||||||
|
userId: string | null | undefined
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { userId, className } = props
|
||||||
|
const metrics = usePortfolioHistory(userId ?? '', 'daily') ?? []
|
||||||
|
const [first, last] = [metrics[0], metrics[metrics.length - 1]]
|
||||||
|
|
||||||
|
if (first === undefined || last === undefined) return null
|
||||||
|
|
||||||
|
const profit =
|
||||||
|
calculatePortfolioProfit(last) - calculatePortfolioProfit(first)
|
||||||
|
|
||||||
|
const balanceChange = last.balance - first.balance
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(className, 'text-lg')}>
|
||||||
|
<span className={clsx(profit >= 0 ? 'text-green-500' : 'text-red-500')}>
|
||||||
|
{profit >= 0 && '+'}
|
||||||
|
{formatMoney(profit)}
|
||||||
|
</span>{' '}
|
||||||
|
profit and{' '}
|
||||||
|
<span
|
||||||
|
className={clsx(balanceChange >= 0 ? 'text-green-500' : 'text-red-500')}
|
||||||
|
>
|
||||||
|
{balanceChange >= 0 && '+'}
|
||||||
|
{formatMoney(balanceChange)}
|
||||||
|
</span>{' '}
|
||||||
|
balance today
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -231,6 +231,7 @@ export default function GroupPage(props: {
|
||||||
defaultSort={'newest'}
|
defaultSort={'newest'}
|
||||||
defaultFilter={suggestedFilter}
|
defaultFilter={suggestedFilter}
|
||||||
additionalFilter={{ groupSlug: group.slug }}
|
additionalFilter={{ groupSlug: group.slug }}
|
||||||
|
persistPrefix={`group-${group.slug}`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -390,7 +390,7 @@ function IncomeNotificationItem(props: {
|
||||||
reasonText = !simple
|
reasonText = !simple
|
||||||
? `Bonus for ${
|
? `Bonus for ${
|
||||||
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
|
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
|
||||||
} new bettors on`
|
} new traders on`
|
||||||
: 'bonus on'
|
: 'bonus on'
|
||||||
} else if (sourceType === 'tip') {
|
} else if (sourceType === 'tip') {
|
||||||
reasonText = !simple ? `tipped you on` : `in tips on`
|
reasonText = !simple ? `tipped you on` : `in tips on`
|
||||||
|
@ -508,7 +508,7 @@ function IncomeNotificationItem(props: {
|
||||||
{(isTip || isUniqueBettorBonus) && (
|
{(isTip || isUniqueBettorBonus) && (
|
||||||
<MultiUserTransactionLink
|
<MultiUserTransactionLink
|
||||||
userInfos={userLinks}
|
userInfos={userLinks}
|
||||||
modalLabel={isTip ? 'Who tipped you' : 'Unique bettors'}
|
modalLabel={isTip ? 'Who tipped you' : 'Unique traders'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Row className={'line-clamp-2 flex max-w-xl'}>
|
<Row className={'line-clamp-2 flex max-w-xl'}>
|
||||||
|
|
|
@ -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'
|
||||||
|
@ -16,17 +16,27 @@ import { Col } from 'web/components/layout/col'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import Custom404 from 'web/pages/404'
|
import Custom404 from 'web/pages/404'
|
||||||
import { UserLink } from 'web/components/user-link'
|
import { UserLink } from 'web/components/user-link'
|
||||||
|
import { listAllCommentsOnPost } from 'web/lib/firebase/comments'
|
||||||
|
import { PostComment } from 'common/comment'
|
||||||
|
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[] } }) {
|
export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||||
const { slugs } = props.params
|
const { slugs } = props.params
|
||||||
|
|
||||||
const post = await getPostBySlug(slugs[0])
|
const post = await getPostBySlug(slugs[0])
|
||||||
const creator = post ? await getUser(post.creatorId) : null
|
const creator = post ? await getUser(post.creatorId) : null
|
||||||
|
const comments = post && (await listAllCommentsOnPost(post.id))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
post: post,
|
post: post,
|
||||||
creator: creator,
|
creator: creator,
|
||||||
|
comments: comments,
|
||||||
},
|
},
|
||||||
|
|
||||||
revalidate: 60, // regenerate after a minute
|
revalidate: 60, // regenerate after a minute
|
||||||
|
@ -37,32 +47,41 @@ export async function getStaticPaths() {
|
||||||
return { paths: [], fallback: 'blocking' }
|
return { paths: [], fallback: 'blocking' }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PostPage(props: { post: Post; creator: User }) {
|
export default function PostPage(props: {
|
||||||
|
post: Post
|
||||||
|
creator: User
|
||||||
|
comments: PostComment[]
|
||||||
|
}) {
|
||||||
const [isShareOpen, setShareOpen] = useState(false)
|
const [isShareOpen, setShareOpen] = useState(false)
|
||||||
|
const { creator } = props
|
||||||
|
const post = usePost(props.post.id) ?? props.post
|
||||||
|
|
||||||
if (props.post == null) {
|
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 />
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareUrl = `https://${ENV_CONFIG.domain}${postPath(props?.post.slug)}`
|
|
||||||
|
|
||||||
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={props.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
|
||||||
className="text-neutral"
|
className="text-neutral"
|
||||||
name={props.creator.name}
|
name={creator.name}
|
||||||
username={props.creator.username}
|
username={creator.username}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col className="px-2">
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
color="gray-white"
|
color="gray-white"
|
||||||
|
@ -88,10 +107,121 @@ export default function PostPage(props: { post: Post; creator: User }) {
|
||||||
<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={props.post.content} />
|
{user && user.id === post.creatorId ? (
|
||||||
|
<RichEditPost post={post} />
|
||||||
|
) : (
|
||||||
|
<Content content={post.content} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
<div className="rounded-lg bg-white px-6 py-4 sm:py-0">
|
||||||
|
<PostCommentsActivity
|
||||||
|
post={post}
|
||||||
|
comments={comments}
|
||||||
|
tips={tips}
|
||||||
|
user={creator}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PostCommentsActivity(props: {
|
||||||
|
post: Post
|
||||||
|
comments: PostComment[]
|
||||||
|
tips: CommentTipMap
|
||||||
|
user: User | null | undefined
|
||||||
|
}) {
|
||||||
|
const { post, comments, user, tips } = props
|
||||||
|
const commentsByUserId = groupBy(comments, (c) => c.userId)
|
||||||
|
const commentsByParentId = groupBy(comments, (c) => c.replyToCommentId ?? '_')
|
||||||
|
const topLevelComments = sortBy(
|
||||||
|
commentsByParentId['_'] ?? [],
|
||||||
|
(c) => -c.createdTime
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className="p-2">
|
||||||
|
<PostCommentInput post={post} />
|
||||||
|
{topLevelComments.map((parent) => (
|
||||||
|
<PostCommentThread
|
||||||
|
key={parent.id}
|
||||||
|
user={user}
|
||||||
|
post={post}
|
||||||
|
parentComment={parent}
|
||||||
|
threadComments={sortBy(
|
||||||
|
commentsByParentId[parent.id] ?? [],
|
||||||
|
(c) => c.createdTime
|
||||||
|
)}
|
||||||
|
tips={tips}
|
||||||
|
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(
|
||||||
|
({ tourney, slug, numPeople }) =>
|
||||||
|
tourney.award && (
|
||||||
<div key={slug}>
|
<div key={slug}>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
url={groupPath(slug)}
|
url={groupPath(slug, 'about')}
|
||||||
title={tourney.title}
|
title={tourney.title}
|
||||||
ppl={numPeople}
|
ppl={numPeople}
|
||||||
award={tourney.award}
|
award={tourney.award}
|
||||||
endTime={tourney.endTime}
|
endTime={tourney.endTime}
|
||||||
/>
|
/>
|
||||||
<span>{tourney.blurb}</span>
|
<span className="text-gray-500">{tourney.blurb}</span>
|
||||||
<MarketCarousel slug={slug} />
|
<MarketCarousel slug={slug} />
|
||||||
</div>
|
</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 && (
|
||||||
|
@ -237,7 +303,7 @@ const MarketCarousel = (props: { slug: string }) => {
|
||||||
key={m.id}
|
key={m.id}
|
||||||
contract={m}
|
contract={m}
|
||||||
hideGroupLink
|
hideGroupLink
|
||||||
className="mb-2 max-h-[200px] w-96 shrink-0"
|
className="mb-2 max-h-[200px] w-96 shrink-0 snap-start scroll-m-4 md:snap-align-none"
|
||||||
questionClass="line-clamp-3"
|
questionClass="line-clamp-3"
|
||||||
trackingPostfix=" tournament"
|
trackingPostfix=" tournament"
|
||||||
/>
|
/>
|
||||||
|
|
172
web/posts/post-comments.tsx
Normal file
172
web/posts/post-comments.tsx
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import { track } from '@amplitude/analytics-browser'
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { PostComment } from 'common/comment'
|
||||||
|
import { Post } from 'common/post'
|
||||||
|
import { User } from 'common/user'
|
||||||
|
import { Dictionary } from 'lodash'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Avatar } from 'web/components/avatar'
|
||||||
|
import { CommentInput } from 'web/components/comment-input'
|
||||||
|
import { Content } from 'web/components/editor'
|
||||||
|
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
||||||
|
import { Col } from 'web/components/layout/col'
|
||||||
|
import { Row } from 'web/components/layout/row'
|
||||||
|
import { Tipper } from 'web/components/tipper'
|
||||||
|
import { UserLink } from 'web/components/user-link'
|
||||||
|
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { createCommentOnPost } from 'web/lib/firebase/comments'
|
||||||
|
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
|
export function PostCommentThread(props: {
|
||||||
|
user: User | null | undefined
|
||||||
|
post: Post
|
||||||
|
threadComments: PostComment[]
|
||||||
|
tips: CommentTipMap
|
||||||
|
parentComment: PostComment
|
||||||
|
commentsByUserId: Dictionary<PostComment[]>
|
||||||
|
}) {
|
||||||
|
const { post, threadComments, tips, parentComment } = props
|
||||||
|
const [showReply, setShowReply] = useState(false)
|
||||||
|
const [replyTo, setReplyTo] = useState<{ id: string; username: string }>()
|
||||||
|
|
||||||
|
function scrollAndOpenReplyInput(comment: PostComment) {
|
||||||
|
setReplyTo({ id: comment.userId, username: comment.userUsername })
|
||||||
|
setShowReply(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className="relative w-full items-stretch gap-3 pb-4">
|
||||||
|
<span
|
||||||
|
className="absolute top-5 left-4 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{[parentComment].concat(threadComments).map((comment, commentIdx) => (
|
||||||
|
<PostComment
|
||||||
|
key={comment.id}
|
||||||
|
indent={commentIdx != 0}
|
||||||
|
post={post}
|
||||||
|
comment={comment}
|
||||||
|
tips={tips[comment.id]}
|
||||||
|
onReplyClick={scrollAndOpenReplyInput}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{showReply && (
|
||||||
|
<Col className="-pb-2 relative ml-6">
|
||||||
|
<span
|
||||||
|
className="absolute -left-1 -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<PostCommentInput
|
||||||
|
post={post}
|
||||||
|
parentCommentId={parentComment.id}
|
||||||
|
replyToUser={replyTo}
|
||||||
|
onSubmitComment={() => setShowReply(false)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostCommentInput(props: {
|
||||||
|
post: Post
|
||||||
|
parentCommentId?: string
|
||||||
|
replyToUser?: { id: string; username: string }
|
||||||
|
onSubmitComment?: () => void
|
||||||
|
}) {
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
const { post, parentCommentId, replyToUser } = props
|
||||||
|
|
||||||
|
async function onSubmitComment(editor: Editor) {
|
||||||
|
if (!user) {
|
||||||
|
track('sign in to comment')
|
||||||
|
return await firebaseLogin()
|
||||||
|
}
|
||||||
|
await createCommentOnPost(post.id, editor.getJSON(), user, parentCommentId)
|
||||||
|
props.onSubmitComment?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommentInput
|
||||||
|
replyToUser={replyToUser}
|
||||||
|
parentCommentId={parentCommentId}
|
||||||
|
onSubmitComment={onSubmitComment}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostComment(props: {
|
||||||
|
post: Post
|
||||||
|
comment: PostComment
|
||||||
|
tips: CommentTips
|
||||||
|
indent?: boolean
|
||||||
|
probAtCreatedTime?: number
|
||||||
|
onReplyClick?: (comment: PostComment) => void
|
||||||
|
}) {
|
||||||
|
const { post, comment, tips, indent, onReplyClick } = props
|
||||||
|
const { text, content, userUsername, userName, userAvatarUrl, createdTime } =
|
||||||
|
comment
|
||||||
|
|
||||||
|
const [highlighted, setHighlighted] = useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
useEffect(() => {
|
||||||
|
if (router.asPath.endsWith(`#${comment.id}`)) {
|
||||||
|
setHighlighted(true)
|
||||||
|
}
|
||||||
|
}, [comment.id, router.asPath])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
id={comment.id}
|
||||||
|
className={clsx(
|
||||||
|
'relative',
|
||||||
|
indent ? 'ml-6' : '',
|
||||||
|
highlighted ? `-m-1.5 rounded bg-indigo-500/[0.2] p-1.5` : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/*draw a gray line from the comment to the left:*/}
|
||||||
|
{indent ? (
|
||||||
|
<span
|
||||||
|
className="absolute -left-1 -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Avatar size="sm" username={userUsername} avatarUrl={userAvatarUrl} />
|
||||||
|
<div className="ml-1.5 min-w-0 flex-1 pl-0.5 sm:ml-3">
|
||||||
|
<div className="mt-0.5 text-sm text-gray-500">
|
||||||
|
<UserLink
|
||||||
|
className="text-gray-500"
|
||||||
|
username={userUsername}
|
||||||
|
name={userName}
|
||||||
|
/>{' '}
|
||||||
|
<CopyLinkDateTimeComponent
|
||||||
|
prefix={comment.userName}
|
||||||
|
slug={post.slug}
|
||||||
|
createdTime={createdTime}
|
||||||
|
elementId={comment.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Content
|
||||||
|
className="mt-2 text-[15px] text-gray-700"
|
||||||
|
content={content || text}
|
||||||
|
smallImage
|
||||||
|
/>
|
||||||
|
<Row className="mt-2 items-center gap-6 text-xs text-gray-500">
|
||||||
|
<Tipper comment={comment} tips={tips ?? {}} />
|
||||||
|
{onReplyClick && (
|
||||||
|
<button
|
||||||
|
className="font-bold hover:underline"
|
||||||
|
onClick={() => onReplyClick(comment)}
|
||||||
|
>
|
||||||
|
Reply
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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,21 +97,34 @@ 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':
|
||||||
|
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'])
|
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]
|
||||||
|
@ -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,11 +145,13 @@
|
||||||
action="guess.html"
|
action="guess.html"
|
||||||
style="display: flex; flex-direction: column; align-items: center"
|
style="display: flex; flex-direction: column; align-items: center"
|
||||||
>
|
>
|
||||||
|
<div class="option-row">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
id="counterspell"
|
id="counterspell"
|
||||||
name="whichguesser"
|
name="whichguesser"
|
||||||
value="counterspell"
|
value="counterspell"
|
||||||
|
onchange="updateSettingDefault(true, true, false)"
|
||||||
checked
|
checked
|
||||||
/>
|
/>
|
||||||
<label class="radio-label" for="counterspell">
|
<label class="radio-label" for="counterspell">
|
||||||
|
@ -138,18 +160,44 @@
|
||||||
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/71cfcba5-1571-48b8-a3db-55dca135506e.jpg?1562843855"
|
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
|
<h3>Counterspell Guesser</h3></label
|
||||||
><br />
|
>
|
||||||
|
<img
|
||||||
|
class="level-badge"
|
||||||
|
src="https://static.wikia.nocookie.net/mtgsalvation_gamepedia/images/a/a8/Advanced_level.jpg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
<input type="radio" id="burn" name="whichguesser" value="burn" />
|
<div class="option-row">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="burn"
|
||||||
|
name="whichguesser"
|
||||||
|
value="burn"
|
||||||
|
onchange="updateSettingDefault(true, true, false)"
|
||||||
|
/>
|
||||||
<label class="radio-label" for="burn">
|
<label class="radio-label" for="burn">
|
||||||
<img
|
<img
|
||||||
class="thumbnail"
|
class="thumbnail"
|
||||||
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60b2fae1-242b-45e0-a757-b1adc02c06f3.jpg?1562760596"
|
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
|
<h3>Match With Hot Singles</h3></label
|
||||||
><br />
|
>
|
||||||
|
<img
|
||||||
|
class="level-badge"
|
||||||
|
src="https://static.wikia.nocookie.net/mtgsalvation_gamepedia/images/a/a8/Advanced_level.jpg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
<input type="radio" id="beast" name="whichguesser" value="beast" />
|
<div class="option-row">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="beast"
|
||||||
|
name="whichguesser"
|
||||||
|
value="beast"
|
||||||
|
onchange="updateSettingDefault(true, true, false)"
|
||||||
|
/>
|
||||||
<label class="radio-label" for="beast">
|
<label class="radio-label" for="beast">
|
||||||
<img
|
<img
|
||||||
class="thumbnail"
|
class="thumbnail"
|
||||||
|
@ -157,16 +205,55 @@
|
||||||
/>
|
/>
|
||||||
<h3>Finding Fantastic Beasts</h3></label
|
<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 />
|
<br />
|
||||||
|
|
||||||
<input type="radio" id="basic" name="whichguesser" value="basic" />
|
<div class="option-row">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="basic"
|
||||||
|
name="whichguesser"
|
||||||
|
value="basic"
|
||||||
|
onchange="updateSettingDefault(true, true, true)"
|
||||||
|
/>
|
||||||
<label class="radio-label" for="basic">
|
<label class="radio-label" for="basic">
|
||||||
<img
|
<img
|
||||||
class="thumbnail"
|
class="thumbnail"
|
||||||
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/03683fbb-9843-4c14-bb95-387150e97c90.jpg?1642161346"
|
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
|
<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