Merge branch 'main' into loans2
This commit is contained in:
commit
d79783ea25
|
@ -565,6 +565,30 @@ Improve the lives of the world's most vulnerable people.
|
||||||
Reduce the number of easily preventable deaths worldwide.
|
Reduce the number of easily preventable deaths worldwide.
|
||||||
Work towards sustainable, systemic change.`,
|
Work towards sustainable, systemic change.`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'YIMBY Law',
|
||||||
|
website: 'https://www.yimbylaw.org/',
|
||||||
|
photo: 'https://i.imgur.com/zlzp21Z.png',
|
||||||
|
preview:
|
||||||
|
'YIMBY Law works to make housing in California more accessible and affordable, by enforcing state housing laws.',
|
||||||
|
description: `
|
||||||
|
YIMBY Law works to make housing in California more accessible and affordable. Our method is to enforce state housing laws, and some examples are outlined below. We send letters to cities considering zoning or general plan compliant housing developments informing them of their duties under state law, and sue them when they do not comply.
|
||||||
|
|
||||||
|
If you would like to support our work, you can do so by getting involved or by donating.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CaRLA',
|
||||||
|
website: 'https://carlaef.org/',
|
||||||
|
photo: 'https://i.imgur.com/IsNVTOY.png',
|
||||||
|
preview:
|
||||||
|
'The California Renters Legal Advocacy and Education Fund’s core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.',
|
||||||
|
description: `
|
||||||
|
The California Renters Legal Advocacy and Education Fund’s core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.
|
||||||
|
|
||||||
|
CaRLA uses legal advocacy and education to ensure all cities comply with their own zoning and state housing laws and do their part to help solve the state’s housing shortage.
|
||||||
|
|
||||||
|
In addition to housing impact litigation, we provide free legal aid, education and workshops, counseling and advocacy to advocates, homeowners, small developers, and city and state government officials.`,
|
||||||
|
},
|
||||||
].map((charity) => {
|
].map((charity) => {
|
||||||
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
|
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import type { JSONContent } from '@tiptap/core'
|
import type { JSONContent } from '@tiptap/core'
|
||||||
|
|
||||||
|
export type AnyCommentType = OnContract | OnGroup
|
||||||
|
|
||||||
// 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.
|
||||||
export type Comment = {
|
export type Comment<T extends AnyCommentType = AnyCommentType> = {
|
||||||
id: string
|
id: string
|
||||||
contractId?: string
|
|
||||||
groupId?: string
|
|
||||||
betId?: string
|
|
||||||
answerOutcome?: string
|
|
||||||
replyToCommentId?: string
|
replyToCommentId?: string
|
||||||
userId: string
|
userId: string
|
||||||
|
|
||||||
|
@ -20,4 +18,21 @@ export type Comment = {
|
||||||
userName: string
|
userName: string
|
||||||
userUsername: string
|
userUsername: string
|
||||||
userAvatarUrl?: string
|
userAvatarUrl?: string
|
||||||
|
} & T
|
||||||
|
|
||||||
|
type OnContract = {
|
||||||
|
commentType: 'contract'
|
||||||
|
contractId: string
|
||||||
|
contractSlug: string
|
||||||
|
contractQuestion: string
|
||||||
|
answerOutcome?: string
|
||||||
|
betId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OnGroup = {
|
||||||
|
commentType: 'group'
|
||||||
|
groupId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContractComment = Comment<OnContract>
|
||||||
|
export type GroupComment = Comment<OnGroup>
|
||||||
|
|
|
@ -1,187 +0,0 @@
|
||||||
import { union, sum, sumBy, sortBy, groupBy, mapValues } from 'lodash'
|
|
||||||
import { Bet } from './bet'
|
|
||||||
import { Contract } from './contract'
|
|
||||||
import { ClickEvent } from './tracking'
|
|
||||||
import { filterDefined } from './util/array'
|
|
||||||
import { addObjects } from './util/object'
|
|
||||||
|
|
||||||
export const MAX_FEED_CONTRACTS = 75
|
|
||||||
|
|
||||||
export const getRecommendedContracts = (
|
|
||||||
contractsById: { [contractId: string]: Contract },
|
|
||||||
yourBetOnContractIds: string[]
|
|
||||||
) => {
|
|
||||||
const contracts = Object.values(contractsById)
|
|
||||||
const yourContracts = filterDefined(
|
|
||||||
yourBetOnContractIds.map((contractId) => contractsById[contractId])
|
|
||||||
)
|
|
||||||
|
|
||||||
const yourContractIds = new Set(yourContracts.map((c) => c.id))
|
|
||||||
const notYourContracts = contracts.filter((c) => !yourContractIds.has(c.id))
|
|
||||||
|
|
||||||
const yourWordFrequency = contractsToWordFrequency(yourContracts)
|
|
||||||
const otherWordFrequency = contractsToWordFrequency(notYourContracts)
|
|
||||||
const words = union(
|
|
||||||
Object.keys(yourWordFrequency),
|
|
||||||
Object.keys(otherWordFrequency)
|
|
||||||
)
|
|
||||||
|
|
||||||
const yourWeightedFrequency = Object.fromEntries(
|
|
||||||
words.map((word) => {
|
|
||||||
const [yourFreq, otherFreq] = [
|
|
||||||
yourWordFrequency[word] ?? 0,
|
|
||||||
otherWordFrequency[word] ?? 0,
|
|
||||||
]
|
|
||||||
|
|
||||||
const score = yourFreq / (yourFreq + otherFreq + 0.0001)
|
|
||||||
|
|
||||||
return [word, score]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// console.log(
|
|
||||||
// 'your weighted frequency',
|
|
||||||
// _.sortBy(_.toPairs(yourWeightedFrequency), ([, freq]) => -freq)
|
|
||||||
// )
|
|
||||||
|
|
||||||
const scoredContracts = contracts.map((contract) => {
|
|
||||||
const wordFrequency = contractToWordFrequency(contract)
|
|
||||||
|
|
||||||
const score = sumBy(Object.keys(wordFrequency), (word) => {
|
|
||||||
const wordFreq = wordFrequency[word] ?? 0
|
|
||||||
const weight = yourWeightedFrequency[word] ?? 0
|
|
||||||
return wordFreq * weight
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
contract,
|
|
||||||
score,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return sortBy(scoredContracts, (scored) => -scored.score).map(
|
|
||||||
(scored) => scored.contract
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const contractToText = (contract: Contract) => {
|
|
||||||
const { description, question, tags, creatorUsername } = contract
|
|
||||||
return `${creatorUsername} ${question} ${tags.join(' ')} ${description}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_CHARS_IN_WORD = 100
|
|
||||||
|
|
||||||
const getWordsCount = (text: string) => {
|
|
||||||
const normalizedText = text.replace(/[^a-zA-Z]/g, ' ').toLowerCase()
|
|
||||||
const words = normalizedText
|
|
||||||
.split(' ')
|
|
||||||
.filter((word) => word)
|
|
||||||
.filter((word) => word.length <= MAX_CHARS_IN_WORD)
|
|
||||||
|
|
||||||
const counts: { [word: string]: number } = {}
|
|
||||||
for (const word of words) {
|
|
||||||
if (counts[word]) counts[word]++
|
|
||||||
else counts[word] = 1
|
|
||||||
}
|
|
||||||
return counts
|
|
||||||
}
|
|
||||||
|
|
||||||
const toFrequency = (counts: { [word: string]: number }) => {
|
|
||||||
const total = sum(Object.values(counts))
|
|
||||||
return mapValues(counts, (count) => count / total)
|
|
||||||
}
|
|
||||||
|
|
||||||
const contractToWordFrequency = (contract: Contract) =>
|
|
||||||
toFrequency(getWordsCount(contractToText(contract)))
|
|
||||||
|
|
||||||
const contractsToWordFrequency = (contracts: Contract[]) => {
|
|
||||||
const frequencySum = contracts
|
|
||||||
.map(contractToWordFrequency)
|
|
||||||
.reduce(addObjects, {})
|
|
||||||
|
|
||||||
return toFrequency(frequencySum)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getWordScores = (
|
|
||||||
contracts: Contract[],
|
|
||||||
contractViewCounts: { [contractId: string]: number },
|
|
||||||
clicks: ClickEvent[],
|
|
||||||
bets: Bet[]
|
|
||||||
) => {
|
|
||||||
const contractClicks = groupBy(clicks, (click) => click.contractId)
|
|
||||||
const contractBets = groupBy(bets, (bet) => bet.contractId)
|
|
||||||
|
|
||||||
const yourContracts = contracts.filter(
|
|
||||||
(c) =>
|
|
||||||
contractViewCounts[c.id] || contractClicks[c.id] || contractBets[c.id]
|
|
||||||
)
|
|
||||||
const yourTfIdf = calculateContractTfIdf(yourContracts)
|
|
||||||
|
|
||||||
const contractWordScores = mapValues(yourTfIdf, (wordsTfIdf, contractId) => {
|
|
||||||
const viewCount = contractViewCounts[contractId] ?? 0
|
|
||||||
const clickCount = contractClicks[contractId]?.length ?? 0
|
|
||||||
const betCount = contractBets[contractId]?.length ?? 0
|
|
||||||
|
|
||||||
const factor =
|
|
||||||
-1 * Math.log(viewCount + 1) +
|
|
||||||
10 * Math.log(betCount + clickCount / 4 + 1)
|
|
||||||
|
|
||||||
return mapValues(wordsTfIdf, (tfIdf) => tfIdf * factor)
|
|
||||||
})
|
|
||||||
|
|
||||||
const wordScores = Object.values(contractWordScores).reduce(addObjects, {})
|
|
||||||
const minScore = Math.min(...Object.values(wordScores))
|
|
||||||
const maxScore = Math.max(...Object.values(wordScores))
|
|
||||||
const normalizedWordScores = mapValues(
|
|
||||||
wordScores,
|
|
||||||
(score) => (score - minScore) / (maxScore - minScore)
|
|
||||||
)
|
|
||||||
|
|
||||||
// console.log(
|
|
||||||
// 'your word scores',
|
|
||||||
// _.sortBy(_.toPairs(normalizedWordScores), ([, score]) => -score).slice(0, 100),
|
|
||||||
// _.sortBy(_.toPairs(normalizedWordScores), ([, score]) => -score).slice(-100)
|
|
||||||
// )
|
|
||||||
|
|
||||||
return normalizedWordScores
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getContractScore(
|
|
||||||
contract: Contract,
|
|
||||||
wordScores: { [word: string]: number }
|
|
||||||
) {
|
|
||||||
if (Object.keys(wordScores).length === 0) return 1
|
|
||||||
|
|
||||||
const wordFrequency = contractToWordFrequency(contract)
|
|
||||||
const score = sumBy(Object.keys(wordFrequency), (word) => {
|
|
||||||
const wordFreq = wordFrequency[word] ?? 0
|
|
||||||
const weight = wordScores[word] ?? 0
|
|
||||||
return wordFreq * weight
|
|
||||||
})
|
|
||||||
|
|
||||||
return score
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caluculate Term Frequency-Inverse Document Frequency (TF-IDF):
|
|
||||||
// https://medium.datadriveninvestor.com/tf-idf-in-natural-language-processing-8db8ef4a7736
|
|
||||||
function calculateContractTfIdf(contracts: Contract[]) {
|
|
||||||
const contractFreq = contracts.map((c) => contractToWordFrequency(c))
|
|
||||||
const contractWords = contractFreq.map((freq) => Object.keys(freq))
|
|
||||||
|
|
||||||
const wordsCount: { [word: string]: number } = {}
|
|
||||||
for (const words of contractWords) {
|
|
||||||
for (const word of words) {
|
|
||||||
wordsCount[word] = (wordsCount[word] ?? 0) + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const wordIdf = mapValues(wordsCount, (count) =>
|
|
||||||
Math.log(contracts.length / count)
|
|
||||||
)
|
|
||||||
const contractWordsTfIdf = contractFreq.map((wordFreq) =>
|
|
||||||
mapValues(wordFreq, (freq, word) => freq * wordIdf[word])
|
|
||||||
)
|
|
||||||
return Object.fromEntries(
|
|
||||||
contracts.map((c, i) => [c.id, contractWordsTfIdf[i]])
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { isEqual } from 'lodash'
|
||||||
|
|
||||||
export function filterDefined<T>(array: (T | null | undefined)[]) {
|
export function filterDefined<T>(array: (T | null | undefined)[]) {
|
||||||
return array.filter((item) => item !== null && item !== undefined) as T[]
|
return array.filter((item) => item !== null && item !== undefined) as T[]
|
||||||
}
|
}
|
||||||
|
@ -26,7 +28,7 @@ export function groupConsecutive<T, U>(xs: T[], key: (x: T) => U) {
|
||||||
let curr = { key: key(xs[0]), items: [xs[0]] }
|
let curr = { key: key(xs[0]), items: [xs[0]] }
|
||||||
for (const x of xs.slice(1)) {
|
for (const x of xs.slice(1)) {
|
||||||
const k = key(x)
|
const k = key(x)
|
||||||
if (k !== curr.key) {
|
if (!isEqual(key, curr.key)) {
|
||||||
result.push(curr)
|
result.push(curr)
|
||||||
curr = { key: k, items: [x] }
|
curr = { key: k, items: [x] }
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -135,7 +135,8 @@ Requires no authorization.
|
||||||
// Market attributes. All times are in milliseconds since epoch
|
// Market attributes. All times are in milliseconds since epoch
|
||||||
closeTime?: number // Min of creator's chosen date, and resolutionTime
|
closeTime?: number // Min of creator's chosen date, and resolutionTime
|
||||||
question: string
|
question: string
|
||||||
description: string
|
description: JSONContent // Rich text content. See https://tiptap.dev/guide/output#option-1-json
|
||||||
|
textDescription: string // string description without formatting, images, or embeds
|
||||||
|
|
||||||
// A list of tags on each market. Any user can add tags to any market.
|
// A list of tags on each market. Any user can add tags to any market.
|
||||||
// This list also includes the predefined categories shown as filters on the home page.
|
// This list also includes the predefined categories shown as filters on the home page.
|
||||||
|
@ -162,6 +163,8 @@ Requires no authorization.
|
||||||
resolutionTime?: number
|
resolutionTime?: number
|
||||||
resolution?: string
|
resolution?: string
|
||||||
resolutionProbability?: number // Used for BINARY markets resolved to MKT
|
resolutionProbability?: number // Used for BINARY markets resolved to MKT
|
||||||
|
|
||||||
|
lastUpdatedTime?: number
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -541,6 +544,7 @@ Parameters:
|
||||||
- `outcomeType`: Required. One of `BINARY`, `FREE_RESPONSE`, or `NUMERIC`.
|
- `outcomeType`: Required. One of `BINARY`, `FREE_RESPONSE`, or `NUMERIC`.
|
||||||
- `question`: Required. The headline question for the market.
|
- `question`: Required. The headline question for the market.
|
||||||
- `description`: Required. A long description describing the rules for the market.
|
- `description`: Required. A long description describing the rules for the market.
|
||||||
|
- Note: string descriptions do **not** turn into links, mentions, formatted text. Instead, rich text descriptions must be in [TipTap json](https://tiptap.dev/guide/output#option-1-json).
|
||||||
- `closeTime`: Required. The time at which the market will close, represented as milliseconds since the epoch.
|
- `closeTime`: Required. The time at which the market will close, represented as milliseconds since the epoch.
|
||||||
- `tags`: Optional. An array of string tags for the market.
|
- `tags`: Optional. An array of string tags for the market.
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ A list of community-created projects built on, or related to, Manifold Markets.
|
||||||
- [manifold-markets-python](https://github.com/vluzko/manifold-markets-python) - Python tools for working with Manifold Markets (including various accuracy metrics)
|
- [manifold-markets-python](https://github.com/vluzko/manifold-markets-python) - Python tools for working with Manifold Markets (including various accuracy metrics)
|
||||||
- [ManifoldMarketManager](https://github.com/gappleto97/ManifoldMarketManager) - Python script and library to automatically manage markets
|
- [ManifoldMarketManager](https://github.com/gappleto97/ManifoldMarketManager) - Python script and library to automatically manage markets
|
||||||
- [manifeed](https://github.com/joy-void-joy/manifeed) - Tool that creates an RSS feed for new Manifold markets
|
- [manifeed](https://github.com/joy-void-joy/manifeed) - Tool that creates an RSS feed for new Manifold markets
|
||||||
|
- [manifold-sdk](https://github.com/keriwarr/manifold-sdk) - TypeScript/JavaScript client for the Manifold API
|
||||||
|
|
||||||
## Bots
|
## Bots
|
||||||
|
|
||||||
|
|
|
@ -496,6 +496,28 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"collectionGroup": "comments",
|
||||||
|
"fieldPath": "contractId",
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"order": "ASCENDING",
|
||||||
|
"queryScope": "COLLECTION"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": "DESCENDING",
|
||||||
|
"queryScope": "COLLECTION"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"arrayConfig": "CONTAINS",
|
||||||
|
"queryScope": "COLLECTION"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": "ASCENDING",
|
||||||
|
"queryScope": "COLLECTION_GROUP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"collectionGroup": "comments",
|
"collectionGroup": "comments",
|
||||||
"fieldPath": "createdTime",
|
"fieldPath": "createdTime",
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { CandidateBet } from '../../common/new-bet'
|
||||||
import { createChallengeAcceptedNotification } from './create-notification'
|
import { createChallengeAcceptedNotification } from './create-notification'
|
||||||
import { noFees } from '../../common/fees'
|
import { noFees } from '../../common/fees'
|
||||||
import { formatMoney, formatPercent } from '../../common/util/format'
|
import { formatMoney, formatPercent } from '../../common/util/format'
|
||||||
|
import { redeemShares } from './redeem-shares'
|
||||||
|
|
||||||
const bodySchema = z.object({
|
const bodySchema = z.object({
|
||||||
contractId: z.string(),
|
contractId: z.string(),
|
||||||
|
@ -163,5 +164,7 @@ export const acceptchallenge = newEndpoint({}, async (req, auth) => {
|
||||||
return yourNewBetDoc
|
return yourNewBetDoc
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await redeemShares(auth.uid, contractId)
|
||||||
|
|
||||||
return { betId: result.id }
|
return { betId: result.id }
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as Amplitude from '@amplitude/node'
|
||||||
import { DEV_CONFIG } from '../../common/envs/dev'
|
import { DEV_CONFIG } from '../../common/envs/dev'
|
||||||
import { PROD_CONFIG } from '../../common/envs/prod'
|
import { PROD_CONFIG } from '../../common/envs/prod'
|
||||||
|
|
||||||
import { isProd } from './utils'
|
import { isProd, tryOrLogError } from './utils'
|
||||||
|
|
||||||
const key = isProd() ? PROD_CONFIG.amplitudeApiKey : DEV_CONFIG.amplitudeApiKey
|
const key = isProd() ? PROD_CONFIG.amplitudeApiKey : DEV_CONFIG.amplitudeApiKey
|
||||||
|
|
||||||
|
@ -15,10 +15,12 @@ export const track = async (
|
||||||
eventProperties?: any,
|
eventProperties?: any,
|
||||||
amplitudeProperties?: Partial<Amplitude.Event>
|
amplitudeProperties?: Partial<Amplitude.Event>
|
||||||
) => {
|
) => {
|
||||||
await amp.logEvent({
|
return await tryOrLogError(
|
||||||
event_type: eventName,
|
amp.logEvent({
|
||||||
user_id: userId,
|
event_type: eventName,
|
||||||
event_properties: eventProperties,
|
user_id: userId,
|
||||||
...amplitudeProperties,
|
event_properties: eventProperties,
|
||||||
})
|
...amplitudeProperties,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,15 +14,17 @@ import {
|
||||||
import { slugify } from '../../common/util/slugify'
|
import { slugify } from '../../common/util/slugify'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
|
|
||||||
import { chargeUser, getContract } from './utils'
|
import { chargeUser, getContract, isProd } from './utils'
|
||||||
import { APIError, newEndpoint, validate, zTimestamp } from './api'
|
import { APIError, newEndpoint, validate, zTimestamp } from './api'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
FIXED_ANTE,
|
FIXED_ANTE,
|
||||||
getCpmmInitialLiquidity,
|
getCpmmInitialLiquidity,
|
||||||
getFreeAnswerAnte,
|
getFreeAnswerAnte,
|
||||||
getMultipleChoiceAntes,
|
getMultipleChoiceAntes,
|
||||||
getNumericAnte,
|
getNumericAnte,
|
||||||
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
} from '../../common/antes'
|
} from '../../common/antes'
|
||||||
import { Answer, getNoneAnswer } from '../../common/answer'
|
import { Answer, getNoneAnswer } from '../../common/answer'
|
||||||
import { getNewContract } from '../../common/new-contract'
|
import { getNewContract } from '../../common/new-contract'
|
||||||
|
@ -59,7 +61,7 @@ const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
|
||||||
|
|
||||||
const bodySchema = z.object({
|
const bodySchema = z.object({
|
||||||
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
|
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
|
||||||
description: descScehma.optional(),
|
description: descScehma.or(z.string()).optional(),
|
||||||
tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(),
|
tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(),
|
||||||
closeTime: zTimestamp().refine(
|
closeTime: zTimestamp().refine(
|
||||||
(date) => date.getTime() > new Date().getTime(),
|
(date) => date.getTime() > new Date().getTime(),
|
||||||
|
@ -133,41 +135,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
if (ante > user.balance)
|
if (ante > user.balance)
|
||||||
throw new APIError(400, `Balance must be at least ${ante}.`)
|
throw new APIError(400, `Balance must be at least ${ante}.`)
|
||||||
|
|
||||||
const slug = await getSlug(question)
|
let group: Group | null = null
|
||||||
const contractRef = firestore.collection('contracts').doc()
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'creating contract for',
|
|
||||||
user.username,
|
|
||||||
'on',
|
|
||||||
question,
|
|
||||||
'ante:',
|
|
||||||
ante || 0
|
|
||||||
)
|
|
||||||
|
|
||||||
const contract = getNewContract(
|
|
||||||
contractRef.id,
|
|
||||||
slug,
|
|
||||||
user,
|
|
||||||
question,
|
|
||||||
outcomeType,
|
|
||||||
description ?? {},
|
|
||||||
initialProb ?? 0,
|
|
||||||
ante,
|
|
||||||
closeTime.getTime(),
|
|
||||||
tags ?? [],
|
|
||||||
NUMERIC_BUCKET_COUNT,
|
|
||||||
min ?? 0,
|
|
||||||
max ?? 0,
|
|
||||||
isLogScale ?? false,
|
|
||||||
answers ?? []
|
|
||||||
)
|
|
||||||
|
|
||||||
if (ante) await chargeUser(user.id, ante, true)
|
|
||||||
|
|
||||||
await contractRef.create(contract)
|
|
||||||
|
|
||||||
let group = null
|
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
const groupDocRef = firestore.collection('groups').doc(groupId)
|
const groupDocRef = firestore.collection('groups').doc(groupId)
|
||||||
const groupDoc = await groupDocRef.get()
|
const groupDoc = await groupDocRef.get()
|
||||||
|
@ -186,15 +154,68 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
'User must be a member/creator of the group or group must be open to add markets to it.'
|
'User must be a member/creator of the group or group must be open to add markets to it.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const slug = await getSlug(question)
|
||||||
|
const contractRef = firestore.collection('contracts').doc()
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'creating contract for',
|
||||||
|
user.username,
|
||||||
|
'on',
|
||||||
|
question,
|
||||||
|
'ante:',
|
||||||
|
ante || 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// convert string descriptions into JSONContent
|
||||||
|
const newDescription =
|
||||||
|
typeof description === 'string'
|
||||||
|
? {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: description }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: description ?? {}
|
||||||
|
|
||||||
|
const contract = getNewContract(
|
||||||
|
contractRef.id,
|
||||||
|
slug,
|
||||||
|
user,
|
||||||
|
question,
|
||||||
|
outcomeType,
|
||||||
|
newDescription,
|
||||||
|
initialProb ?? 0,
|
||||||
|
ante,
|
||||||
|
closeTime.getTime(),
|
||||||
|
tags ?? [],
|
||||||
|
NUMERIC_BUCKET_COUNT,
|
||||||
|
min ?? 0,
|
||||||
|
max ?? 0,
|
||||||
|
isLogScale ?? false,
|
||||||
|
answers ?? []
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ante) await chargeUser(user.id, ante, true)
|
||||||
|
|
||||||
|
await contractRef.create(contract)
|
||||||
|
|
||||||
|
if (group != null) {
|
||||||
if (!group.contractIds.includes(contractRef.id)) {
|
if (!group.contractIds.includes(contractRef.id)) {
|
||||||
await createGroupLinks(group, [contractRef.id], auth.uid)
|
await createGroupLinks(group, [contractRef.id], auth.uid)
|
||||||
await groupDocRef.update({
|
const groupDocRef = firestore.collection('groups').doc(group.id)
|
||||||
|
groupDocRef.update({
|
||||||
contractIds: uniq([...group.contractIds, contractRef.id]),
|
contractIds: uniq([...group.contractIds, contractRef.id]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerId = user.id
|
const providerId = isProd()
|
||||||
|
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
|
||||||
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
||||||
const liquidityDoc = firestore
|
const liquidityDoc = firestore
|
|
@ -16,7 +16,7 @@ import {
|
||||||
cleanDisplayName,
|
cleanDisplayName,
|
||||||
cleanUsername,
|
cleanUsername,
|
||||||
} from '../../common/util/clean-username'
|
} from '../../common/util/clean-username'
|
||||||
import { sendWelcomeEmail } from './emails'
|
import { sendPersonalFollowupEmail, sendWelcomeEmail } from './emails'
|
||||||
import { isWhitelisted } from '../../common/envs/constants'
|
import { isWhitelisted } from '../../common/envs/constants'
|
||||||
import {
|
import {
|
||||||
CATEGORIES_GROUP_SLUG_POSTFIX,
|
CATEGORIES_GROUP_SLUG_POSTFIX,
|
||||||
|
@ -96,6 +96,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
|
||||||
|
|
||||||
await addUserToDefaultGroups(user)
|
await addUserToDefaultGroups(user)
|
||||||
await sendWelcomeEmail(user, privateUser)
|
await sendWelcomeEmail(user, privateUser)
|
||||||
|
await sendPersonalFollowupEmail(user, privateUser)
|
||||||
await track(auth.uid, 'create user', { username }, { ip: req.ip })
|
await track(auth.uid, 'create user', { username }, { ip: req.ip })
|
||||||
|
|
||||||
return { user, privateUser }
|
return { user, privateUser }
|
||||||
|
|
|
@ -128,7 +128,20 @@
|
||||||
<div
|
<div
|
||||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
<p class="text-build-content"
|
<p class="text-build-content"
|
||||||
style="line-height: 24px; text-align: center; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||||
|
data-testid="4XoHRGw1Y"><span
|
||||||
|
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||||
|
Hi {{name}},</span></p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content"
|
||||||
|
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||||
data-testid="4XoHRGw1Y"><span
|
data-testid="4XoHRGw1Y"><span
|
||||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">Thanks for
|
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">Thanks for
|
||||||
using Manifold Markets. Running low
|
using Manifold Markets. Running low
|
||||||
|
@ -161,6 +174,51 @@
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
|
||||||
|
data-testid="3Q8BP69fq"><span style="font-family:Arial, sans-serif;font-size:18px;">Did
|
||||||
|
you know, besides making correct predictions, there are
|
||||||
|
plenty of other ways to earn mana?</span></p>
|
||||||
|
<ul>
|
||||||
|
<li style="line-height:23px;"><span
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;">Receiving
|
||||||
|
tips on comments</span></li>
|
||||||
|
<li style="line-height:23px;"><span
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;">Unique
|
||||||
|
trader bonus for each user who bets on your
|
||||||
|
markets</span></li>
|
||||||
|
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
|
||||||
|
class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||||
|
target="_blank" href="https://manifold.markets/referrals"><span
|
||||||
|
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Referring
|
||||||
|
friends</u></span></a></span></li>
|
||||||
|
<li style="line-height:23px;"><a class="link-build-content"
|
||||||
|
style="color:inherit;; text-decoration: none;" target="_blank"
|
||||||
|
href="https://manifold.markets/group/bugs?s=most-traded"><span
|
||||||
|
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Reporting
|
||||||
|
bugs</u></span></a><span style="font-family:Arial, sans-serif;font-size:18px;">
|
||||||
|
and </span><a class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||||
|
target="_blank"
|
||||||
|
href="https://manifold.markets/group/manifold-features-25bad7c7792e/chat?s=most-traded"><span
|
||||||
|
style="color:#55575d;font-family:Arial;font-size:18px;"><u>giving
|
||||||
|
feedback</u></span></a></li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"> </p>
|
||||||
|
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
||||||
|
style="color:#000000;font-family:Arial;font-size:18px;">Cheers,</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
||||||
|
style="color:#000000;font-family:Arial;font-size:18px;">David
|
||||||
|
from Manifold</span></p>
|
||||||
|
<p class="text-build-content" data-testid="3Q8BP69fq"
|
||||||
|
style="margin: 10px 0; margin-bottom: 10px;"> </p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left"
|
<td align="left"
|
||||||
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -188,6 +188,56 @@
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
|
||||||
|
data-testid="3Q8BP69fq"><span style="font-family:Arial, sans-serif;font-size:18px;">Did
|
||||||
|
you know, besides betting and making predictions, you can also <a
|
||||||
|
class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||||
|
target="_blank" href="https://manifold.markets/create"><span
|
||||||
|
style="color:#55575d;font-family:Arial;font-size:18px;font-weight: bold;"><u>create
|
||||||
|
your
|
||||||
|
own
|
||||||
|
market</u></span></a> on
|
||||||
|
any question you care about?</span></p>
|
||||||
|
|
||||||
|
<p>More resources:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
|
||||||
|
class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||||
|
target="_blank" href="https://manifold.markets/about"><span
|
||||||
|
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Learn more</u></span></a>
|
||||||
|
about Manifold and how our markets work</span></li>
|
||||||
|
|
||||||
|
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
|
||||||
|
class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||||
|
target="_blank" href="https://manifold.markets/referrals"><span
|
||||||
|
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Refer
|
||||||
|
your friends</u></span></a> and earn M$500 for each signup!</span></li>
|
||||||
|
|
||||||
|
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
|
||||||
|
class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||||
|
target="_blank" href="https://discord.com/invite/eHQBNBqXuh"><span
|
||||||
|
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Join our Discord
|
||||||
|
chat</u></span></a></span></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"> </p>
|
||||||
|
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
||||||
|
style="color:#000000;font-family:Arial;font-size:18px;">Cheers,</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
||||||
|
style="color:#000000;font-family:Arial;font-size:18px;">David
|
||||||
|
from Manifold</span></p>
|
||||||
|
<p class="text-build-content" data-testid="3Q8BP69fq"
|
||||||
|
style="margin: 10px 0; margin-bottom: 10px;"> </p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left"
|
<td align="left"
|
||||||
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import * as dayjs from 'dayjs'
|
||||||
|
|
||||||
import { DOMAIN } from '../../common/envs/constants'
|
import { DOMAIN } from '../../common/envs/constants'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
|
@ -14,7 +16,7 @@ import {
|
||||||
import { getValueFromBucket } from '../../common/calculate-dpm'
|
import { getValueFromBucket } from '../../common/calculate-dpm'
|
||||||
import { formatNumericProbability } from '../../common/pseudo-numeric'
|
import { formatNumericProbability } from '../../common/pseudo-numeric'
|
||||||
|
|
||||||
import { sendTemplateEmail } from './send-email'
|
import { sendTemplateEmail, sendTextEmail } from './send-email'
|
||||||
import { getPrivateUser, getUser } from './utils'
|
import { getPrivateUser, getUser } from './utils'
|
||||||
import { getFunctionUrl } from '../../common/api'
|
import { getFunctionUrl } from '../../common/api'
|
||||||
import { richTextToString } from '../../common/util/parse'
|
import { richTextToString } from '../../common/util/parse'
|
||||||
|
@ -74,9 +76,8 @@ export const sendMarketResolutionEmail = async (
|
||||||
|
|
||||||
// Modify template here:
|
// Modify template here:
|
||||||
// https://app.mailgun.com/app/sending/domains/mg.manifold.markets/templates/edit/market-resolved/initial
|
// https://app.mailgun.com/app/sending/domains/mg.manifold.markets/templates/edit/market-resolved/initial
|
||||||
// Mailgun username: james@mantic.markets
|
|
||||||
|
|
||||||
await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
subject,
|
subject,
|
||||||
'market-resolved',
|
'market-resolved',
|
||||||
|
@ -152,7 +153,7 @@ export const sendWelcomeEmail = async (
|
||||||
const emailType = 'generic'
|
const emailType = 'generic'
|
||||||
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||||
|
|
||||||
await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
'Welcome to Manifold Markets!',
|
'Welcome to Manifold Markets!',
|
||||||
'welcome',
|
'welcome',
|
||||||
|
@ -166,6 +167,43 @@ export const sendWelcomeEmail = async (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sendPersonalFollowupEmail = async (
|
||||||
|
user: User,
|
||||||
|
privateUser: PrivateUser
|
||||||
|
) => {
|
||||||
|
if (!privateUser || !privateUser.email) return
|
||||||
|
|
||||||
|
const { name } = user
|
||||||
|
const firstName = name.split(' ')[0]
|
||||||
|
|
||||||
|
const emailBody = `Hi ${firstName},
|
||||||
|
|
||||||
|
Thanks for signing up! I'm one of the cofounders of Manifold Markets, and was wondering how you've found your exprience on the platform so far?
|
||||||
|
|
||||||
|
If you haven't already, I encourage you to try creating your own prediction market (https://manifold.markets/create) and joining our Discord chat (https://discord.com/invite/eHQBNBqXuh).
|
||||||
|
|
||||||
|
Feel free to reply to this email with any questions or concerns you have.
|
||||||
|
|
||||||
|
Cheers,
|
||||||
|
|
||||||
|
James
|
||||||
|
Cofounder of Manifold Markets
|
||||||
|
https://manifold.markets
|
||||||
|
`
|
||||||
|
|
||||||
|
const sendTime = dayjs().add(4, 'hours').toString()
|
||||||
|
|
||||||
|
await sendTextEmail(
|
||||||
|
privateUser.email,
|
||||||
|
'How are you finding Manifold?',
|
||||||
|
emailBody,
|
||||||
|
{
|
||||||
|
from: 'James from Manifold <james@manifold.markets>',
|
||||||
|
'o:deliverytime': sendTime,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const sendOneWeekBonusEmail = async (
|
export const sendOneWeekBonusEmail = async (
|
||||||
user: User,
|
user: User,
|
||||||
privateUser: PrivateUser
|
privateUser: PrivateUser
|
||||||
|
@ -183,7 +221,7 @@ export const sendOneWeekBonusEmail = async (
|
||||||
const emailType = 'generic'
|
const emailType = 'generic'
|
||||||
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||||
|
|
||||||
await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
'Manifold Markets one week anniversary gift',
|
'Manifold Markets one week anniversary gift',
|
||||||
'one-week',
|
'one-week',
|
||||||
|
@ -198,6 +236,37 @@ export const sendOneWeekBonusEmail = async (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sendCreatorGuideEmail = async (
|
||||||
|
user: User,
|
||||||
|
privateUser: PrivateUser
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!privateUser ||
|
||||||
|
!privateUser.email ||
|
||||||
|
privateUser.unsubscribedFromGenericEmails
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
const { name, id: userId } = user
|
||||||
|
const firstName = name.split(' ')[0]
|
||||||
|
|
||||||
|
const emailType = 'generic'
|
||||||
|
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||||
|
|
||||||
|
return await sendTemplateEmail(
|
||||||
|
privateUser.email,
|
||||||
|
'Market creation guide',
|
||||||
|
'creating-market',
|
||||||
|
{
|
||||||
|
name: firstName,
|
||||||
|
unsubscribeLink,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'David from Manifold <david@manifold.markets>',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const sendThankYouEmail = async (
|
export const sendThankYouEmail = async (
|
||||||
user: User,
|
user: User,
|
||||||
privateUser: PrivateUser
|
privateUser: PrivateUser
|
||||||
|
@ -215,7 +284,7 @@ export const sendThankYouEmail = async (
|
||||||
const emailType = 'generic'
|
const emailType = 'generic'
|
||||||
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||||
|
|
||||||
await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
'Thanks for your Manifold purchase',
|
'Thanks for your Manifold purchase',
|
||||||
'thank-you',
|
'thank-you',
|
||||||
|
@ -250,7 +319,7 @@ export const sendMarketCloseEmail = async (
|
||||||
const emailType = 'market-resolve'
|
const emailType = 'market-resolve'
|
||||||
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||||
|
|
||||||
await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
'Your market has closed',
|
'Your market has closed',
|
||||||
'market-close',
|
'market-close',
|
||||||
|
@ -309,7 +378,7 @@ export const sendNewCommentEmail = async (
|
||||||
if (contract.outcomeType === 'FREE_RESPONSE' && answerId && answerText) {
|
if (contract.outcomeType === 'FREE_RESPONSE' && answerId && answerText) {
|
||||||
const answerNumber = `#${answerId}`
|
const answerNumber = `#${answerId}`
|
||||||
|
|
||||||
await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
subject,
|
subject,
|
||||||
'market-answer-comment',
|
'market-answer-comment',
|
||||||
|
@ -332,7 +401,7 @@ export const sendNewCommentEmail = async (
|
||||||
bet.outcome
|
bet.outcome
|
||||||
)}`
|
)}`
|
||||||
}
|
}
|
||||||
await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
subject,
|
subject,
|
||||||
'market-comment',
|
'market-comment',
|
||||||
|
@ -377,7 +446,7 @@ export const sendNewAnswerEmail = async (
|
||||||
const subject = `New answer on ${question}`
|
const subject = `New answer on ${question}`
|
||||||
const from = `${name} <info@manifold.markets>`
|
const from = `${name} <info@manifold.markets>`
|
||||||
|
|
||||||
await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
subject,
|
subject,
|
||||||
'market-answer',
|
'market-answer',
|
||||||
|
|
|
@ -38,7 +38,7 @@ export * from './cancel-bet'
|
||||||
export * from './sell-bet'
|
export * from './sell-bet'
|
||||||
export * from './sell-shares'
|
export * from './sell-shares'
|
||||||
export * from './claim-manalink'
|
export * from './claim-manalink'
|
||||||
export * from './create-contract'
|
export * from './create-market'
|
||||||
export * from './add-liquidity'
|
export * from './add-liquidity'
|
||||||
export * from './withdraw-liquidity'
|
export * from './withdraw-liquidity'
|
||||||
export * from './create-group'
|
export * from './create-group'
|
||||||
|
@ -57,7 +57,7 @@ import { cancelbet } from './cancel-bet'
|
||||||
import { sellbet } from './sell-bet'
|
import { sellbet } from './sell-bet'
|
||||||
import { sellshares } from './sell-shares'
|
import { sellshares } from './sell-shares'
|
||||||
import { claimmanalink } from './claim-manalink'
|
import { claimmanalink } from './claim-manalink'
|
||||||
import { createmarket } from './create-contract'
|
import { createmarket } from './create-market'
|
||||||
import { addliquidity } from './add-liquidity'
|
import { addliquidity } from './add-liquidity'
|
||||||
import { withdrawliquidity } from './withdraw-liquidity'
|
import { withdrawliquidity } from './withdraw-liquidity'
|
||||||
import { creategroup } from './create-group'
|
import { creategroup } from './create-group'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { compact, uniq } from 'lodash'
|
import { compact, uniq } from 'lodash'
|
||||||
import { getContract, getUser, getValues } from './utils'
|
import { getContract, getUser, getValues } from './utils'
|
||||||
import { Comment } from '../../common/comment'
|
import { ContractComment } from '../../common/comment'
|
||||||
import { sendNewCommentEmail } from './emails'
|
import { sendNewCommentEmail } from './emails'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
|
@ -24,7 +24,12 @@ export const onCreateCommentOnContract = functions
|
||||||
if (!contract)
|
if (!contract)
|
||||||
throw new Error('Could not find contract corresponding with comment')
|
throw new Error('Could not find contract corresponding with comment')
|
||||||
|
|
||||||
const comment = change.data() as Comment
|
await change.ref.update({
|
||||||
|
contractSlug: contract.slug,
|
||||||
|
contractQuestion: contract.question,
|
||||||
|
})
|
||||||
|
|
||||||
|
const comment = change.data() as ContractComment
|
||||||
const lastCommentTime = comment.createdTime
|
const lastCommentTime = comment.createdTime
|
||||||
|
|
||||||
const commentCreator = await getUser(comment.userId)
|
const commentCreator = await getUser(comment.userId)
|
||||||
|
@ -59,7 +64,7 @@ export const onCreateCommentOnContract = functions
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const comments = await getValues<Comment>(
|
const comments = await getValues<ContractComment>(
|
||||||
firestore.collection('contracts').doc(contractId).collection('comments')
|
firestore.collection('contracts').doc(contractId).collection('comments')
|
||||||
)
|
)
|
||||||
const relatedSourceType = comment.replyToCommentId
|
const relatedSourceType = comment.replyToCommentId
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import { Comment } from '../../common/comment'
|
import { GroupComment } from '../../common/comment'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { Group } from '../../common/group'
|
import { Group } from '../../common/group'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
|
@ -14,7 +14,7 @@ export const onCreateCommentOnGroup = functions.firestore
|
||||||
groupId: string
|
groupId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const comment = change.data() as Comment
|
const comment = change.data() as GroupComment
|
||||||
const creatorSnapshot = await firestore
|
const creatorSnapshot = await firestore
|
||||||
.collection('users')
|
.collection('users')
|
||||||
.doc(comment.userId)
|
.doc(comment.userId)
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import { getUser } from './utils'
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
import { getPrivateUser, getUser } from './utils'
|
||||||
import { createNotification } from './create-notification'
|
import { createNotification } from './create-notification'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { parseMentions, richTextToString } from '../../common/util/parse'
|
import { parseMentions, richTextToString } from '../../common/util/parse'
|
||||||
import { JSONContent } from '@tiptap/core'
|
import { JSONContent } from '@tiptap/core'
|
||||||
|
import { User } from 'common/user'
|
||||||
|
import { sendCreatorGuideEmail } from './emails'
|
||||||
|
|
||||||
export const onCreateContract = functions.firestore
|
export const onCreateContract = functions
|
||||||
.document('contracts/{contractId}')
|
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||||
|
.firestore.document('contracts/{contractId}')
|
||||||
.onCreate(async (snapshot, context) => {
|
.onCreate(async (snapshot, context) => {
|
||||||
const contract = snapshot.data() as Contract
|
const contract = snapshot.data() as Contract
|
||||||
const { eventId } = context
|
const { eventId } = context
|
||||||
|
@ -26,4 +31,23 @@ export const onCreateContract = functions.firestore
|
||||||
richTextToString(desc),
|
richTextToString(desc),
|
||||||
{ contract, recipients: mentioned }
|
{ contract, recipients: mentioned }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await sendGuideEmail(contractCreator)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
const sendGuideEmail = async (contractCreator: User) => {
|
||||||
|
const query = await firestore
|
||||||
|
.collection(`contracts`)
|
||||||
|
.where('creatorId', '==', contractCreator.id)
|
||||||
|
.limit(2)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
if (query.size >= 2) return
|
||||||
|
|
||||||
|
const privateUser = await getPrivateUser(contractCreator.id)
|
||||||
|
if (!privateUser) return
|
||||||
|
|
||||||
|
await sendCreatorGuideEmail(contractCreator, privateUser)
|
||||||
|
}
|
||||||
|
|
|
@ -30,15 +30,7 @@ const bodySchema = z.object({
|
||||||
|
|
||||||
const binarySchema = z.object({
|
const binarySchema = z.object({
|
||||||
outcome: z.enum(['YES', 'NO']),
|
outcome: z.enum(['YES', 'NO']),
|
||||||
limitProb: z
|
limitProb: z.number().gte(0.001).lte(0.999).optional(),
|
||||||
.number()
|
|
||||||
.gte(0.001)
|
|
||||||
.lte(0.999)
|
|
||||||
.refine(
|
|
||||||
(p) => Math.round(p * 100) === p * 100,
|
|
||||||
'limitProb must be in increments of 0.01 (i.e. whole percentage points)'
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const freeResponseSchema = z.object({
|
const freeResponseSchema = z.object({
|
||||||
|
@ -89,7 +81,22 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
(outcomeType == 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') &&
|
(outcomeType == 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') &&
|
||||||
mechanism == 'cpmm-1'
|
mechanism == 'cpmm-1'
|
||||||
) {
|
) {
|
||||||
const { outcome, limitProb } = validate(binarySchema, req.body)
|
// eslint-disable-next-line prefer-const
|
||||||
|
let { outcome, limitProb } = validate(binarySchema, req.body)
|
||||||
|
|
||||||
|
if (limitProb !== undefined && outcomeType === 'BINARY') {
|
||||||
|
const isRounded = floatingEqual(
|
||||||
|
Math.round(limitProb * 100),
|
||||||
|
limitProb * 100
|
||||||
|
)
|
||||||
|
if (!isRounded)
|
||||||
|
throw new APIError(
|
||||||
|
400,
|
||||||
|
'limitProb must be in increments of 0.01 (i.e. whole percentage points)'
|
||||||
|
)
|
||||||
|
|
||||||
|
limitProb = Math.round(limitProb * 100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
const unfilledBetsSnap = await trans.get(
|
const unfilledBetsSnap = await trans.get(
|
||||||
getUnfilledBetsQuery(contractDoc)
|
getUnfilledBetsQuery(contractDoc)
|
||||||
|
|
31
functions/src/scripts/backfill-comment-types.ts
Normal file
31
functions/src/scripts/backfill-comment-types.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Comment types were introduced in August 2022.
|
||||||
|
|
||||||
|
import { initAdmin } from './script-init'
|
||||||
|
import { log, writeAsync } from '../utils'
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
const app = initAdmin()
|
||||||
|
const firestore = app.firestore()
|
||||||
|
const commentsRef = firestore.collectionGroup('comments')
|
||||||
|
commentsRef.get().then(async (commentsSnaps) => {
|
||||||
|
log(`Loaded ${commentsSnaps.size} comments.`)
|
||||||
|
const needsFilling = commentsSnaps.docs.filter((ct) => {
|
||||||
|
return !('commentType' in ct.data())
|
||||||
|
})
|
||||||
|
log(`Found ${needsFilling.length} comments to update.`)
|
||||||
|
const updates = needsFilling.map((d) => {
|
||||||
|
const comment = d.data()
|
||||||
|
const fields: { [k: string]: unknown } = {}
|
||||||
|
if (comment.contractId != null && comment.groupId == null) {
|
||||||
|
fields.commentType = 'contract'
|
||||||
|
} else if (comment.groupId != null && comment.contractId == null) {
|
||||||
|
fields.commentType = 'group'
|
||||||
|
} else {
|
||||||
|
log(`Invalid comment ${comment}; not touching it.`)
|
||||||
|
}
|
||||||
|
return { doc: d.ref, fields, info: comment }
|
||||||
|
})
|
||||||
|
await writeAsync(firestore, updates)
|
||||||
|
log(`Updated all comments.`)
|
||||||
|
})
|
||||||
|
}
|
70
functions/src/scripts/denormalize-comment-contract-data.ts
Normal file
70
functions/src/scripts/denormalize-comment-contract-data.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// Filling in the contract-based fields on comments.
|
||||||
|
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { initAdmin } from './script-init'
|
||||||
|
import {
|
||||||
|
DocumentCorrespondence,
|
||||||
|
findDiffs,
|
||||||
|
describeDiff,
|
||||||
|
applyDiff,
|
||||||
|
} from './denormalize'
|
||||||
|
import { DocumentSnapshot, Transaction } from 'firebase-admin/firestore'
|
||||||
|
|
||||||
|
initAdmin()
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
async function getContractsById(transaction: Transaction) {
|
||||||
|
const contracts = await transaction.get(firestore.collection('contracts'))
|
||||||
|
const results = Object.fromEntries(contracts.docs.map((doc) => [doc.id, doc]))
|
||||||
|
console.log(`Found ${contracts.size} contracts.`)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCommentsByContractId(transaction: Transaction) {
|
||||||
|
const comments = await transaction.get(
|
||||||
|
firestore.collectionGroup('comments').where('contractId', '!=', null)
|
||||||
|
)
|
||||||
|
const results = new Map<string, DocumentSnapshot[]>()
|
||||||
|
comments.forEach((doc) => {
|
||||||
|
const contractId = doc.get('contractId')
|
||||||
|
const contractComments = results.get(contractId) || []
|
||||||
|
contractComments.push(doc)
|
||||||
|
results.set(contractId, contractComments)
|
||||||
|
})
|
||||||
|
console.log(`Found ${comments.size} comments on ${results.size} contracts.`)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
async function denormalize() {
|
||||||
|
let hasMore = true
|
||||||
|
while (hasMore) {
|
||||||
|
hasMore = await admin.firestore().runTransaction(async (transaction) => {
|
||||||
|
const [contractsById, commentsByContractId] = await Promise.all([
|
||||||
|
getContractsById(transaction),
|
||||||
|
getCommentsByContractId(transaction),
|
||||||
|
])
|
||||||
|
const mapping = Object.entries(contractsById).map(
|
||||||
|
([id, doc]): DocumentCorrespondence => {
|
||||||
|
return [doc, commentsByContractId.get(id) || []]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const slugDiffs = findDiffs(mapping, 'slug', 'contractSlug')
|
||||||
|
const qDiffs = findDiffs(mapping, 'question', 'contractQuestion')
|
||||||
|
console.log(`Found ${slugDiffs.length} comments with mismatched slugs.`)
|
||||||
|
console.log(`Found ${qDiffs.length} comments with mismatched questions.`)
|
||||||
|
const diffs = slugDiffs.concat(qDiffs)
|
||||||
|
diffs.slice(0, 500).forEach((d) => {
|
||||||
|
console.log(describeDiff(d))
|
||||||
|
applyDiff(transaction, d)
|
||||||
|
})
|
||||||
|
if (diffs.length > 500) {
|
||||||
|
console.log(`Applying first 500 because of Firestore limit...`)
|
||||||
|
}
|
||||||
|
return diffs.length > 500
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
denormalize().catch((e) => console.error(e))
|
||||||
|
}
|
|
@ -1,27 +1,35 @@
|
||||||
import * as mailgun from 'mailgun-js'
|
import * as mailgun from 'mailgun-js'
|
||||||
|
import { tryOrLogError } from './utils'
|
||||||
|
|
||||||
const initMailgun = () => {
|
const initMailgun = () => {
|
||||||
const apiKey = process.env.MAILGUN_KEY as string
|
const apiKey = process.env.MAILGUN_KEY as string
|
||||||
return mailgun({ apiKey, domain: 'mg.manifold.markets' })
|
return mailgun({ apiKey, domain: 'mg.manifold.markets' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendTextEmail = (to: string, subject: string, text: string) => {
|
export const sendTextEmail = async (
|
||||||
|
to: string,
|
||||||
|
subject: string,
|
||||||
|
text: string,
|
||||||
|
options?: Partial<mailgun.messages.SendData>
|
||||||
|
) => {
|
||||||
const data: mailgun.messages.SendData = {
|
const data: mailgun.messages.SendData = {
|
||||||
from: 'Manifold Markets <info@manifold.markets>',
|
...options,
|
||||||
|
from: options?.from ?? 'Manifold Markets <info@manifold.markets>',
|
||||||
to,
|
to,
|
||||||
subject,
|
subject,
|
||||||
text,
|
text,
|
||||||
// Don't rewrite urls in plaintext emails
|
// Don't rewrite urls in plaintext emails
|
||||||
'o:tracking-clicks': 'htmlonly',
|
'o:tracking-clicks': 'htmlonly',
|
||||||
}
|
}
|
||||||
const mg = initMailgun()
|
const mg = initMailgun().messages()
|
||||||
return mg.messages().send(data, (error) => {
|
const result = await tryOrLogError(mg.send(data))
|
||||||
if (error) console.log('Error sending email', error)
|
if (result != null) {
|
||||||
else console.log('Sent text email', to, subject)
|
console.log('Sent text email', to, subject)
|
||||||
})
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendTemplateEmail = (
|
export const sendTemplateEmail = async (
|
||||||
to: string,
|
to: string,
|
||||||
subject: string,
|
subject: string,
|
||||||
templateId: string,
|
templateId: string,
|
||||||
|
@ -38,10 +46,10 @@ export const sendTemplateEmail = (
|
||||||
'o:tag': templateId,
|
'o:tag': templateId,
|
||||||
'o:tracking': true,
|
'o:tracking': true,
|
||||||
}
|
}
|
||||||
const mg = initMailgun()
|
const mg = initMailgun().messages()
|
||||||
|
const result = await tryOrLogError(mg.send(data))
|
||||||
return mg.messages().send(data, (error) => {
|
if (result != null) {
|
||||||
if (error) console.log('Error sending email', error)
|
console.log('Sent template email', templateId, to, subject)
|
||||||
else console.log('Sent template email', templateId, to, subject)
|
}
|
||||||
})
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { cancelbet } from './cancel-bet'
|
||||||
import { sellbet } from './sell-bet'
|
import { sellbet } from './sell-bet'
|
||||||
import { sellshares } from './sell-shares'
|
import { sellshares } from './sell-shares'
|
||||||
import { claimmanalink } from './claim-manalink'
|
import { claimmanalink } from './claim-manalink'
|
||||||
import { createmarket } from './create-contract'
|
import { createmarket } from './create-market'
|
||||||
import { addliquidity } from './add-liquidity'
|
import { addliquidity } from './add-liquidity'
|
||||||
import { withdrawliquidity } from './withdraw-liquidity'
|
import { withdrawliquidity } from './withdraw-liquidity'
|
||||||
import { creategroup } from './create-group'
|
import { creategroup } from './create-group'
|
||||||
|
|
|
@ -42,6 +42,15 @@ export const writeAsync = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const tryOrLogError = async <T>(task: Promise<T>) => {
|
||||||
|
try {
|
||||||
|
return await task
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const isProd = () => {
|
export const isProd = () => {
|
||||||
return admin.instanceId().app.options.projectId === 'mantic-markets'
|
return admin.instanceId().app.options.projectId === 'mantic-markets'
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ module.exports = {
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:react-hooks/recommended',
|
'plugin:react-hooks/recommended',
|
||||||
'plugin:@next/next/recommended',
|
'plugin:@next/next/recommended',
|
||||||
|
'prettier',
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-empty-function': 'off',
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Router from 'next/router'
|
import Router from 'next/router'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { MouseEvent, 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'
|
||||||
|
|
||||||
export function Avatar(props: {
|
export function Avatar(props: {
|
||||||
|
@ -12,6 +12,7 @@ export function Avatar(props: {
|
||||||
}) {
|
}) {
|
||||||
const { username, noLink, size, className } = props
|
const { username, noLink, size, className } = props
|
||||||
const [avatarUrl, setAvatarUrl] = useState(props.avatarUrl)
|
const [avatarUrl, setAvatarUrl] = useState(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 onClick =
|
const onClick =
|
||||||
|
|
|
@ -448,8 +448,6 @@ function LimitOrderPanel(props: {
|
||||||
const yesAmount = shares * (yesLimitProb ?? 1)
|
const yesAmount = shares * (yesLimitProb ?? 1)
|
||||||
const noAmount = shares * (1 - (noLimitProb ?? 0))
|
const noAmount = shares * (1 - (noLimitProb ?? 0))
|
||||||
|
|
||||||
const profitIfBothFilled = shares - (yesAmount + noAmount)
|
|
||||||
|
|
||||||
function onBetChange(newAmount: number | undefined) {
|
function onBetChange(newAmount: number | undefined) {
|
||||||
setWasSubmitted(false)
|
setWasSubmitted(false)
|
||||||
setBetAmount(newAmount)
|
setBetAmount(newAmount)
|
||||||
|
@ -559,6 +557,8 @@ function LimitOrderPanel(props: {
|
||||||
)
|
)
|
||||||
const noReturnPercent = formatPercent(noReturn)
|
const noReturnPercent = formatPercent(noReturn)
|
||||||
|
|
||||||
|
const profitIfBothFilled = shares - (yesAmount + noAmount) - yesFees - noFees
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={hidden ? 'hidden' : ''}>
|
<Col className={hidden ? 'hidden' : ''}>
|
||||||
<Row className="mt-1 items-center gap-4">
|
<Row className="mt-1 items-center gap-4">
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { User } from 'common/user'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
import { Button } from '../button'
|
import { Button } from '../button'
|
||||||
import { createChallenge, getChallengeUrl } from 'web/lib/firebase/challenges'
|
import { createChallenge, getChallengeUrl } from 'web/lib/firebase/challenges'
|
||||||
import { BinaryContract } from 'common/contract'
|
import { BinaryContract, MAX_QUESTION_LENGTH } from 'common/contract'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { NoLabel, YesLabel } from '../outcome-label'
|
import { NoLabel, YesLabel } from '../outcome-label'
|
||||||
|
@ -19,24 +19,32 @@ import { QRCode } from '../qr-code'
|
||||||
import { copyToClipboard } from 'web/lib/util/copy'
|
import { copyToClipboard } from 'web/lib/util/copy'
|
||||||
import { AmountInput } from '../amount-input'
|
import { AmountInput } from '../amount-input'
|
||||||
import { getProbability } from 'common/calculate'
|
import { getProbability } from 'common/calculate'
|
||||||
|
import { createMarket } from 'web/lib/firebase/api'
|
||||||
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
|
import { FIXED_ANTE } from 'common/antes'
|
||||||
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
import { useTextEditor } from 'web/components/editor'
|
||||||
|
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
|
||||||
type challengeInfo = {
|
type challengeInfo = {
|
||||||
amount: number
|
amount: number
|
||||||
expiresTime: number | null
|
expiresTime: number | null
|
||||||
message: string
|
|
||||||
outcome: 'YES' | 'NO' | number
|
outcome: 'YES' | 'NO' | number
|
||||||
acceptorAmount: number
|
acceptorAmount: number
|
||||||
|
question: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CreateChallengeModal(props: {
|
export function CreateChallengeModal(props: {
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
contract: BinaryContract
|
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void
|
||||||
|
contract?: BinaryContract
|
||||||
}) {
|
}) {
|
||||||
const { user, contract, isOpen, setOpen } = props
|
const { user, contract, isOpen, setOpen } = props
|
||||||
const [challengeSlug, setChallengeSlug] = useState('')
|
const [challengeSlug, setChallengeSlug] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const { editor } = useTextEditor({ placeholder: '' })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isOpen} setOpen={setOpen}>
|
<Modal open={isOpen} setOpen={setOpen}>
|
||||||
|
@ -46,24 +54,42 @@ export function CreateChallengeModal(props: {
|
||||||
<CreateChallengeForm
|
<CreateChallengeForm
|
||||||
user={user}
|
user={user}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
loading={loading}
|
||||||
onCreate={async (newChallenge) => {
|
onCreate={async (newChallenge) => {
|
||||||
const challenge = await createChallenge({
|
setLoading(true)
|
||||||
creator: user,
|
try {
|
||||||
creatorAmount: newChallenge.amount,
|
const challengeContract = contract
|
||||||
expiresTime: newChallenge.expiresTime,
|
? contract
|
||||||
message: newChallenge.message,
|
: await createMarket(
|
||||||
acceptorAmount: newChallenge.acceptorAmount,
|
removeUndefinedProps({
|
||||||
outcome: newChallenge.outcome,
|
question: newChallenge.question,
|
||||||
contract: contract,
|
outcomeType: 'BINARY',
|
||||||
})
|
initialProb: 50,
|
||||||
if (challenge) {
|
description: editor?.getJSON(),
|
||||||
setChallengeSlug(getChallengeUrl(challenge))
|
ante: FIXED_ANTE,
|
||||||
track('challenge created', {
|
closeTime: dayjs().add(30, 'day').valueOf(),
|
||||||
creator: user.username,
|
})
|
||||||
amount: newChallenge.amount,
|
)
|
||||||
contractId: contract.id,
|
const challenge = await createChallenge({
|
||||||
|
creator: user,
|
||||||
|
creatorAmount: newChallenge.amount,
|
||||||
|
expiresTime: newChallenge.expiresTime,
|
||||||
|
acceptorAmount: newChallenge.acceptorAmount,
|
||||||
|
outcome: newChallenge.outcome,
|
||||||
|
contract: challengeContract as BinaryContract,
|
||||||
})
|
})
|
||||||
|
if (challenge) {
|
||||||
|
setChallengeSlug(getChallengeUrl(challenge))
|
||||||
|
track('challenge created', {
|
||||||
|
creator: user.username,
|
||||||
|
amount: newChallenge.amount,
|
||||||
|
contractId: challengeContract.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("couldn't create market/challenge:", e)
|
||||||
}
|
}
|
||||||
|
setLoading(false)
|
||||||
}}
|
}}
|
||||||
challengeSlug={challengeSlug}
|
challengeSlug={challengeSlug}
|
||||||
/>
|
/>
|
||||||
|
@ -75,25 +101,24 @@ export function CreateChallengeModal(props: {
|
||||||
|
|
||||||
function CreateChallengeForm(props: {
|
function CreateChallengeForm(props: {
|
||||||
user: User
|
user: User
|
||||||
contract: BinaryContract
|
|
||||||
onCreate: (m: challengeInfo) => Promise<void>
|
onCreate: (m: challengeInfo) => Promise<void>
|
||||||
challengeSlug: string
|
challengeSlug: string
|
||||||
|
loading: boolean
|
||||||
|
contract?: BinaryContract
|
||||||
}) {
|
}) {
|
||||||
const { user, onCreate, contract, challengeSlug } = props
|
const { user, onCreate, contract, challengeSlug, loading } = props
|
||||||
const [isCreating, setIsCreating] = useState(false)
|
const [isCreating, setIsCreating] = useState(false)
|
||||||
const [finishedCreating, setFinishedCreating] = useState(false)
|
const [finishedCreating, setFinishedCreating] = useState(false)
|
||||||
const [error, setError] = useState<string>('')
|
const [error, setError] = useState<string>('')
|
||||||
const [editingAcceptorAmount, setEditingAcceptorAmount] = useState(false)
|
const [editingAcceptorAmount, setEditingAcceptorAmount] = useState(false)
|
||||||
const defaultExpire = 'week'
|
const defaultExpire = 'week'
|
||||||
|
|
||||||
const defaultMessage = `${user.name} is challenging you to a bet! Do you think ${contract.question}`
|
|
||||||
|
|
||||||
const [challengeInfo, setChallengeInfo] = useState<challengeInfo>({
|
const [challengeInfo, setChallengeInfo] = useState<challengeInfo>({
|
||||||
expiresTime: dayjs().add(2, defaultExpire).valueOf(),
|
expiresTime: dayjs().add(2, defaultExpire).valueOf(),
|
||||||
outcome: 'YES',
|
outcome: 'YES',
|
||||||
amount: 100,
|
amount: 100,
|
||||||
acceptorAmount: 100,
|
acceptorAmount: 100,
|
||||||
message: defaultMessage,
|
question: contract ? contract.question : '',
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError('')
|
setError('')
|
||||||
|
@ -106,7 +131,15 @@ function CreateChallengeForm(props: {
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (user.balance < challengeInfo.amount) {
|
if (user.balance < challengeInfo.amount) {
|
||||||
setError('You do not have enough mana to create this challenge')
|
setError("You don't have enough mana to create this challenge")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!contract && user.balance < FIXED_ANTE + challengeInfo.amount) {
|
||||||
|
setError(
|
||||||
|
`You don't have enough mana to create this challenge and market. You need ${formatMoney(
|
||||||
|
FIXED_ANTE + challengeInfo.amount
|
||||||
|
)}`
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setIsCreating(true)
|
setIsCreating(true)
|
||||||
|
@ -118,7 +151,23 @@ function CreateChallengeForm(props: {
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
Challenge a friend to bet on{' '}
|
Challenge a friend to bet on{' '}
|
||||||
<span className="underline">{contract.question}</span>
|
{contract ? (
|
||||||
|
<span className="underline">{contract.question}</span>
|
||||||
|
) : (
|
||||||
|
<Textarea
|
||||||
|
placeholder="e.g. Will a Democrat be the next president?"
|
||||||
|
className="input input-bordered mt-1 w-full resize-none"
|
||||||
|
autoFocus={true}
|
||||||
|
maxLength={MAX_QUESTION_LENGTH}
|
||||||
|
value={challengeInfo.question}
|
||||||
|
onChange={(e) =>
|
||||||
|
setChallengeInfo({
|
||||||
|
...challengeInfo,
|
||||||
|
question: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 flex flex-col flex-wrap justify-center gap-x-5 gap-y-2">
|
<div className="mt-2 flex flex-col flex-wrap justify-center gap-x-5 gap-y-2">
|
||||||
|
@ -187,22 +236,23 @@ function CreateChallengeForm(props: {
|
||||||
{challengeInfo.outcome === 'YES' ? <NoLabel /> : <YesLabel />}
|
{challengeInfo.outcome === 'YES' ? <NoLabel /> : <YesLabel />}
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{contract && (
|
||||||
size="2xs"
|
<Button
|
||||||
color="gray"
|
size="2xs"
|
||||||
onClick={() => {
|
color="gray"
|
||||||
setEditingAcceptorAmount(true)
|
onClick={() => {
|
||||||
|
setEditingAcceptorAmount(true)
|
||||||
const p = getProbability(contract)
|
|
||||||
const prob = challengeInfo.outcome === 'YES' ? p : 1 - p
|
|
||||||
const { amount } = challengeInfo
|
|
||||||
const acceptorAmount = Math.round(amount / prob - amount)
|
|
||||||
setChallengeInfo({ ...challengeInfo, acceptorAmount })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Use market odds
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
|
const p = getProbability(contract)
|
||||||
|
const prob = challengeInfo.outcome === 'YES' ? p : 1 - p
|
||||||
|
const { amount } = challengeInfo
|
||||||
|
const acceptorAmount = Math.round(amount / prob - amount)
|
||||||
|
setChallengeInfo({ ...challengeInfo, acceptorAmount })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Use market odds
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
If the challenge is accepted, whoever is right will earn{' '}
|
If the challenge is accepted, whoever is right will earn{' '}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
|
@ -210,7 +260,18 @@ function CreateChallengeForm(props: {
|
||||||
challengeInfo.acceptorAmount + challengeInfo.amount || 0
|
challengeInfo.acceptorAmount + challengeInfo.amount || 0
|
||||||
)}
|
)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
in total.
|
in total.{' '}
|
||||||
|
<span>
|
||||||
|
{!contract && (
|
||||||
|
<span>
|
||||||
|
Because there's no market yet, you'll be charged
|
||||||
|
<span className={'mx-1 font-semibold'}>
|
||||||
|
{formatMoney(FIXED_ANTE)}
|
||||||
|
</span>
|
||||||
|
to create it.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Row className="mt-8 items-center">
|
<Row className="mt-8 items-center">
|
||||||
|
@ -218,10 +279,8 @@ function CreateChallengeForm(props: {
|
||||||
type="submit"
|
type="submit"
|
||||||
color={'gradient'}
|
color={'gradient'}
|
||||||
size="xl"
|
size="xl"
|
||||||
className={clsx(
|
disabled={isCreating || challengeInfo.question === ''}
|
||||||
'whitespace-nowrap drop-shadow-md',
|
className={clsx('whitespace-nowrap drop-shadow-md')}
|
||||||
isCreating ? 'disabled' : ''
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
Create challenge bet
|
Create challenge bet
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -229,7 +288,12 @@ function CreateChallengeForm(props: {
|
||||||
<Row className={'text-error'}>{error} </Row>
|
<Row className={'text-error'}>{error} </Row>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
{finishedCreating && (
|
{loading && (
|
||||||
|
<Col className={'h-56 w-full items-center justify-center'}>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
{finishedCreating && !loading && (
|
||||||
<>
|
<>
|
||||||
<Title className="!my-0" text="Challenge Created!" />
|
<Title className="!my-0" text="Challenge Created!" />
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Dictionary, keyBy, uniq } from 'lodash'
|
|
||||||
|
|
||||||
import { Comment } from 'common/comment'
|
import { Comment, ContractComment } from 'common/comment'
|
||||||
import { Contract } from 'common/contract'
|
import { groupConsecutive } from 'common/util/array'
|
||||||
import { filterDefined, groupConsecutive } from 'common/util/array'
|
|
||||||
import { contractPath } from 'web/lib/firebase/contracts'
|
|
||||||
import { getUsersComments } from 'web/lib/firebase/comments'
|
import { getUsersComments } from 'web/lib/firebase/comments'
|
||||||
import { getContractFromId } from 'web/lib/firebase/contracts'
|
|
||||||
import { SiteLink } from './site-link'
|
import { SiteLink } from './site-link'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { Avatar } from './avatar'
|
import { Avatar } from './avatar'
|
||||||
|
@ -20,12 +16,15 @@ import { LoadingIndicator } from './loading-indicator'
|
||||||
|
|
||||||
const COMMENTS_PER_PAGE = 50
|
const COMMENTS_PER_PAGE = 50
|
||||||
|
|
||||||
type ContractComment = Comment & { contractId: string }
|
function contractPath(slug: string) {
|
||||||
|
// by convention this includes the contract creator username, but we don't
|
||||||
|
// have that handy, so we just put /market/
|
||||||
|
return `/market/${slug}`
|
||||||
|
}
|
||||||
|
|
||||||
export function UserCommentsList(props: { user: User }) {
|
export function UserCommentsList(props: { user: User }) {
|
||||||
const { user } = props
|
const { user } = props
|
||||||
const [comments, setComments] = useState<ContractComment[] | undefined>()
|
const [comments, setComments] = useState<ContractComment[] | undefined>()
|
||||||
const [contracts, setContracts] = useState<Dictionary<Contract> | undefined>()
|
|
||||||
const [page, setPage] = useState(0)
|
const [page, setPage] = useState(0)
|
||||||
const start = page * COMMENTS_PER_PAGE
|
const start = page * COMMENTS_PER_PAGE
|
||||||
const end = start + COMMENTS_PER_PAGE
|
const end = start + COMMENTS_PER_PAGE
|
||||||
|
@ -33,38 +32,29 @@ export function UserCommentsList(props: { user: User }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUsersComments(user.id).then((cs) => {
|
getUsersComments(user.id).then((cs) => {
|
||||||
// we don't show comments in groups here atm, just comments on contracts
|
// we don't show comments in groups here atm, just comments on contracts
|
||||||
setComments(cs.filter((c) => c.contractId) as ContractComment[])
|
setComments(
|
||||||
|
cs.filter((c) => c.commentType == 'contract') as ContractComment[]
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}, [user.id])
|
}, [user.id])
|
||||||
|
|
||||||
useEffect(() => {
|
if (comments == null) {
|
||||||
if (comments) {
|
|
||||||
const contractIds = uniq(comments.map((c) => c.contractId))
|
|
||||||
Promise.all(contractIds.map(getContractFromId)).then((contracts) => {
|
|
||||||
setContracts(keyBy(filterDefined(contracts), 'id'))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [comments])
|
|
||||||
|
|
||||||
if (comments == null || contracts == null) {
|
|
||||||
return <LoadingIndicator />
|
return <LoadingIndicator />
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageComments = groupConsecutive(
|
const pageComments = groupConsecutive(comments.slice(start, end), (c) => {
|
||||||
comments.slice(start, end),
|
return { question: c.contractQuestion, slug: c.contractSlug }
|
||||||
(c) => c.contractId
|
})
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<Col className={'bg-white'}>
|
<Col className={'bg-white'}>
|
||||||
{pageComments.map(({ key, items }, i) => {
|
{pageComments.map(({ key, items }, i) => {
|
||||||
const contract = contracts[key]
|
|
||||||
return (
|
return (
|
||||||
<div key={start + i} className="border-b p-5">
|
<div key={start + i} className="border-b p-5">
|
||||||
<SiteLink
|
<SiteLink
|
||||||
className="mb-2 block pb-2 font-medium text-indigo-700"
|
className="mb-2 block pb-2 font-medium text-indigo-700"
|
||||||
href={contractPath(contract)}
|
href={contractPath(key.slug)}
|
||||||
>
|
>
|
||||||
{contract.question}
|
{key.question}
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
<Col className="gap-6">
|
<Col className="gap-6">
|
||||||
{items.map((comment) => (
|
{items.map((comment) => (
|
||||||
|
|
|
@ -83,7 +83,7 @@ export function ContractSearch(props: {
|
||||||
highlightOptions?: ContractHighlightOptions
|
highlightOptions?: ContractHighlightOptions
|
||||||
onContractClick?: (contract: Contract) => void
|
onContractClick?: (contract: Contract) => void
|
||||||
hideOrderSelector?: boolean
|
hideOrderSelector?: boolean
|
||||||
gridClassName?: string
|
overrideGridClassName?: string
|
||||||
cardHideOptions?: {
|
cardHideOptions?: {
|
||||||
hideGroupLink?: boolean
|
hideGroupLink?: boolean
|
||||||
hideQuickBet?: boolean
|
hideQuickBet?: boolean
|
||||||
|
@ -91,6 +91,7 @@ export function ContractSearch(props: {
|
||||||
headerClassName?: string
|
headerClassName?: string
|
||||||
useQuerySortLocalStorage?: boolean
|
useQuerySortLocalStorage?: boolean
|
||||||
useQuerySortUrlParams?: boolean
|
useQuerySortUrlParams?: boolean
|
||||||
|
isWholePage?: boolean
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
|
@ -98,13 +99,14 @@ export function ContractSearch(props: {
|
||||||
defaultFilter,
|
defaultFilter,
|
||||||
additionalFilter,
|
additionalFilter,
|
||||||
onContractClick,
|
onContractClick,
|
||||||
gridClassName,
|
overrideGridClassName,
|
||||||
hideOrderSelector,
|
hideOrderSelector,
|
||||||
cardHideOptions,
|
cardHideOptions,
|
||||||
highlightOptions,
|
highlightOptions,
|
||||||
headerClassName,
|
headerClassName,
|
||||||
useQuerySortLocalStorage,
|
useQuerySortLocalStorage,
|
||||||
useQuerySortUrlParams,
|
useQuerySortUrlParams,
|
||||||
|
isWholePage,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const [numPages, setNumPages] = useState(1)
|
const [numPages, setNumPages] = useState(1)
|
||||||
|
@ -139,7 +141,7 @@ export function ContractSearch(props: {
|
||||||
setNumPages(results.nbPages)
|
setNumPages(results.nbPages)
|
||||||
if (freshQuery) {
|
if (freshQuery) {
|
||||||
setPages([newPage])
|
setPages([newPage])
|
||||||
window.scrollTo(0, 0)
|
if (isWholePage) window.scrollTo(0, 0)
|
||||||
} else {
|
} else {
|
||||||
setPages((pages) => [...pages, newPage])
|
setPages((pages) => [...pages, newPage])
|
||||||
}
|
}
|
||||||
|
@ -181,7 +183,7 @@ export function ContractSearch(props: {
|
||||||
loadMore={performQuery}
|
loadMore={performQuery}
|
||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
onContractClick={onContractClick}
|
onContractClick={onContractClick}
|
||||||
gridClassName={gridClassName}
|
overrideGridClassName={overrideGridClassName}
|
||||||
highlightOptions={highlightOptions}
|
highlightOptions={highlightOptions}
|
||||||
cardHideOptions={cardHideOptions}
|
cardHideOptions={cardHideOptions}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -122,19 +122,6 @@ export function ContractCard(props: {
|
||||||
) : (
|
) : (
|
||||||
<FreeResponseTopAnswer contract={contract} truncate="long" />
|
<FreeResponseTopAnswer contract={contract} truncate="long" />
|
||||||
))}
|
))}
|
||||||
<Row className={'absolute bottom-3 gap-2 md:gap-0'}>
|
|
||||||
<AvatarDetails
|
|
||||||
contract={contract}
|
|
||||||
short={true}
|
|
||||||
className={'block md:hidden'}
|
|
||||||
/>
|
|
||||||
<MiscDetails
|
|
||||||
contract={contract}
|
|
||||||
showHotVolume={showHotVolume}
|
|
||||||
showTime={showTime}
|
|
||||||
hideGroupLink={hideGroupLink}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
</Col>
|
||||||
{showQuickBet ? (
|
{showQuickBet ? (
|
||||||
<QuickBet contract={contract} user={user} />
|
<QuickBet contract={contract} user={user} />
|
||||||
|
@ -172,6 +159,24 @@ export function ContractCard(props: {
|
||||||
<ProbBar contract={contract} />
|
<ProbBar contract={contract} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<Row
|
||||||
|
className={clsx(
|
||||||
|
'absolute bottom-3 gap-2 truncate px-5 md:gap-0',
|
||||||
|
showQuickBet ? 'w-[85%]' : 'w-full'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AvatarDetails
|
||||||
|
contract={contract}
|
||||||
|
short={true}
|
||||||
|
className={'block md:hidden'}
|
||||||
|
/>
|
||||||
|
<MiscDetails
|
||||||
|
contract={contract}
|
||||||
|
showHotVolume={showHotVolume}
|
||||||
|
showTime={showTime}
|
||||||
|
hideGroupLink={hideGroupLink}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,13 +58,13 @@ export function MiscDetails(props: {
|
||||||
const isNew = createdTime > Date.now() - DAY_MS && !isResolved
|
const isNew = createdTime > Date.now() - DAY_MS && !isResolved
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="items-center gap-3 text-sm text-gray-400">
|
<Row className="items-center gap-3 truncate text-sm text-gray-400">
|
||||||
{showHotVolume ? (
|
{showHotVolume ? (
|
||||||
<Row className="gap-0.5">
|
<Row className="gap-0.5">
|
||||||
<TrendingUpIcon className="h-5 w-5" /> {formatMoney(volume24Hours)}
|
<TrendingUpIcon className="h-5 w-5" /> {formatMoney(volume24Hours)}
|
||||||
</Row>
|
</Row>
|
||||||
) : showTime === 'close-date' ? (
|
) : showTime === 'close-date' ? (
|
||||||
<Row className="gap-0.5">
|
<Row className="gap-0.5 whitespace-nowrap">
|
||||||
<ClockIcon className="h-5 w-5" />
|
<ClockIcon className="h-5 w-5" />
|
||||||
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
|
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
|
||||||
{fromNow(closeTime || 0)}
|
{fromNow(closeTime || 0)}
|
||||||
|
|
|
@ -66,17 +66,17 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Payout</td>
|
<td>Payout</td>
|
||||||
<td>
|
<td className="flex gap-1">
|
||||||
{mechanism === 'cpmm-1' ? (
|
{mechanism === 'cpmm-1' ? (
|
||||||
<>
|
<>
|
||||||
Fixed{' '}
|
Fixed{' '}
|
||||||
<InfoTooltip text="Each YES share is worth M$1 if YES wins." />
|
<InfoTooltip text="Each YES share is worth M$1 if YES wins." />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<>
|
||||||
Parimutuel{' '}
|
Parimutuel{' '}
|
||||||
<InfoTooltip text="Each share is a fraction of the pool. " />
|
<InfoTooltip text="Each share is a fraction of the pool. " />
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Comment } from 'common/comment'
|
import { ContractComment } from 'common/comment'
|
||||||
import { resolvedPayout } from 'common/calculate'
|
import { resolvedPayout } from 'common/calculate'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
|
@ -65,7 +65,7 @@ export function ContractLeaderboard(props: {
|
||||||
export function ContractTopTrades(props: {
|
export function ContractTopTrades(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: ContractComment[]
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
}) {
|
}) {
|
||||||
const { contract, bets, comments, tips } = props
|
const { contract, bets, comments, tips } = props
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { Comment } from 'web/lib/firebase/comments'
|
import { ContractComment } from 'common/comment'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { ContractActivity } from '../feed/contract-activity'
|
import { ContractActivity } from '../feed/contract-activity'
|
||||||
import { ContractBetsTable, BetsSummary } from '../bets-list'
|
import { ContractBetsTable, BetsSummary } from '../bets-list'
|
||||||
|
@ -15,7 +15,7 @@ export function ContractTabs(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: ContractComment[]
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
}) {
|
}) {
|
||||||
const { contract, user, bets, tips } = props
|
const { contract, user, bets, tips } = props
|
||||||
|
|
|
@ -20,7 +20,7 @@ export function ContractsGrid(props: {
|
||||||
loadMore?: () => void
|
loadMore?: () => void
|
||||||
showTime?: ShowTime
|
showTime?: ShowTime
|
||||||
onContractClick?: (contract: Contract) => void
|
onContractClick?: (contract: Contract) => void
|
||||||
gridClassName?: string
|
overrideGridClassName?: string
|
||||||
cardHideOptions?: {
|
cardHideOptions?: {
|
||||||
hideQuickBet?: boolean
|
hideQuickBet?: boolean
|
||||||
hideGroupLink?: boolean
|
hideGroupLink?: boolean
|
||||||
|
@ -32,7 +32,7 @@ export function ContractsGrid(props: {
|
||||||
showTime,
|
showTime,
|
||||||
loadMore,
|
loadMore,
|
||||||
onContractClick,
|
onContractClick,
|
||||||
gridClassName,
|
overrideGridClassName,
|
||||||
cardHideOptions,
|
cardHideOptions,
|
||||||
highlightOptions,
|
highlightOptions,
|
||||||
} = props
|
} = props
|
||||||
|
@ -66,8 +66,9 @@ export function ContractsGrid(props: {
|
||||||
<Col className="gap-8">
|
<Col className="gap-8">
|
||||||
<ul
|
<ul
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full columns-1 gap-4 space-y-4 md:columns-2',
|
overrideGridClassName
|
||||||
gridClassName
|
? overrideGridClassName
|
||||||
|
: 'grid w-full grid-cols-1 gap-4 md:grid-cols-2'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{contracts.map((contract) => (
|
{contracts.map((contract) => (
|
||||||
|
@ -80,10 +81,11 @@ export function ContractsGrid(props: {
|
||||||
}
|
}
|
||||||
hideQuickBet={hideQuickBet}
|
hideQuickBet={hideQuickBet}
|
||||||
hideGroupLink={hideGroupLink}
|
hideGroupLink={hideGroupLink}
|
||||||
className={clsx(
|
className={
|
||||||
'break-inside-avoid-column',
|
contractIds?.includes(contract.id)
|
||||||
contractIds?.includes(contract.id) && highlightClassName
|
? highlightClassName
|
||||||
)}
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -65,7 +65,9 @@ export function MarketModal(props: {
|
||||||
<ContractSearch
|
<ContractSearch
|
||||||
hideOrderSelector
|
hideOrderSelector
|
||||||
onContractClick={addContract}
|
onContractClick={addContract}
|
||||||
gridClassName="gap-3 space-y-3"
|
overrideGridClassName={
|
||||||
|
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
|
||||||
|
}
|
||||||
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
||||||
highlightOptions={{
|
highlightOptions={{
|
||||||
contractIds: contracts.map((c) => c.id),
|
contractIds: contracts.map((c) => c.id),
|
||||||
|
|
|
@ -51,6 +51,7 @@ export const MentionList = forwardRef((props: SuggestionProps<User>, ref) => {
|
||||||
selectedIndex === i ? 'bg-indigo-500 text-white' : 'text-gray-900'
|
selectedIndex === i ? 'bg-indigo-500 text-white' : 'text-gray-900'
|
||||||
)}
|
)}
|
||||||
onClick={() => submitUser(i)}
|
onClick={() => submitUser(i)}
|
||||||
|
key={user.id}
|
||||||
>
|
>
|
||||||
<Avatar avatarUrl={user.avatarUrl} size="xs" />
|
<Avatar avatarUrl={user.avatarUrl} size="xs" />
|
||||||
{user.username}
|
{user.username}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { uniq, sortBy } from 'lodash'
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { getOutcomeProbability } from 'common/calculate'
|
import { getOutcomeProbability } from 'common/calculate'
|
||||||
import { Comment } from 'common/comment'
|
import { ContractComment } from 'common/comment'
|
||||||
import { Contract, FreeResponseContract } from 'common/contract'
|
import { Contract, FreeResponseContract } from 'common/contract'
|
||||||
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'
|
||||||
|
@ -28,7 +28,7 @@ type BaseActivityItem = {
|
||||||
export type CommentInputItem = BaseActivityItem & {
|
export type CommentInputItem = BaseActivityItem & {
|
||||||
type: 'commentInput'
|
type: 'commentInput'
|
||||||
betsByCurrentUser: Bet[]
|
betsByCurrentUser: Bet[]
|
||||||
commentsByCurrentUser: Comment[]
|
commentsByCurrentUser: ContractComment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DescriptionItem = BaseActivityItem & {
|
export type DescriptionItem = BaseActivityItem & {
|
||||||
|
@ -50,8 +50,8 @@ export type BetItem = BaseActivityItem & {
|
||||||
|
|
||||||
export type CommentThreadItem = BaseActivityItem & {
|
export type CommentThreadItem = BaseActivityItem & {
|
||||||
type: 'commentThread'
|
type: 'commentThread'
|
||||||
parentComment: Comment
|
parentComment: ContractComment
|
||||||
comments: Comment[]
|
comments: ContractComment[]
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ export type AnswerGroupItem = BaseActivityItem & {
|
||||||
type: 'answergroup'
|
type: 'answergroup'
|
||||||
user: User | undefined | null
|
user: User | undefined | null
|
||||||
answer: Answer
|
answer: Answer
|
||||||
comments: Comment[]
|
comments: ContractComment[]
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ export type LiquidityItem = BaseActivityItem & {
|
||||||
function getAnswerAndCommentInputGroups(
|
function getAnswerAndCommentInputGroups(
|
||||||
contract: FreeResponseContract,
|
contract: FreeResponseContract,
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
comments: Comment[],
|
comments: ContractComment[],
|
||||||
tips: CommentTipMap,
|
tips: CommentTipMap,
|
||||||
user: User | undefined | null
|
user: User | undefined | null
|
||||||
) {
|
) {
|
||||||
|
@ -116,7 +116,7 @@ function getAnswerAndCommentInputGroups(
|
||||||
|
|
||||||
function getCommentThreads(
|
function getCommentThreads(
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
comments: Comment[],
|
comments: ContractComment[],
|
||||||
tips: CommentTipMap,
|
tips: CommentTipMap,
|
||||||
contract: Contract
|
contract: Contract
|
||||||
) {
|
) {
|
||||||
|
@ -135,7 +135,7 @@ function getCommentThreads(
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
function commentIsGeneralComment(comment: Comment, contract: Contract) {
|
function commentIsGeneralComment(comment: ContractComment, contract: Contract) {
|
||||||
return (
|
return (
|
||||||
comment.answerOutcome === undefined &&
|
comment.answerOutcome === undefined &&
|
||||||
(contract.outcomeType === 'FREE_RESPONSE'
|
(contract.outcomeType === 'FREE_RESPONSE'
|
||||||
|
@ -147,7 +147,7 @@ function commentIsGeneralComment(comment: Comment, contract: Contract) {
|
||||||
export function getSpecificContractActivityItems(
|
export function getSpecificContractActivityItems(
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
comments: Comment[],
|
comments: ContractComment[],
|
||||||
liquidityProvisions: LiquidityProvision[],
|
liquidityProvisions: LiquidityProvision[],
|
||||||
tips: CommentTipMap,
|
tips: CommentTipMap,
|
||||||
user: User | null | undefined,
|
user: User | null | undefined,
|
||||||
|
|
|
@ -1,167 +0,0 @@
|
||||||
import clsx from 'clsx'
|
|
||||||
import { PencilIcon } from '@heroicons/react/outline'
|
|
||||||
import { union, difference } from 'lodash'
|
|
||||||
|
|
||||||
import { Row } from '../layout/row'
|
|
||||||
import { CATEGORIES, category, CATEGORY_LIST } from '../../../common/categories'
|
|
||||||
import { Modal } from '../layout/modal'
|
|
||||||
import { Col } from '../layout/col'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { updateUser, User } from 'web/lib/firebase/users'
|
|
||||||
import { Checkbox } from '../checkbox'
|
|
||||||
import { track } from 'web/lib/service/analytics'
|
|
||||||
|
|
||||||
export function CategorySelector(props: {
|
|
||||||
category: string
|
|
||||||
setCategory: (category: string) => void
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const { className, category, setCategory } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row
|
|
||||||
className={clsx(
|
|
||||||
'carousel mr-2 items-center space-x-2 space-y-2 overflow-x-scroll pb-4 sm:flex-wrap',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
<CategoryButton
|
|
||||||
key="all"
|
|
||||||
category="All"
|
|
||||||
isFollowed={category === 'all'}
|
|
||||||
toggle={() => {
|
|
||||||
setCategory('all')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CategoryButton
|
|
||||||
key="following"
|
|
||||||
category="Following"
|
|
||||||
isFollowed={category === 'following'}
|
|
||||||
toggle={() => {
|
|
||||||
setCategory('following')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{CATEGORY_LIST.map((cat) => (
|
|
||||||
<CategoryButton
|
|
||||||
key={cat}
|
|
||||||
category={CATEGORIES[cat as category].split(' ')[0]}
|
|
||||||
isFollowed={cat === category}
|
|
||||||
toggle={() => {
|
|
||||||
setCategory(cat)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CategoryButton(props: {
|
|
||||||
category: string
|
|
||||||
isFollowed: boolean
|
|
||||||
toggle: () => void
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const { toggle, category, isFollowed, className } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
className,
|
|
||||||
'rounded-full border-2 px-4 py-1 shadow-md hover:bg-gray-200',
|
|
||||||
'cursor-pointer select-none',
|
|
||||||
isFollowed ? 'border-gray-300 bg-gray-300' : 'bg-white'
|
|
||||||
)}
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<span className="text-sm text-gray-500">{category}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EditCategoriesButton(props: {
|
|
||||||
user: User
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const { user, className } = props
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
className,
|
|
||||||
'btn btn-sm btn-ghost cursor-pointer gap-2 whitespace-nowrap text-sm normal-case text-gray-700'
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpen(true)
|
|
||||||
track('edit categories button')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PencilIcon className="inline h-4 w-4" />
|
|
||||||
Categories
|
|
||||||
<CategorySelectorModal
|
|
||||||
user={user}
|
|
||||||
isOpen={isOpen}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CategorySelectorModal(props: {
|
|
||||||
user: User
|
|
||||||
isOpen: boolean
|
|
||||||
setIsOpen: (isOpen: boolean) => void
|
|
||||||
}) {
|
|
||||||
const { user, isOpen, setIsOpen } = props
|
|
||||||
const followedCategories =
|
|
||||||
user?.followedCategories === undefined
|
|
||||||
? CATEGORY_LIST
|
|
||||||
: user.followedCategories
|
|
||||||
|
|
||||||
const selectAll =
|
|
||||||
user.followedCategories === undefined ||
|
|
||||||
followedCategories.length < CATEGORY_LIST.length
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal open={isOpen} setOpen={setIsOpen}>
|
|
||||||
<Col className="rounded bg-white p-6">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-outline mb-4 self-start normal-case"
|
|
||||||
onClick={() => {
|
|
||||||
if (selectAll) {
|
|
||||||
updateUser(user.id, {
|
|
||||||
followedCategories: CATEGORY_LIST,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
updateUser(user.id, {
|
|
||||||
followedCategories: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Select {selectAll ? 'all' : 'none'}
|
|
||||||
</button>
|
|
||||||
<Col className="grid w-full grid-cols-2 gap-4">
|
|
||||||
{CATEGORY_LIST.map((cat) => (
|
|
||||||
<Checkbox
|
|
||||||
className="col-span-1"
|
|
||||||
key={cat}
|
|
||||||
label={CATEGORIES[cat as category].split(' ')[0]}
|
|
||||||
checked={followedCategories.includes(cat)}
|
|
||||||
toggle={(checked) => {
|
|
||||||
updateUser(user.id, {
|
|
||||||
followedCategories: checked
|
|
||||||
? difference(followedCategories, [cat])
|
|
||||||
: union([cat], followedCategories),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Col>
|
|
||||||
</Col>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Contract } from 'web/lib/firebase/contracts'
|
import { Contract } from 'web/lib/firebase/contracts'
|
||||||
import { Comment } from 'web/lib/firebase/comments'
|
import { ContractComment } from 'common/comment'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { useBets } from 'web/hooks/use-bets'
|
import { useBets } from 'web/hooks/use-bets'
|
||||||
import { getSpecificContractActivityItems } from './activity-items'
|
import { getSpecificContractActivityItems } from './activity-items'
|
||||||
|
@ -12,7 +12,7 @@ import { LiquidityProvision } from 'common/liquidity-provision'
|
||||||
export function ContractActivity(props: {
|
export function ContractActivity(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: ContractComment[]
|
||||||
liquidityProvisions: LiquidityProvision[]
|
liquidityProvisions: LiquidityProvision[]
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Comment } from 'common/comment'
|
import { ContractComment } from 'common/comment'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
|
@ -24,7 +24,7 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
contract: any
|
contract: any
|
||||||
user: User | undefined | null
|
user: User | undefined | null
|
||||||
answer: Answer
|
answer: Answer
|
||||||
comments: Comment[]
|
comments: ContractComment[]
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
}) {
|
}) {
|
||||||
|
@ -69,7 +69,7 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
])
|
])
|
||||||
|
|
||||||
const scrollAndOpenReplyInput = useEvent(
|
const scrollAndOpenReplyInput = useEvent(
|
||||||
(comment?: Comment, answer?: Answer) => {
|
(comment?: ContractComment, answer?: Answer) => {
|
||||||
setReplyToUser(
|
setReplyToUser(
|
||||||
comment
|
comment
|
||||||
? { id: comment.userId, username: comment.userUsername }
|
? { id: comment.userId, username: comment.userUsername }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Comment } from 'common/comment'
|
import { ContractComment } from 'common/comment'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
@ -32,9 +32,9 @@ import { Editor } from '@tiptap/react'
|
||||||
|
|
||||||
export function FeedCommentThread(props: {
|
export function FeedCommentThread(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
comments: Comment[]
|
comments: ContractComment[]
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
parentComment: Comment
|
parentComment: ContractComment
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
smallAvatar?: boolean
|
smallAvatar?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
@ -50,7 +50,7 @@ export function FeedCommentThread(props: {
|
||||||
)
|
)
|
||||||
commentsList.unshift(parentComment)
|
commentsList.unshift(parentComment)
|
||||||
|
|
||||||
function scrollAndOpenReplyInput(comment: Comment) {
|
function scrollAndOpenReplyInput(comment: ContractComment) {
|
||||||
setReplyToUser({ id: comment.userId, username: comment.userUsername })
|
setReplyToUser({ id: comment.userId, username: comment.userUsername })
|
||||||
setShowReply(true)
|
setShowReply(true)
|
||||||
}
|
}
|
||||||
|
@ -95,10 +95,10 @@ export function FeedCommentThread(props: {
|
||||||
|
|
||||||
export function CommentRepliesList(props: {
|
export function CommentRepliesList(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
commentsList: Comment[]
|
commentsList: ContractComment[]
|
||||||
betsByUserId: Dictionary<Bet[]>
|
betsByUserId: Dictionary<Bet[]>
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
scrollAndOpenReplyInput: (comment: Comment) => void
|
scrollAndOpenReplyInput: (comment: ContractComment) => void
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
treatFirstIndexEqually?: boolean
|
treatFirstIndexEqually?: boolean
|
||||||
smallAvatar?: boolean
|
smallAvatar?: boolean
|
||||||
|
@ -156,12 +156,12 @@ export function CommentRepliesList(props: {
|
||||||
|
|
||||||
export function FeedComment(props: {
|
export function FeedComment(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
comment: Comment
|
comment: ContractComment
|
||||||
tips: CommentTips
|
tips: CommentTips
|
||||||
betsBySameUser: Bet[]
|
betsBySameUser: Bet[]
|
||||||
probAtCreatedTime?: number
|
probAtCreatedTime?: number
|
||||||
smallAvatar?: boolean
|
smallAvatar?: boolean
|
||||||
onReplyClick?: (comment: Comment) => void
|
onReplyClick?: (comment: ContractComment) => void
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
contract,
|
contract,
|
||||||
|
@ -274,7 +274,7 @@ export function FeedComment(props: {
|
||||||
|
|
||||||
export function getMostRecentCommentableBet(
|
export function getMostRecentCommentableBet(
|
||||||
betsByCurrentUser: Bet[],
|
betsByCurrentUser: Bet[],
|
||||||
commentsByCurrentUser: Comment[],
|
commentsByCurrentUser: ContractComment[],
|
||||||
user?: User | null,
|
user?: User | null,
|
||||||
answerOutcome?: string
|
answerOutcome?: string
|
||||||
) {
|
) {
|
||||||
|
@ -319,7 +319,7 @@ function CommentStatus(props: {
|
||||||
export function CommentInput(props: {
|
export function CommentInput(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
betsByCurrentUser: Bet[]
|
betsByCurrentUser: Bet[]
|
||||||
commentsByCurrentUser: Comment[]
|
commentsByCurrentUser: ContractComment[]
|
||||||
replyToUser?: { id: string; username: string }
|
replyToUser?: { id: string; username: string }
|
||||||
// Reply to a free response answer
|
// Reply to a free response answer
|
||||||
parentAnswerOutcome?: string
|
parentAnswerOutcome?: string
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { groupBy, mapValues, maxBy, sortBy } from 'lodash'
|
import { groupBy, mapValues, maxBy, sortBy } from 'lodash'
|
||||||
import { Contract } from 'web/lib/firebase/contracts'
|
import { Contract } from 'web/lib/firebase/contracts'
|
||||||
import { Comment } from 'web/lib/firebase/comments'
|
import { ContractComment } from 'common/comment'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
|
|
||||||
const MAX_ACTIVE_CONTRACTS = 75
|
const MAX_ACTIVE_CONTRACTS = 75
|
||||||
|
@ -19,7 +19,7 @@ function lastActivityTime(contract: Contract) {
|
||||||
// - Bet on market
|
// - Bet on market
|
||||||
export function findActiveContracts(
|
export function findActiveContracts(
|
||||||
allContracts: Contract[],
|
allContracts: Contract[],
|
||||||
recentComments: Comment[],
|
recentComments: ContractComment[],
|
||||||
recentBets: Bet[],
|
recentBets: Bet[],
|
||||||
seenContracts: { [contractId: string]: number }
|
seenContracts: { [contractId: string]: number }
|
||||||
) {
|
) {
|
||||||
|
@ -73,7 +73,7 @@ export function findActiveContracts(
|
||||||
)
|
)
|
||||||
const contractMostRecentComment = mapValues(
|
const contractMostRecentComment = mapValues(
|
||||||
contractComments,
|
contractComments,
|
||||||
(comments) => maxBy(comments, (c) => c.createdTime) as Comment
|
(comments) => maxBy(comments, (c) => c.createdTime) as ContractComment
|
||||||
)
|
)
|
||||||
|
|
||||||
const prioritizedContracts = sortBy(activeContracts, (c) => {
|
const prioritizedContracts = sortBy(activeContracts, (c) => {
|
||||||
|
|
|
@ -4,7 +4,8 @@ import { PrivateUser, User } from 'common/user'
|
||||||
import React, { useEffect, memo, useState, useMemo } from 'react'
|
import React, { useEffect, memo, useState, useMemo } from 'react'
|
||||||
import { Avatar } from 'web/components/avatar'
|
import { Avatar } from 'web/components/avatar'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { Comment, createCommentOnGroup } from 'web/lib/firebase/comments'
|
import { Comment, GroupComment } from 'common/comment'
|
||||||
|
import { createCommentOnGroup } from 'web/lib/firebase/comments'
|
||||||
import { CommentInputTextArea } from 'web/components/feed/feed-comments'
|
import { CommentInputTextArea } from 'web/components/feed/feed-comments'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||||
|
@ -24,7 +25,7 @@ import { setNotificationsAsSeen } from 'web/pages/notifications'
|
||||||
import { usePrivateUser } from 'web/hooks/use-user'
|
import { usePrivateUser } from 'web/hooks/use-user'
|
||||||
|
|
||||||
export function GroupChat(props: {
|
export function GroupChat(props: {
|
||||||
messages: Comment[]
|
messages: GroupComment[]
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
group: Group
|
group: Group
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
|
@ -58,7 +59,7 @@ export function GroupChat(props: {
|
||||||
// array of groups, where each group is an array of messages that are displayed as one
|
// array of groups, where each group is an array of messages that are displayed as one
|
||||||
const groupedMessages = useMemo(() => {
|
const groupedMessages = useMemo(() => {
|
||||||
// Group messages with createdTime within 2 minutes of each other.
|
// Group messages with createdTime within 2 minutes of each other.
|
||||||
const tempGrouped: Comment[][] = []
|
const tempGrouped: GroupComment[][] = []
|
||||||
for (let i = 0; i < messages.length; i++) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
const message = messages[i]
|
const message = messages[i]
|
||||||
if (i === 0) tempGrouped.push([message])
|
if (i === 0) tempGrouped.push([message])
|
||||||
|
@ -193,7 +194,7 @@ export function GroupChat(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupChatInBubble(props: {
|
export function GroupChatInBubble(props: {
|
||||||
messages: Comment[]
|
messages: GroupComment[]
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
privateUser: PrivateUser | null | undefined
|
privateUser: PrivateUser | null | undefined
|
||||||
group: Group
|
group: Group
|
||||||
|
@ -309,7 +310,7 @@ function GroupChatNotificationsIcon(props: {
|
||||||
|
|
||||||
const GroupMessage = memo(function GroupMessage_(props: {
|
const GroupMessage = memo(function GroupMessage_(props: {
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
comments: Comment[]
|
comments: GroupComment[]
|
||||||
group: Group
|
group: Group
|
||||||
onReplyClick?: (comment: Comment) => void
|
onReplyClick?: (comment: Comment) => void
|
||||||
setRef?: (ref: HTMLDivElement) => void
|
setRef?: (ref: HTMLDivElement) => void
|
||||||
|
@ -353,7 +354,7 @@ const GroupMessage = memo(function GroupMessage_(props: {
|
||||||
elementId={id}
|
elementId={id}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<div className="mt-2 text-black">
|
<div className="mt-2 text-base text-black">
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => (
|
||||||
<Content
|
<Content
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { Tabs } from './layout/tabs'
|
||||||
import { NoLabel, YesLabel } from './outcome-label'
|
import { NoLabel, YesLabel } from './outcome-label'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
import { InfoTooltip } from './info-tooltip'
|
||||||
|
|
||||||
export function LiquidityPanel(props: { contract: CPMMContract }) {
|
export function LiquidityPanel(props: { contract: CPMMContract }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
|
@ -101,8 +102,9 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="align-center mb-4 text-gray-500">
|
<div className="mb-4 text-gray-500">
|
||||||
Subsidize this market by adding M$ to the liquidity pool.
|
Contribute your M$ to make this market more accurate.{' '}
|
||||||
|
<InfoTooltip text="More liquidity stabilizes the market, encouraging traders to bet. You can withdraw your subsidy at any time." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
|
|
|
@ -33,7 +33,7 @@ function getNavigation() {
|
||||||
|
|
||||||
const signedOutNavigation = [
|
const signedOutNavigation = [
|
||||||
{ name: 'Home', href: '/', icon: HomeIcon },
|
{ name: 'Home', href: '/', icon: HomeIcon },
|
||||||
{ name: 'Explore', href: '/markets', icon: SearchIcon },
|
{ name: 'Explore', href: '/home', icon: SearchIcon },
|
||||||
]
|
]
|
||||||
|
|
||||||
// From https://codepen.io/chris__sev/pen/QWGvYbL
|
// From https://codepen.io/chris__sev/pen/QWGvYbL
|
||||||
|
|
|
@ -99,8 +99,8 @@ function getMoreNavigation(user?: User | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const signedOutNavigation = [
|
const signedOutNavigation = [
|
||||||
{ name: 'Home', href: '/home', icon: HomeIcon },
|
{ name: 'Home', href: '/', icon: HomeIcon },
|
||||||
{ name: 'Explore', href: '/markets', icon: SearchIcon },
|
{ name: 'Explore', href: '/home', icon: SearchIcon },
|
||||||
{
|
{
|
||||||
name: 'About',
|
name: 'About',
|
||||||
href: 'https://docs.manifold.markets/$how-to',
|
href: 'https://docs.manifold.markets/$how-to',
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import clsx from 'clsx'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { parseWordsAsTags } from 'common/util/parse'
|
|
||||||
import { Contract, updateContract } from 'web/lib/firebase/contracts'
|
|
||||||
import { Col } from './layout/col'
|
|
||||||
import { Row } from './layout/row'
|
|
||||||
import { TagsList } from './tags-list'
|
|
||||||
import { MAX_TAG_LENGTH } from 'common/contract'
|
|
||||||
|
|
||||||
export function TagsInput(props: { contract: Contract; className?: string }) {
|
|
||||||
const { contract, className } = props
|
|
||||||
const { tags } = contract
|
|
||||||
|
|
||||||
const [tagText, setTagText] = useState('')
|
|
||||||
const newTags = parseWordsAsTags(`${tags.join(' ')} ${tagText}`)
|
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
||||||
|
|
||||||
const updateTags = async () => {
|
|
||||||
setIsSubmitting(true)
|
|
||||||
await updateContract(contract.id, {
|
|
||||||
tags: newTags,
|
|
||||||
lowercaseTags: newTags.map((tag) => tag.toLowerCase()),
|
|
||||||
})
|
|
||||||
setIsSubmitting(false)
|
|
||||||
setTagText('')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col className={clsx('gap-4', className)}>
|
|
||||||
<TagsList tags={newTags} noLabel />
|
|
||||||
|
|
||||||
<Row className="items-center gap-4">
|
|
||||||
<input
|
|
||||||
style={{ maxWidth: 150 }}
|
|
||||||
placeholder="Type a tag..."
|
|
||||||
className="input input-sm input-bordered resize-none"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
value={tagText}
|
|
||||||
maxLength={MAX_TAG_LENGTH}
|
|
||||||
onChange={(e) => setTagText(e.target.value || '')}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
updateTags()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button className="btn btn-xs btn-outline" onClick={updateTags}>
|
|
||||||
Save tags
|
|
||||||
</button>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
import clsx from 'clsx'
|
|
||||||
import { CATEGORIES, category } from '../../common/categories'
|
|
||||||
import { Col } from './layout/col'
|
|
||||||
|
|
||||||
import { Row } from './layout/row'
|
|
||||||
import { SiteLink } from './site-link'
|
|
||||||
|
|
||||||
function Hashtag(props: { tag: string; noLink?: boolean }) {
|
|
||||||
const { tag, noLink } = props
|
|
||||||
const category = CATEGORIES[tag.replace('#', '').toLowerCase() as category]
|
|
||||||
|
|
||||||
const body = (
|
|
||||||
<div className={clsx('', !noLink && 'cursor-pointer')}>
|
|
||||||
<span className="text-sm">{category ? '#' + category : tag} </span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (noLink) return body
|
|
||||||
return (
|
|
||||||
<SiteLink href={`/tag/${tag.substring(1)}`} className="flex items-center">
|
|
||||||
{body}
|
|
||||||
</SiteLink>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TagsList(props: {
|
|
||||||
tags: string[]
|
|
||||||
className?: string
|
|
||||||
noLink?: boolean
|
|
||||||
noLabel?: boolean
|
|
||||||
label?: string
|
|
||||||
}) {
|
|
||||||
const { tags, className, noLink, noLabel, label } = props
|
|
||||||
return (
|
|
||||||
<Row className={clsx('flex-wrap items-center gap-2', className)}>
|
|
||||||
{!noLabel && <div className="mr-1">{label || 'Tags'}</div>}
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<Hashtag
|
|
||||||
key={tag}
|
|
||||||
tag={tag.startsWith('#') ? tag : `#${tag}`}
|
|
||||||
noLink={noLink}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FoldTag(props: { fold: { slug: string; name: string } }) {
|
|
||||||
const { fold } = props
|
|
||||||
const { slug, name } = fold
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SiteLink href={`/fold/${slug}`} className="flex items-center">
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'rounded-full border-2 bg-white px-4 py-1 shadow-md',
|
|
||||||
'cursor-pointer'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="text-sm text-gray-500">{name}</span>
|
|
||||||
</div>
|
|
||||||
</SiteLink>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FoldTagList(props: {
|
|
||||||
folds: { slug: string; name: string }[]
|
|
||||||
noLabel?: boolean
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const { folds, noLabel, className } = props
|
|
||||||
return (
|
|
||||||
<Col className="gap-2">
|
|
||||||
{!noLabel && <div className="mr-1 text-gray-500">Communities</div>}
|
|
||||||
<Row className={clsx('flex-wrap items-center gap-2', className)}>
|
|
||||||
{folds.length > 0 && (
|
|
||||||
<>
|
|
||||||
{folds.map((fold) => (
|
|
||||||
<FoldTag key={fold.slug} fold={fold} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -42,6 +42,10 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contractId =
|
||||||
|
comment.commentType === 'contract' ? comment.contractId : undefined
|
||||||
|
const groupId =
|
||||||
|
comment.commentType === 'group' ? comment.groupId : undefined
|
||||||
await transact({
|
await transact({
|
||||||
amount: change,
|
amount: change,
|
||||||
fromId: user.id,
|
fromId: user.id,
|
||||||
|
@ -50,18 +54,14 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
||||||
toType: 'USER',
|
toType: 'USER',
|
||||||
token: 'M$',
|
token: 'M$',
|
||||||
category: 'TIP',
|
category: 'TIP',
|
||||||
data: {
|
data: { commentId: comment.id, contractId, groupId },
|
||||||
contractId: comment.contractId,
|
|
||||||
commentId: comment.id,
|
|
||||||
groupId: comment.groupId,
|
|
||||||
},
|
|
||||||
description: `${user.name} tipped M$ ${change} to ${comment.userName} for a comment`,
|
description: `${user.name} tipped M$ ${change} to ${comment.userName} for a comment`,
|
||||||
})
|
})
|
||||||
|
|
||||||
track('send comment tip', {
|
track('send comment tip', {
|
||||||
contractId: comment.contractId,
|
|
||||||
commentId: comment.id,
|
commentId: comment.id,
|
||||||
groupId: comment.groupId,
|
contractId,
|
||||||
|
groupId,
|
||||||
amount: change,
|
amount: change,
|
||||||
fromId: user.id,
|
fromId: user.id,
|
||||||
toId: comment.userId,
|
toId: comment.userId,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
Placement,
|
Placement,
|
||||||
shift,
|
shift,
|
||||||
useFloating,
|
useFloating,
|
||||||
useFocus,
|
|
||||||
useHover,
|
useHover,
|
||||||
useInteractions,
|
useInteractions,
|
||||||
useRole,
|
useRole,
|
||||||
|
@ -48,7 +47,6 @@ export function Tooltip(props: {
|
||||||
|
|
||||||
const { getReferenceProps, getFloatingProps } = useInteractions([
|
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||||
useHover(context, { mouseOnly: noTap }),
|
useHover(context, { mouseOnly: noTap }),
|
||||||
useFocus(context),
|
|
||||||
useRole(context, { role: 'tooltip' }),
|
useRole(context, { role: 'tooltip' }),
|
||||||
])
|
])
|
||||||
// which side of tooltip arrow is on. like: if tooltip is top-left, arrow is on bottom of tooltip
|
// which side of tooltip arrow is on. like: if tooltip is top-left, arrow is on bottom of tooltip
|
||||||
|
@ -64,7 +62,6 @@ export function Tooltip(props: {
|
||||||
<div
|
<div
|
||||||
className={clsx('inline-block', className)}
|
className={clsx('inline-block', className)}
|
||||||
ref={reference}
|
ref={reference}
|
||||||
tabIndex={noTap ? undefined : 0}
|
|
||||||
{...getReferenceProps()}
|
{...getReferenceProps()}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -58,8 +58,6 @@ export function UserLink(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TAB_IDS = ['markets', 'comments', 'bets', 'groups']
|
|
||||||
|
|
||||||
export function UserPage(props: { user: User }) {
|
export function UserPage(props: { user: User }) {
|
||||||
const { user } = props
|
const { user } = props
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -103,10 +101,10 @@ export function UserPage(props: { user: User }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Top right buttons (e.g. edit, follow) */}
|
{/* Top right buttons (e.g. edit, follow) */}
|
||||||
<div className="absolute right-0 top-0 mt-4 mr-4">
|
<div className="absolute right-0 top-0 mt-2 mr-4">
|
||||||
{!isCurrentUser && <UserFollowButton userId={user.id} />}
|
{!isCurrentUser && <UserFollowButton userId={user.id} />}
|
||||||
{isCurrentUser && (
|
{isCurrentUser && (
|
||||||
<SiteLink className="btn" href="/profile">
|
<SiteLink className="sm:btn-md btn-sm btn" href="/profile">
|
||||||
<PencilIcon className="h-5 w-5" />{' '}
|
<PencilIcon className="h-5 w-5" />{' '}
|
||||||
<div className="ml-2">Edit</div>
|
<div className="ml-2">Edit</div>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
|
@ -116,19 +114,22 @@ export function UserPage(props: { user: User }) {
|
||||||
|
|
||||||
{/* Profile details: name, username, bio, and link to twitter/discord */}
|
{/* Profile details: name, username, bio, and link to twitter/discord */}
|
||||||
<Col className="mx-4 -mt-6">
|
<Col className="mx-4 -mt-6">
|
||||||
<span className="text-2xl font-bold">{user.name}</span>
|
<Row className={'items-center gap-2'}>
|
||||||
|
<span className="text-2xl font-bold">{user.name}</span>
|
||||||
|
<span className="mt-1 text-gray-500">
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'text-md',
|
||||||
|
profit >= 0 ? 'text-green-600' : 'text-red-400'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatMoney(profit)}
|
||||||
|
</span>{' '}
|
||||||
|
profit
|
||||||
|
</span>
|
||||||
|
</Row>
|
||||||
<span className="text-gray-500">@{user.username}</span>
|
<span className="text-gray-500">@{user.username}</span>
|
||||||
<span className="text-gray-500">
|
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
'text-md',
|
|
||||||
profit >= 0 ? 'text-green-600' : 'text-red-400'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatMoney(profit)}
|
|
||||||
</span>{' '}
|
|
||||||
profit
|
|
||||||
</span>
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
{user.bio && (
|
{user.bio && (
|
||||||
<>
|
<>
|
||||||
|
@ -138,17 +139,7 @@ export function UserPage(props: { user: User }) {
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Col className="flex-wrap gap-2 sm:flex-row sm:items-center sm:gap-4">
|
<Row className="flex-wrap items-center gap-2 sm:gap-4">
|
||||||
<Row className="gap-4">
|
|
||||||
<FollowingButton user={user} />
|
|
||||||
<FollowersButton user={user} />
|
|
||||||
{currentUser &&
|
|
||||||
['ian', 'Austin', 'SG', 'JamesGrugett'].includes(
|
|
||||||
currentUser.username
|
|
||||||
) && <ReferralsButton user={user} />}
|
|
||||||
<GroupsButton user={user} />
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{user.website && (
|
{user.website && (
|
||||||
<SiteLink
|
<SiteLink
|
||||||
href={
|
href={
|
||||||
|
@ -198,7 +189,7 @@ export function UserPage(props: { user: User }) {
|
||||||
</Row>
|
</Row>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Row>
|
||||||
<Spacer h={5} />
|
<Spacer h={5} />
|
||||||
{currentUser?.id === user.id && (
|
{currentUser?.id === user.id && (
|
||||||
<Row
|
<Row
|
||||||
|
@ -208,7 +199,7 @@ export function UserPage(props: { user: User }) {
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<SiteLink href="/referrals">
|
<SiteLink href="/referrals">
|
||||||
Refer a friend and earn {formatMoney(500)} when they sign up!
|
Earn {formatMoney(500)} when you refer a friend!
|
||||||
</SiteLink>{' '}
|
</SiteLink>{' '}
|
||||||
You have <ReferralsButton user={user} currentUser={currentUser} />
|
You have <ReferralsButton user={user} currentUser={currentUser} />
|
||||||
</span>
|
</span>
|
||||||
|
@ -244,6 +235,22 @@ export function UserPage(props: { user: User }) {
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Social',
|
||||||
|
content: (
|
||||||
|
<Row
|
||||||
|
className={'mt-2 flex-wrap items-center justify-center gap-6'}
|
||||||
|
>
|
||||||
|
<FollowingButton user={user} />
|
||||||
|
<FollowersButton user={user} />
|
||||||
|
{currentUser &&
|
||||||
|
['ian', 'Austin', 'SG', 'JamesGrugett'].includes(
|
||||||
|
currentUser.username
|
||||||
|
) && <ReferralsButton user={user} />}
|
||||||
|
<GroupsButton user={user} />
|
||||||
|
</Row>
|
||||||
|
),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
import type { feed } from 'common/feed'
|
|
||||||
import { useTimeSinceFirstRender } from './use-time-since-first-render'
|
|
||||||
import { trackLatency } from 'web/lib/firebase/tracking'
|
|
||||||
import { User } from 'common/user'
|
|
||||||
import { getCategoryFeeds, getUserFeed } from 'web/lib/firebase/users'
|
|
||||||
import {
|
|
||||||
getRecentBetsAndComments,
|
|
||||||
getTopWeeklyContracts,
|
|
||||||
} from 'web/lib/firebase/contracts'
|
|
||||||
|
|
||||||
export const useAlgoFeed = (
|
|
||||||
user: User | null | undefined,
|
|
||||||
category: string
|
|
||||||
) => {
|
|
||||||
const [allFeed, setAllFeed] = useState<feed>()
|
|
||||||
const [categoryFeeds, setCategoryFeeds] = useState<{ [x: string]: feed }>()
|
|
||||||
|
|
||||||
const getTime = useTimeSinceFirstRender()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
getUserFeed(user.id).then((feed) => {
|
|
||||||
if (feed.length === 0) {
|
|
||||||
getDefaultFeed().then((feed) => setAllFeed(feed))
|
|
||||||
} else setAllFeed(feed)
|
|
||||||
|
|
||||||
trackLatency(user.id, 'feed', getTime())
|
|
||||||
console.log('"all" feed load time', getTime())
|
|
||||||
})
|
|
||||||
|
|
||||||
getCategoryFeeds(user.id).then((feeds) => {
|
|
||||||
setCategoryFeeds(feeds)
|
|
||||||
console.log('category feeds load time', getTime())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [user?.id])
|
|
||||||
|
|
||||||
const feed = category === 'all' ? allFeed : categoryFeeds?.[category]
|
|
||||||
|
|
||||||
return feed
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDefaultFeed = async () => {
|
|
||||||
const contracts = await getTopWeeklyContracts()
|
|
||||||
const feed = await Promise.all(
|
|
||||||
contracts.map((c) => getRecentBetsAndComments(c))
|
|
||||||
)
|
|
||||||
return feed
|
|
||||||
}
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Comment, ContractComment, GroupComment } from 'common/comment'
|
||||||
import {
|
import {
|
||||||
Comment,
|
|
||||||
listenForCommentsOnContract,
|
listenForCommentsOnContract,
|
||||||
listenForCommentsOnGroup,
|
listenForCommentsOnGroup,
|
||||||
listenForRecentComments,
|
listenForRecentComments,
|
||||||
} from 'web/lib/firebase/comments'
|
} from 'web/lib/firebase/comments'
|
||||||
|
|
||||||
export const useComments = (contractId: string) => {
|
export const useComments = (contractId: string) => {
|
||||||
const [comments, setComments] = useState<Comment[] | undefined>()
|
const [comments, setComments] = useState<ContractComment[] | undefined>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contractId) return listenForCommentsOnContract(contractId, setComments)
|
if (contractId) return listenForCommentsOnContract(contractId, setComments)
|
||||||
|
@ -16,7 +16,7 @@ export const useComments = (contractId: string) => {
|
||||||
return comments
|
return comments
|
||||||
}
|
}
|
||||||
export const useCommentsOnGroup = (groupId: string | undefined) => {
|
export const useCommentsOnGroup = (groupId: string | undefined) => {
|
||||||
const [comments, setComments] = useState<Comment[] | undefined>()
|
const [comments, setComments] = useState<GroupComment[] | undefined>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (groupId) return listenForCommentsOnGroup(groupId, setComments)
|
if (groupId) return listenForCommentsOnGroup(groupId, setComments)
|
||||||
|
|
|
@ -29,13 +29,11 @@ export async function createChallenge(data: {
|
||||||
creatorAmount: number
|
creatorAmount: number
|
||||||
acceptorAmount: number
|
acceptorAmount: number
|
||||||
expiresTime: number | null
|
expiresTime: number | null
|
||||||
message: string
|
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
creator,
|
creator,
|
||||||
creatorAmount,
|
creatorAmount,
|
||||||
expiresTime,
|
expiresTime,
|
||||||
message,
|
|
||||||
contract,
|
contract,
|
||||||
outcome,
|
outcome,
|
||||||
acceptorAmount,
|
acceptorAmount,
|
||||||
|
@ -73,7 +71,7 @@ export async function createChallenge(data: {
|
||||||
acceptedByUserIds: [],
|
acceptedByUserIds: [],
|
||||||
acceptances: [],
|
acceptances: [],
|
||||||
isResolved: false,
|
isResolved: false,
|
||||||
message,
|
message: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
await setDoc(doc(challenges(contract.id), slug), challenge)
|
await setDoc(doc(challenges(contract.id), slug), challenge)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
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 } from 'common/comment'
|
import { Comment, ContractComment, GroupComment } 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'
|
||||||
|
@ -31,8 +31,10 @@ export async function createCommentOnContract(
|
||||||
const ref = betId
|
const ref = betId
|
||||||
? doc(getCommentsCollection(contractId), betId)
|
? doc(getCommentsCollection(contractId), betId)
|
||||||
: doc(getCommentsCollection(contractId))
|
: doc(getCommentsCollection(contractId))
|
||||||
const comment: Comment = removeUndefinedProps({
|
// contract slug and question are set via trigger
|
||||||
|
const comment = removeUndefinedProps({
|
||||||
id: ref.id,
|
id: ref.id,
|
||||||
|
commentType: 'contract',
|
||||||
contractId,
|
contractId,
|
||||||
userId: commenter.id,
|
userId: commenter.id,
|
||||||
content: content,
|
content: content,
|
||||||
|
@ -59,8 +61,9 @@ export async function createCommentOnGroup(
|
||||||
replyToCommentId?: string
|
replyToCommentId?: string
|
||||||
) {
|
) {
|
||||||
const ref = doc(getCommentsOnGroupCollection(groupId))
|
const ref = doc(getCommentsOnGroupCollection(groupId))
|
||||||
const comment: Comment = removeUndefinedProps({
|
const comment = removeUndefinedProps({
|
||||||
id: ref.id,
|
id: ref.id,
|
||||||
|
commentType: 'group',
|
||||||
groupId,
|
groupId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
content: content,
|
content: content,
|
||||||
|
@ -94,7 +97,7 @@ export async function listAllComments(contractId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listAllCommentsOnGroup(groupId: string) {
|
export async function listAllCommentsOnGroup(groupId: string) {
|
||||||
const comments = await getValues<Comment>(
|
const comments = await getValues<GroupComment>(
|
||||||
getCommentsOnGroupCollection(groupId)
|
getCommentsOnGroupCollection(groupId)
|
||||||
)
|
)
|
||||||
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||||
|
@ -103,9 +106,9 @@ export async function listAllCommentsOnGroup(groupId: string) {
|
||||||
|
|
||||||
export function listenForCommentsOnContract(
|
export function listenForCommentsOnContract(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
setComments: (comments: Comment[]) => void
|
setComments: (comments: ContractComment[]) => void
|
||||||
) {
|
) {
|
||||||
return listenForValues<Comment>(
|
return listenForValues<ContractComment>(
|
||||||
getCommentsCollection(contractId),
|
getCommentsCollection(contractId),
|
||||||
(comments) => {
|
(comments) => {
|
||||||
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||||
|
@ -115,9 +118,9 @@ export function listenForCommentsOnContract(
|
||||||
}
|
}
|
||||||
export function listenForCommentsOnGroup(
|
export function listenForCommentsOnGroup(
|
||||||
groupId: string,
|
groupId: string,
|
||||||
setComments: (comments: Comment[]) => void
|
setComments: (comments: GroupComment[]) => void
|
||||||
) {
|
) {
|
||||||
return listenForValues<Comment>(
|
return listenForValues<GroupComment>(
|
||||||
getCommentsOnGroupCollection(groupId),
|
getCommentsOnGroupCollection(groupId),
|
||||||
(comments) => {
|
(comments) => {
|
||||||
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { createRNG, shuffle } from 'common/util/random'
|
||||||
import { getCpmmProbability } from 'common/calculate-cpmm'
|
import { getCpmmProbability } from 'common/calculate-cpmm'
|
||||||
import { formatMoney, formatPercent } from 'common/util/format'
|
import { formatMoney, formatPercent } from 'common/util/format'
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
import { MAX_FEED_CONTRACTS } from 'common/recommended-contracts'
|
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Comment } from 'common/comment'
|
import { Comment } from 'common/comment'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
@ -285,16 +284,6 @@ export async function getContractsBySlugs(slugs: string[]) {
|
||||||
return sortBy(data, (contract) => -1 * contract.volume24Hours)
|
return sortBy(data, (contract) => -1 * contract.volume24Hours)
|
||||||
}
|
}
|
||||||
|
|
||||||
const topWeeklyQuery = query(
|
|
||||||
contracts,
|
|
||||||
where('isResolved', '==', false),
|
|
||||||
orderBy('volume7Days', 'desc'),
|
|
||||||
limit(MAX_FEED_CONTRACTS)
|
|
||||||
)
|
|
||||||
export async function getTopWeeklyContracts() {
|
|
||||||
return await getValues<Contract>(topWeeklyQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
const closingSoonQuery = query(
|
const closingSoonQuery = query(
|
||||||
contracts,
|
contracts,
|
||||||
where('isResolved', '==', false),
|
where('isResolved', '==', false),
|
||||||
|
|
|
@ -14,20 +14,10 @@ import {
|
||||||
onSnapshot,
|
onSnapshot,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import { getAuth } from 'firebase/auth'
|
import { getAuth } from 'firebase/auth'
|
||||||
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
|
|
||||||
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'
|
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'
|
||||||
import { zip } from 'lodash'
|
|
||||||
import { app, db } from './init'
|
import { app, db } from './init'
|
||||||
import { PortfolioMetrics, PrivateUser, User } from 'common/user'
|
import { PortfolioMetrics, PrivateUser, User } from 'common/user'
|
||||||
import {
|
import { coll, getValues, listenForValue, listenForValues } from './utils'
|
||||||
coll,
|
|
||||||
getValue,
|
|
||||||
getValues,
|
|
||||||
listenForValue,
|
|
||||||
listenForValues,
|
|
||||||
} from './utils'
|
|
||||||
import { feed } from 'common/feed'
|
|
||||||
import { CATEGORY_LIST } from 'common/categories'
|
|
||||||
import { safeLocalStorage } from '../util/local'
|
import { safeLocalStorage } from '../util/local'
|
||||||
import { filterDefined } from 'common/util/array'
|
import { filterDefined } from 'common/util/array'
|
||||||
import { addUserToGroupViaId } from 'web/lib/firebase/groups'
|
import { addUserToGroupViaId } from 'web/lib/firebase/groups'
|
||||||
|
@ -202,20 +192,6 @@ export async function firebaseLogout() {
|
||||||
await auth.signOut()
|
await auth.signOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = getStorage(app)
|
|
||||||
// Example: uploadData('avatars/ajfi8iejsf.png', data)
|
|
||||||
export async function uploadData(
|
|
||||||
path: string,
|
|
||||||
data: ArrayBuffer | Blob | Uint8Array
|
|
||||||
) {
|
|
||||||
const uploadRef = ref(storage, path)
|
|
||||||
// Uploaded files should be cached for 1 day, then revalidated
|
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
|
||||||
const metadata = { cacheControl: 'public, max-age=86400, must-revalidate' }
|
|
||||||
await uploadBytes(uploadRef, data, metadata)
|
|
||||||
return await getDownloadURL(uploadRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listUsers(userIds: string[]) {
|
export async function listUsers(userIds: string[]) {
|
||||||
if (userIds.length > 10) {
|
if (userIds.length > 10) {
|
||||||
throw new Error('Too many users requested at once; Firestore limits to 10')
|
throw new Error('Too many users requested at once; Firestore limits to 10')
|
||||||
|
@ -263,25 +239,6 @@ export function getUsers() {
|
||||||
return getValues<User>(users)
|
return getValues<User>(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserFeed(userId: string) {
|
|
||||||
const feedDoc = doc(privateUsers, userId, 'cache', 'feed')
|
|
||||||
const userFeed = await getValue<{
|
|
||||||
feed: feed
|
|
||||||
}>(feedDoc)
|
|
||||||
return userFeed?.feed ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCategoryFeeds(userId: string) {
|
|
||||||
const cacheCollection = collection(privateUsers, userId, 'cache')
|
|
||||||
const feedData = await Promise.all(
|
|
||||||
CATEGORY_LIST.map((category) =>
|
|
||||||
getValue<{ feed: feed }>(doc(cacheCollection, `feed-${category}`))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
const feeds = feedData.map((data) => data?.feed ?? [])
|
|
||||||
return Object.fromEntries(zip(CATEGORY_LIST, feeds) as [string, feed][])
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function follow(userId: string, followedUserId: string) {
|
export async function follow(userId: string, followedUserId: string) {
|
||||||
const followDoc = doc(collection(users, userId, 'follows'), followedUserId)
|
const followDoc = doc(collection(users, userId, 'follows'), followedUserId)
|
||||||
await setDoc(followDoc, {
|
await setDoc(followDoc, {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
const API_DOCS_URL = 'https://docs.manifold.markets/api'
|
const API_DOCS_URL = 'https://docs.manifold.markets/api'
|
||||||
|
|
||||||
|
const ABOUT_PAGE_URL = 'https://docs.manifold.markets/$how-to'
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
staticPageGenerationTimeout: 600, // e.g. stats page
|
staticPageGenerationTimeout: 600, // e.g. stats page
|
||||||
|
@ -35,6 +37,11 @@ module.exports = {
|
||||||
destination: API_DOCS_URL,
|
destination: API_DOCS_URL,
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: '/about',
|
||||||
|
destination: ABOUT_PAGE_URL,
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: '/analytics',
|
source: '/analytics',
|
||||||
destination: '/stats',
|
destination: '/stats',
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"csstype": "^3.1.0",
|
"csstype": "^3.1.0",
|
||||||
"eslint-config-next": "12.1.6",
|
"eslint-config-next": "12.1.6",
|
||||||
|
"eslint-config-prettier": "8.5.0",
|
||||||
"next-sitemap": "^2.5.14",
|
"next-sitemap": "^2.5.14",
|
||||||
"postcss": "8.3.5",
|
"postcss": "8.3.5",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.5",
|
"prettier-plugin-tailwindcss": "^0.1.5",
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { Bet, listAllBets } from 'web/lib/firebase/bets'
|
import { Bet, listAllBets } from 'web/lib/firebase/bets'
|
||||||
import { Comment, listAllComments } from 'web/lib/firebase/comments'
|
import { listAllComments } from 'web/lib/firebase/comments'
|
||||||
import Custom404 from '../404'
|
import Custom404 from '../404'
|
||||||
import { AnswersPanel } from 'web/components/answers/answers-panel'
|
import { AnswersPanel } from 'web/components/answers/answers-panel'
|
||||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||||
|
@ -38,6 +38,7 @@ import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
||||||
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
import { getOpenGraphProps } from 'web/components/contract/contract-card-preview'
|
import { getOpenGraphProps } from 'web/components/contract/contract-card-preview'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
|
import { ContractComment } from 'common/comment'
|
||||||
import { listUsers } from 'web/lib/firebase/users'
|
import { listUsers } from 'web/lib/firebase/users'
|
||||||
import { FeedComment } from 'web/components/feed/feed-comments'
|
import { FeedComment } from 'web/components/feed/feed-comments'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
|
@ -78,7 +79,7 @@ export default function ContractPage(props: {
|
||||||
contract: Contract | null
|
contract: Contract | null
|
||||||
username: string
|
username: string
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: ContractComment[]
|
||||||
slug: string
|
slug: string
|
||||||
backToHome?: () => void
|
backToHome?: () => void
|
||||||
}) {
|
}) {
|
||||||
|
@ -314,7 +315,7 @@ function ContractLeaderboard(props: { contract: Contract; bets: Bet[] }) {
|
||||||
function ContractTopTrades(props: {
|
function ContractTopTrades(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: ContractComment[]
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
}) {
|
}) {
|
||||||
const { contract, bets, comments, tips } = props
|
const { contract, bets, comments, tips } = props
|
||||||
|
|
|
@ -7,6 +7,7 @@ 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 { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { JSONContent } from '@tiptap/core'
|
import { JSONContent } from '@tiptap/core'
|
||||||
|
import { richTextToString } from 'common/util/parse'
|
||||||
|
|
||||||
export type LiteMarket = {
|
export type LiteMarket = {
|
||||||
// Unique identifer for this market
|
// Unique identifer for this market
|
||||||
|
@ -22,6 +23,7 @@ export type LiteMarket = {
|
||||||
closeTime?: number
|
closeTime?: number
|
||||||
question: string
|
question: string
|
||||||
description: string | JSONContent
|
description: string | JSONContent
|
||||||
|
textDescription: string // string version of description
|
||||||
tags: string[]
|
tags: string[]
|
||||||
url: string
|
url: string
|
||||||
outcomeType: string
|
outcomeType: string
|
||||||
|
@ -40,6 +42,8 @@ export type LiteMarket = {
|
||||||
resolution?: string
|
resolution?: string
|
||||||
resolutionTime?: number
|
resolutionTime?: number
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number
|
||||||
|
|
||||||
|
lastUpdatedTime?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiAnswer = Answer & {
|
export type ApiAnswer = Answer & {
|
||||||
|
@ -90,6 +94,7 @@ export function toLiteMarket(contract: Contract): LiteMarket {
|
||||||
resolution,
|
resolution,
|
||||||
resolutionTime,
|
resolutionTime,
|
||||||
resolutionProbability,
|
resolutionProbability,
|
||||||
|
lastUpdatedTime,
|
||||||
} = contract
|
} = contract
|
||||||
|
|
||||||
const { p, totalLiquidity } = contract as any
|
const { p, totalLiquidity } = contract as any
|
||||||
|
@ -97,6 +102,11 @@ export function toLiteMarket(contract: Contract): LiteMarket {
|
||||||
const probability =
|
const probability =
|
||||||
contract.outcomeType === 'BINARY' ? getProbability(contract) : undefined
|
contract.outcomeType === 'BINARY' ? getProbability(contract) : undefined
|
||||||
|
|
||||||
|
let min, max, isLogScale: any
|
||||||
|
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
|
||||||
|
;({ min, max, isLogScale } = contract)
|
||||||
|
}
|
||||||
|
|
||||||
return removeUndefinedProps({
|
return removeUndefinedProps({
|
||||||
id,
|
id,
|
||||||
creatorUsername,
|
creatorUsername,
|
||||||
|
@ -109,6 +119,10 @@ export function toLiteMarket(contract: Contract): LiteMarket {
|
||||||
: closeTime,
|
: closeTime,
|
||||||
question,
|
question,
|
||||||
description,
|
description,
|
||||||
|
textDescription:
|
||||||
|
typeof description === 'string'
|
||||||
|
? description
|
||||||
|
: richTextToString(description),
|
||||||
tags,
|
tags,
|
||||||
url: `https://manifold.markets/${creatorUsername}/${slug}`,
|
url: `https://manifold.markets/${creatorUsername}/${slug}`,
|
||||||
pool,
|
pool,
|
||||||
|
@ -124,6 +138,10 @@ export function toLiteMarket(contract: Contract): LiteMarket {
|
||||||
resolution,
|
resolution,
|
||||||
resolutionTime,
|
resolutionTime,
|
||||||
resolutionProbability,
|
resolutionProbability,
|
||||||
|
lastUpdatedTime,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
isLogScale,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
useAcceptedChallenges,
|
useAcceptedChallenges,
|
||||||
useUserChallenges,
|
useUserChallenges,
|
||||||
} from 'web/lib/firebase/challenges'
|
} from 'web/lib/firebase/challenges'
|
||||||
import { Challenge } from 'common/challenge'
|
import { Challenge, CHALLENGES_ENABLED } from 'common/challenge'
|
||||||
import { Tabs } from 'web/components/layout/tabs'
|
import { Tabs } from 'web/components/layout/tabs'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { UserLink } from 'web/components/user-page'
|
import { UserLink } from 'web/components/user-page'
|
||||||
|
@ -29,6 +29,7 @@ import { copyToClipboard } from 'web/lib/util/copy'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
import { QRCode } from 'web/components/qr-code'
|
import { QRCode } from 'web/components/qr-code'
|
||||||
|
import { CreateChallengeModal } from 'web/components/challenges/create-challenge-modal'
|
||||||
|
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
const columnClass = 'sm:px-5 px-2 py-3.5 max-w-[100px] truncate'
|
const columnClass = 'sm:px-5 px-2 py-3.5 max-w-[100px] truncate'
|
||||||
|
@ -37,6 +38,7 @@ const amountClass = columnClass + ' max-w-[75px] font-bold'
|
||||||
export default function ChallengesListPage() {
|
export default function ChallengesListPage() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const challenges = useAcceptedChallenges()
|
const challenges = useAcceptedChallenges()
|
||||||
|
const [open, setOpen] = React.useState(false)
|
||||||
const userChallenges = useUserChallenges(user?.id)
|
const userChallenges = useUserChallenges(user?.id)
|
||||||
.concat(
|
.concat(
|
||||||
user ? challenges.filter((c) => c.acceptances[0].userId === user.id) : []
|
user ? challenges.filter((c) => c.acceptances[0].userId === user.id) : []
|
||||||
|
@ -70,8 +72,25 @@ export default function ChallengesListPage() {
|
||||||
<Col className="w-full px-8">
|
<Col className="w-full px-8">
|
||||||
<Row className="items-center justify-between">
|
<Row className="items-center justify-between">
|
||||||
<Title text="Challenges" />
|
<Title text="Challenges" />
|
||||||
|
{CHALLENGES_ENABLED && (
|
||||||
|
<Button size="lg" color="gradient" onClick={() => setOpen(true)}>
|
||||||
|
Create Challenge
|
||||||
|
<CreateChallengeModal
|
||||||
|
isOpen={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<p>Find or create a question to challenge someone to a bet.</p>
|
<p>
|
||||||
|
Want to create your own challenge?
|
||||||
|
<SiteLink className={'mx-1 font-bold'} href={'/home'}>
|
||||||
|
Find
|
||||||
|
</SiteLink>
|
||||||
|
a market you and a friend disagree on and hit the challenge button, or
|
||||||
|
tap the button above to create a new market & challenge in one.
|
||||||
|
</p>
|
||||||
|
|
||||||
<Tabs tabs={[...userTab, ...publicTab]} />
|
<Tabs tabs={[...userTab, ...publicTab]} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -46,7 +46,7 @@ import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
import { Button } from 'web/components/button'
|
import { Button } from 'web/components/button'
|
||||||
import { listAllCommentsOnGroup } from 'web/lib/firebase/comments'
|
import { listAllCommentsOnGroup } from 'web/lib/firebase/comments'
|
||||||
import { Comment } from 'common/comment'
|
import { GroupComment } from 'common/comment'
|
||||||
import { GroupChat } from 'web/components/groups/group-chat'
|
import { GroupChat } from 'web/components/groups/group-chat'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
|
@ -123,7 +123,7 @@ export default function GroupPage(props: {
|
||||||
topTraders: User[]
|
topTraders: User[]
|
||||||
creatorScores: { [userId: string]: number }
|
creatorScores: { [userId: string]: number }
|
||||||
topCreators: User[]
|
topCreators: User[]
|
||||||
messages: Comment[]
|
messages: GroupComment[]
|
||||||
}) {
|
}) {
|
||||||
props = usePropz(props, getStaticPropz) ?? {
|
props = usePropz(props, getStaticPropz) ?? {
|
||||||
group: null,
|
group: null,
|
||||||
|
@ -607,7 +607,9 @@ function AddContractButton(props: { group: Group; user: User }) {
|
||||||
user={user}
|
user={user}
|
||||||
hideOrderSelector={true}
|
hideOrderSelector={true}
|
||||||
onContractClick={addContractToCurrentGroup}
|
onContractClick={addContractToCurrentGroup}
|
||||||
gridClassName="gap-3 space-y-3"
|
overrideGridClassName={
|
||||||
|
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
|
||||||
|
}
|
||||||
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
||||||
additionalFilter={{ excludeContractIds: group.contractIds }}
|
additionalFilter={{ excludeContractIds: group.contractIds }}
|
||||||
highlightOptions={{
|
highlightOptions={{
|
||||||
|
|
|
@ -12,15 +12,18 @@ import { getContractFromSlug } from 'web/lib/firebase/contracts'
|
||||||
import { getUserAndPrivateUser } from 'web/lib/firebase/users'
|
import { getUserAndPrivateUser } 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 { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
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'
|
||||||
|
|
||||||
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
|
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
return { props: { auth: await getUserAndPrivateUser(creds.user.uid) } }
|
const creds = await authenticateOnServer(ctx)
|
||||||
})
|
const auth = creds ? await getUserAndPrivateUser(creds.user.uid) : null
|
||||||
|
return { props: { auth } }
|
||||||
|
}
|
||||||
|
|
||||||
const Home = (props: { auth: { user: User } }) => {
|
const Home = (props: { auth: { user: User } | null }) => {
|
||||||
const { user } = props.auth
|
const user = props.auth ? props.auth.user : null
|
||||||
const [contract, setContract] = useContractPage()
|
const [contract, setContract] = useContractPage()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -42,6 +45,7 @@ const Home = (props: { auth: { user: User } }) => {
|
||||||
// Update the url without switching pages in Nextjs.
|
// Update the url without switching pages in Nextjs.
|
||||||
history.pushState(null, '', `/${c.creatorUsername}/${c.slug}`)
|
history.pushState(null, '', `/${c.creatorUsername}/${c.slug}`)
|
||||||
}}
|
}}
|
||||||
|
isWholePage
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -30,12 +30,6 @@ export default function Home(props: { hotContracts: Contract[] }) {
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="max-w-3xl">
|
<Col className="max-w-3xl">
|
||||||
<LandingPagePanel hotContracts={hotContracts ?? []} />
|
<LandingPagePanel hotContracts={hotContracts ?? []} />
|
||||||
{/* <p className="mt-6 text-gray-500">
|
|
||||||
View{' '}
|
|
||||||
<SiteLink href="/markets" className="font-bold text-gray-700">
|
|
||||||
all markets
|
|
||||||
</SiteLink>
|
|
||||||
</p> */}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { useUser } from 'web/hooks/use-user'
|
|
||||||
import { ContractSearch } from '../components/contract-search'
|
|
||||||
import { Page } from '../components/page'
|
|
||||||
import { SEO } from '../components/SEO'
|
|
||||||
|
|
||||||
// TODO: Rename endpoint to "Explore"
|
|
||||||
export default function Markets() {
|
|
||||||
const user = useUser()
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<SEO
|
|
||||||
title="Explore"
|
|
||||||
description="Discover what's new, trending, or soon-to-close. Or search thousands of prediction markets."
|
|
||||||
url="/markets"
|
|
||||||
/>
|
|
||||||
<ContractSearch user={user} />
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { useUser } from 'web/hooks/use-user'
|
|
||||||
import { ContractSearch } from '../../components/contract-search'
|
|
||||||
import { Page } from '../../components/page'
|
|
||||||
import { Title } from '../../components/title'
|
|
||||||
|
|
||||||
export default function TagPage() {
|
|
||||||
const router = useRouter()
|
|
||||||
const user = useUser()
|
|
||||||
const { tag } = router.query as { tag: string }
|
|
||||||
if (!router.isReady) return <div />
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<Title text={`#${tag}`} />
|
|
||||||
<ContractSearch
|
|
||||||
user={user}
|
|
||||||
defaultSort="newest"
|
|
||||||
defaultFilter="all"
|
|
||||||
additionalFilter={{ tag }}
|
|
||||||
/>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -18,7 +18,9 @@ online = false
|
||||||
firstPrint = false
|
firstPrint = false
|
||||||
flag = true
|
flag = true
|
||||||
page = 1
|
page = 1
|
||||||
|
sets = {}
|
||||||
|
|
||||||
|
window.console.log(sets)
|
||||||
document.location.search.split('&').forEach((pair) => {
|
document.location.search.split('&').forEach((pair) => {
|
||||||
let v = pair.split('=')
|
let v = pair.split('=')
|
||||||
if (v[0] === '?whichguesser') {
|
if (v[0] === '?whichguesser') {
|
||||||
|
@ -32,39 +34,38 @@ document.location.search.split('&').forEach((pair) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let firstFetch = fetch('jsons/' + whichGuesser + page + '.json')
|
if (whichGuesser === 'basic') {
|
||||||
|
fetch('jsons/set.json')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => (sets = data))
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstFetch = fetch('jsons/' + whichGuesser + '.json')
|
||||||
fetchToResponse(firstFetch)
|
fetchToResponse(firstFetch)
|
||||||
|
|
||||||
function putIntoMapAndFetch(data) {
|
function putIntoMapAndFetch(data) {
|
||||||
putIntoMap(data.data)
|
putIntoMap(data.data)
|
||||||
if (data.has_more) {
|
for (const [key, value] of Object.entries(allData)) {
|
||||||
page += 1
|
nameList.push(key)
|
||||||
window.setTimeout(() =>
|
probList.push(
|
||||||
fetchToResponse(fetch('jsons/' + whichGuesser + page + '.json'))
|
value.length + (probList.length === 0 ? 0 : probList[probList.length - 1])
|
||||||
)
|
)
|
||||||
} else {
|
unseenTotal = total
|
||||||
for (const [key, value] of Object.entries(allData)) {
|
|
||||||
nameList.push(key)
|
|
||||||
probList.push(
|
|
||||||
value.length +
|
|
||||||
(probList.length === 0 ? 0 : probList[probList.length - 1])
|
|
||||||
)
|
|
||||||
unseenTotal = total
|
|
||||||
}
|
|
||||||
window.console.log(allData)
|
|
||||||
window.console.log(total)
|
|
||||||
window.console.log(probList)
|
|
||||||
window.console.log(nameList)
|
|
||||||
if (whichGuesser === 'counterspell') {
|
|
||||||
document.getElementById('guess-type').innerText = 'Counterspell Guesser'
|
|
||||||
} else if (whichGuesser === 'burn') {
|
|
||||||
document.getElementById('guess-type').innerText = 'Match With Hot Singles'
|
|
||||||
} else if (whichGuesser === 'beast') {
|
|
||||||
document.getElementById('guess-type').innerText =
|
|
||||||
'Finding Fantastic Beasts'
|
|
||||||
}
|
|
||||||
setUpNewGame()
|
|
||||||
}
|
}
|
||||||
|
window.console.log(allData)
|
||||||
|
window.console.log(total)
|
||||||
|
window.console.log(probList)
|
||||||
|
window.console.log(nameList)
|
||||||
|
if (whichGuesser === 'counterspell') {
|
||||||
|
document.getElementById('guess-type').innerText = 'Counterspell Guesser'
|
||||||
|
} else if (whichGuesser === 'burn') {
|
||||||
|
document.getElementById('guess-type').innerText = 'Match With Hot Singles'
|
||||||
|
} else if (whichGuesser === 'beast') {
|
||||||
|
document.getElementById('guess-type').innerText = 'Finding Fantastic Beasts'
|
||||||
|
} else if (whichGuesser === 'basic') {
|
||||||
|
document.getElementById('guess-type').innerText = 'How Basic'
|
||||||
|
}
|
||||||
|
setUpNewGame()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKSamples() {
|
function getKSamples() {
|
||||||
|
@ -134,11 +135,21 @@ function determineIfSkip(card) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (firstPrint) {
|
if (firstPrint) {
|
||||||
if (
|
if (whichGuesser == 'basic') {
|
||||||
card.reprint === true ||
|
if (
|
||||||
(card.frame_effects && card.frame_effects.includes('showcase'))
|
card.set_type !== 'expansion' &&
|
||||||
) {
|
card.set_type !== 'funny' &&
|
||||||
return true
|
card.set_type !== 'draft_innovation'
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
card.reprint === true ||
|
||||||
|
(card.frame_effects && card.frame_effects.includes('showcase'))
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// reskinned card names show in art crop
|
// reskinned card names show in art crop
|
||||||
|
@ -160,13 +171,16 @@ function putIntoMap(data) {
|
||||||
if (card.card_faces) {
|
if (card.card_faces) {
|
||||||
name = card.card_faces[0].name
|
name = card.card_faces[0].name
|
||||||
}
|
}
|
||||||
|
if (whichGuesser === 'basic') {
|
||||||
|
name =
|
||||||
|
'<img class="symbol" style="width: 17px; height: 17px" src="' +
|
||||||
|
sets[name][1] +
|
||||||
|
'" /> ' +
|
||||||
|
sets[name][0]
|
||||||
|
}
|
||||||
let normalImg = ''
|
let normalImg = ''
|
||||||
if (card.image_uris.normal) {
|
if (card.image_uris.normal) {
|
||||||
normalImg = card.image_uris.normal
|
normalImg = card.image_uris.normal
|
||||||
} else if (card.image_uris.large) {
|
|
||||||
normalImg = card.image_uris.large
|
|
||||||
} else if (card.image_uris.small) {
|
|
||||||
normalImg = card.image_uris.small
|
|
||||||
} else {
|
} else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -211,7 +225,9 @@ function setUpNewGame() {
|
||||||
artDict = sampledData[0]
|
artDict = sampledData[0]
|
||||||
let randomImages = Object.keys(artDict)
|
let randomImages = Object.keys(artDict)
|
||||||
shuffleArray(randomImages)
|
shuffleArray(randomImages)
|
||||||
let namesList = Array.from(sampledData[1]).sort()
|
let namesList = Array.from(sampledData[1]).sort((a, b) =>
|
||||||
|
removeSymbol(a).localeCompare(removeSymbol(b))
|
||||||
|
)
|
||||||
// fill in the new cards and names
|
// fill in the new cards and names
|
||||||
for (let cardIndex = 1; cardIndex <= k; cardIndex++) {
|
for (let cardIndex = 1; cardIndex <= k; cardIndex++) {
|
||||||
let currCard = document.getElementById('card-' + cardIndex)
|
let currCard = document.getElementById('card-' + cardIndex)
|
||||||
|
@ -224,11 +240,16 @@ function setUpNewGame() {
|
||||||
for (nameIndex = 1; nameIndex <= k + extra; nameIndex++) {
|
for (nameIndex = 1; nameIndex <= k + extra; nameIndex++) {
|
||||||
currName = document.getElementById('name-' + nameIndex)
|
currName = document.getElementById('name-' + nameIndex)
|
||||||
// window.console.log(currName)
|
// window.console.log(currName)
|
||||||
currName.innerText = namesList[nameIndex - 1]
|
currName.innerHTML = namesList[nameIndex - 1]
|
||||||
nameBank.appendChild(currName)
|
nameBank.appendChild(currName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeSymbol(name) {
|
||||||
|
let arr = name.split('>')
|
||||||
|
return arr[arr.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
function checkAnswers() {
|
function checkAnswers() {
|
||||||
let score = k
|
let score = k
|
||||||
// show the correct full cards
|
// show the correct full cards
|
||||||
|
@ -236,9 +257,13 @@ function checkAnswers() {
|
||||||
currCard = document.getElementById('card-' + cardIndex)
|
currCard = document.getElementById('card-' + cardIndex)
|
||||||
let incorrect = true
|
let incorrect = true
|
||||||
if (currCard.dataset.name) {
|
if (currCard.dataset.name) {
|
||||||
let guess = document.getElementById(currCard.dataset.name).innerText
|
// remove image text
|
||||||
// window.console.log(artDict[currCard.dataset.url][0], guess);
|
let guess = removeSymbol(
|
||||||
incorrect = artDict[currCard.dataset.url][0] !== guess
|
document.getElementById(currCard.dataset.name).innerText
|
||||||
|
)
|
||||||
|
let ans = removeSymbol(artDict[currCard.dataset.url][0])
|
||||||
|
window.console.log(ans, guess)
|
||||||
|
incorrect = ans !== guess
|
||||||
// decide if their guess was correct
|
// decide if their guess was correct
|
||||||
}
|
}
|
||||||
if (incorrect) currCard.classList.add('incorrect')
|
if (incorrect) currCard.classList.add('incorrect')
|
||||||
|
@ -352,6 +377,10 @@ function dropOnCard(id, data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWordsLeft() {
|
function setWordsLeft() {
|
||||||
|
cardName = 'Unused Card Names: '
|
||||||
|
if (whichGuesser === 'basic') {
|
||||||
|
cardName = 'Unused Set Names: '
|
||||||
|
}
|
||||||
document.getElementById('words-left').innerText =
|
document.getElementById('words-left').innerText =
|
||||||
'Unused Card Names: ' + wordsLeft + '/Images: ' + imagesLeft
|
cardName + wordsLeft + '/Images: ' + imagesLeft
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.answer-page .card {
|
.answer-page .card {
|
||||||
height: 350px;
|
height: 353px;
|
||||||
/*padding-top: 310px;*/
|
/*padding-top: 310px;*/
|
||||||
/*background-size: cover;*/
|
/*background-size: cover;*/
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -253,16 +253,6 @@
|
||||||
.name {
|
.name {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
|
||||||
width: 300px;
|
|
||||||
background-size: 300px;
|
|
||||||
height: 266px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.answer-page .card {
|
|
||||||
height: 454px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -3,7 +3,8 @@ import requests
|
||||||
import json
|
import json
|
||||||
|
|
||||||
# add category name here
|
# add category name here
|
||||||
allCategories = ['counterspell', 'beast', 'terror', 'wrath', 'burn']
|
allCategories = ['counterspell', 'beast', 'burn'] #, 'terror', 'wrath']
|
||||||
|
specialCategories = ['set', 'basic']
|
||||||
|
|
||||||
|
|
||||||
def generate_initial_query(category):
|
def generate_initial_query(category):
|
||||||
|
@ -12,11 +13,11 @@ def generate_initial_query(category):
|
||||||
string_query += 'otag%3Acounterspell+t%3Ainstant+not%3Aadventure'
|
string_query += 'otag%3Acounterspell+t%3Ainstant+not%3Aadventure'
|
||||||
elif category == 'beast':
|
elif category == 'beast':
|
||||||
string_query += '-type%3Alegendary+type%3Abeast+-type%3Atoken'
|
string_query += '-type%3Alegendary+type%3Abeast+-type%3Atoken'
|
||||||
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'
|
||||||
elif category == 'wrath':
|
# elif category == 'wrath':
|
||||||
string_query += 'otag%3Asweeper-creature+%28t%3Ainstant+or+t%3Asorcery%29+not%3Aadventure'
|
# string_query += 'otag%3Asweeper-creature+%28t%3Ainstant+or+t%3Asorcery%29+not%3Aadventure'
|
||||||
elif category == 'burn':
|
elif category == 'burn':
|
||||||
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' \
|
||||||
|
@ -24,9 +25,19 @@ def generate_initial_query(category):
|
||||||
'+%28type%3Ainstant+or+type%3Asorcery%29+not%3Aadventure'
|
'+%28type%3Ainstant+or+type%3Asorcery%29+not%3Aadventure'
|
||||||
# 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%3Adfc+not%3Asplit+-set%3Acmb2+-set%3Acmb1+-set%3Aplist+-set%3Adbl' \
|
||||||
'+-frame%3Aextendedart+language%3Aenglish&unique=art&page='
|
'+language%3Aenglish&order=released&dir=asc&unique=prints&page='
|
||||||
|
print(string_query)
|
||||||
|
return string_query
|
||||||
|
|
||||||
|
def generate_initial_special_query(category):
|
||||||
|
string_query = 'https://api.scryfall.com/cards/search?q='
|
||||||
|
if category == 'set':
|
||||||
|
return 'https://api.scryfall.com/sets'
|
||||||
|
elif category == 'basic':
|
||||||
|
string_query += 't%3Abasic&order=released&dir=asc&unique=prints&page='
|
||||||
|
# add category string query here
|
||||||
print(string_query)
|
print(string_query)
|
||||||
return string_query
|
return string_query
|
||||||
|
|
||||||
|
@ -34,31 +45,60 @@ def generate_initial_query(category):
|
||||||
def fetch_and_write_all(category, query):
|
def fetch_and_write_all(category, query):
|
||||||
count = 1
|
count = 1
|
||||||
will_repeat = True
|
will_repeat = True
|
||||||
|
all_cards = {'data' : []}
|
||||||
|
art_names = set()
|
||||||
while will_repeat:
|
while will_repeat:
|
||||||
will_repeat = fetch_and_write(category, query, count)
|
response = fetch(query, count)
|
||||||
count += 1
|
will_repeat = response['has_more']
|
||||||
|
count+=1
|
||||||
|
to_compact_write_form(all_cards, art_names, response, category)
|
||||||
|
|
||||||
|
with open('jsons/' + category + '.json', 'w') as f:
|
||||||
|
json.dump(all_cards, f)
|
||||||
|
|
||||||
|
|
||||||
def fetch_and_write(category, query, count):
|
def fetch_and_write_all_special(category, query):
|
||||||
|
count = 1
|
||||||
|
will_repeat = True
|
||||||
|
all_cards = {'data' : []}
|
||||||
|
art_names = set()
|
||||||
|
while will_repeat:
|
||||||
|
if category == 'set':
|
||||||
|
response = fetch_special(query)
|
||||||
|
else:
|
||||||
|
response = fetch(query, count)
|
||||||
|
will_repeat = response['has_more']
|
||||||
|
count+=1
|
||||||
|
to_compact_write_form_special(all_cards, art_names, response, category)
|
||||||
|
|
||||||
|
with open('jsons/' + category + '.json', 'w') as f:
|
||||||
|
json.dump(all_cards, f)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(query, count):
|
||||||
query += str(count)
|
query += str(count)
|
||||||
response = requests.get(f"{query}").json()
|
response = requests.get(f"{query}").json()
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
with open('jsons/' + category + str(count) + '.json', 'w') as f:
|
return response
|
||||||
json.dump(to_compact_write_form(response), f)
|
|
||||||
return response['has_more']
|
def fetch_special(query):
|
||||||
|
response = requests.get(f"{query}").json()
|
||||||
|
time.sleep(0.1)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def to_compact_write_form(response):
|
def to_compact_write_form(smallJson, art_names, response, category):
|
||||||
fieldsToUse = ['has_more']
|
|
||||||
fieldsInCard = ['name', 'image_uris', 'content_warning', 'flavor_name', 'reprint', 'frame_effects', 'digital',
|
fieldsInCard = ['name', 'image_uris', 'content_warning', 'flavor_name', 'reprint', 'frame_effects', 'digital',
|
||||||
'set_type']
|
'set_type']
|
||||||
smallJson = dict()
|
|
||||||
data = []
|
data = []
|
||||||
# write all fields needed in response
|
|
||||||
for field in fieldsToUse:
|
|
||||||
smallJson[field] = response[field]
|
|
||||||
# write all fields needed in card
|
# write all fields needed in card
|
||||||
for card in response['data']:
|
for card in response['data']:
|
||||||
|
# do not repeat art
|
||||||
|
if 'illustration_id' not in card or card['illustration_id'] in art_names:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
art_names.add(card['illustration_id'])
|
||||||
write_card = dict()
|
write_card = dict()
|
||||||
for field in fieldsInCard:
|
for field in fieldsInCard:
|
||||||
if field == 'name' and 'card_faces' in card:
|
if field == 'name' and 'card_faces' in card:
|
||||||
|
@ -68,8 +108,33 @@ def to_compact_write_form(response):
|
||||||
elif field in card:
|
elif field in card:
|
||||||
write_card[field] = card[field]
|
write_card[field] = card[field]
|
||||||
data.append(write_card)
|
data.append(write_card)
|
||||||
smallJson['data'] = data
|
smallJson['data'] += data
|
||||||
return smallJson
|
|
||||||
|
def to_compact_write_form_special(smallJson, art_names, response, category):
|
||||||
|
fieldsInBasic = ['image_uris', 'set', 'set_type', 'digital']
|
||||||
|
data = []
|
||||||
|
# write all fields needed in card
|
||||||
|
for card in response['data']:
|
||||||
|
if category == 'basic':
|
||||||
|
write_card = dict()
|
||||||
|
# do not repeat art
|
||||||
|
if 'illustration_id' not in card or card['illustration_id'] in art_names:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
art_names.add(card['illustration_id'])
|
||||||
|
for field in fieldsInBasic:
|
||||||
|
if field == 'image_uris':
|
||||||
|
write_card['image_uris'] = write_image_uris(card['image_uris'])
|
||||||
|
elif field == 'set':
|
||||||
|
write_card['name'] = card['set']
|
||||||
|
elif field in card:
|
||||||
|
write_card[field] = card[field]
|
||||||
|
data.append(write_card)
|
||||||
|
else:
|
||||||
|
if card['set_type'] != 'token':
|
||||||
|
smallJson[card['code']] = [card['name'],card['icon_svg_uri']]
|
||||||
|
|
||||||
|
smallJson['data'] += data
|
||||||
|
|
||||||
|
|
||||||
# only write images needed
|
# only write images needed
|
||||||
|
@ -87,6 +152,9 @@ def write_image_uris(card_image_uris):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
for category in allCategories:
|
# for category in allCategories:
|
||||||
|
# print(category)
|
||||||
|
# fetch_and_write_all(category, generate_initial_query(category))
|
||||||
|
for category in specialCategories:
|
||||||
print(category)
|
print(category)
|
||||||
fetch_and_write_all(category, generate_initial_query(category))
|
fetch_and_write_all_special(category, generate_initial_special_query(category))
|
||||||
|
|
|
@ -159,6 +159,16 @@
|
||||||
>
|
>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
<input type="radio" id="basic" name="whichguesser" value="basic" />
|
||||||
|
<label class="radio-label" for="basic">
|
||||||
|
<img
|
||||||
|
class="thumbnail"
|
||||||
|
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/0/3/03683fbb-9843-4c14-bb95-387150e97c90.jpg?1642161346"
|
||||||
|
/>
|
||||||
|
<h3>How Basic</h3></label
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
|
||||||
<details id="addl-options">
|
<details id="addl-options">
|
||||||
<summary>
|
<summary>
|
||||||
<img
|
<img
|
||||||
|
@ -174,7 +184,7 @@
|
||||||
<label for="un">include un-cards</label>
|
<label for="un">include un-cards</label>
|
||||||
<br />
|
<br />
|
||||||
<input type="checkbox" name="original" id="original" />
|
<input type="checkbox" name="original" id="original" />
|
||||||
<label for="original">restrict to only original printing</label>
|
<label for="original">only original set printing</label>
|
||||||
</details>
|
</details>
|
||||||
<input type="submit" id="submit" value="Play" />
|
<input type="submit" id="submit" value="Play" />
|
||||||
</form>
|
</form>
|
||||||
|
|
1
web/public/mtg/jsons/basic.json
Normal file
1
web/public/mtg/jsons/basic.json
Normal file
File diff suppressed because one or more lines are too long
1
web/public/mtg/jsons/beast.json
Normal file
1
web/public/mtg/jsons/beast.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/burn.json
Normal file
1
web/public/mtg/jsons/burn.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/counterspell.json
Normal file
1
web/public/mtg/jsons/counterspell.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/set.json
Normal file
1
web/public/mtg/jsons/set.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
||||||
<url><loc>https://manifold.markets</loc><changefreq>hourly</changefreq><priority>1.0</priority></url>
|
<url><loc>https://manifold.markets</loc><changefreq>hourly</changefreq><priority>1.0</priority></url>
|
||||||
<url><loc>https://manifold.markets/markets</loc><changefreq>hourly</changefreq><priority>0.2</priority></url>
|
<url><loc>https://manifold.markets/home</loc><changefreq>hourly</changefreq><priority>0.2</priority></url>
|
||||||
<url><loc>https://manifold.markets/leaderboards</loc><changefreq>daily</changefreq><priority>0.2</priority></url>
|
<url><loc>https://manifold.markets/leaderboards</loc><changefreq>daily</changefreq><priority>0.2</priority></url>
|
||||||
</urlset>
|
</urlset>
|
||||||
|
|
|
@ -5764,6 +5764,11 @@ eslint-config-next@12.1.6:
|
||||||
eslint-plugin-react "^7.29.4"
|
eslint-plugin-react "^7.29.4"
|
||||||
eslint-plugin-react-hooks "^4.5.0"
|
eslint-plugin-react-hooks "^4.5.0"
|
||||||
|
|
||||||
|
eslint-config-prettier@8.5.0:
|
||||||
|
version "8.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1"
|
||||||
|
integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==
|
||||||
|
|
||||||
eslint-import-resolver-node@^0.3.6:
|
eslint-import-resolver-node@^0.3.6:
|
||||||
version "0.3.6"
|
version "0.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd"
|
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user