Merge branch 'main' into CPM-ui
This commit is contained in:
commit
1ebd46d49d
27
.github/CONTRIBUTING.md
vendored
Normal file
27
.github/CONTRIBUTING.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Manifold CLA
|
||||||
|
|
||||||
|
**Manifold Markets Contributor License Agreement**
|
||||||
|
|
||||||
|
(Thanks to [Beeminder](http://bmndr.co/cla) and [Discourse.org](https://cla-assistant.io/discourse/discourse) whose CLAs we modeled this on!)
|
||||||
|
|
||||||
|
## Unofficial Summary
|
||||||
|
|
||||||
|
- Manifold can use your contributions
|
||||||
|
- Manifold can sell things involving your contributions
|
||||||
|
- You’re legally able to agree to the above
|
||||||
|
- You’re the one who created these contributions
|
||||||
|
- Manifold decides what gets included in Manifold
|
||||||
|
- Manifold does not promise any support
|
||||||
|
|
||||||
|
## Official Agreement
|
||||||
|
|
||||||
|
The document below clarifies the terms under which You (the copyright owner or legal entity authorized by the copyright owner), may make "The Contributions" (software, bug fixes, configuration changes, documentation, or any other materials) to "The Work" (Manifold Markets). This license protects You, "The Company" (Manifold Markets, Inc.) and licensees; it does not change your rights to use your own contributions for any other purpose.
|
||||||
|
|
||||||
|
You and "The Company" (Manifold Markets, Inc.) agree:
|
||||||
|
|
||||||
|
- You grant to "The Company" (Manifold Markets, Inc.) a non-exclusive, irrevocable, worldwide, royalty-free, sublicenseable, relicenseable, transferable license under all of Your relevant intellectual property rights, to use, copy, prepare derivative works of, distribute and publicly perform and display "The Contributions" on any licensing terms, including without limitation: (a) open source licenses like the GNU General Public (v2.0) license; and (b) binary, proprietary, or commercial licenses. Except for the licenses granted herein, You reserve all right, title, and interest in and to "The Contributions".
|
||||||
|
- You grant to "The Company" a non-exclusive, irrevocable (except as stated in this section), worldwide, royalty-free, sublicenseable, transferable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer "The Work", where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with "The Work" to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or "The Work" to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
|
||||||
|
- You are able to grant us these rights. You represent that You are legally entitled to grant the above license(s). If Your employer has rights to intellectual property that You create, You represent that You have received permission to make "The Contributions" on behalf of that employer, or that Your employer has waived such rights for "The Contributions".
|
||||||
|
- "The Contributions" are your original work. You represent that "The Contributions" are Your original works of authorship, and to Your knowledge, no other person claims, or has the right to claim, any right in any invention or patent related to "The Contributions". You also represent that You are not legally obligated, whether by entering into an agreement or otherwise, in any way that conflicts with the terms of this license. For example, if you have signed an agreement requiring you to assign the intellectual property rights in "The Contributions" to an employer or customer, that would conflict with the terms of this license.
|
||||||
|
- We, as authoritative representatives of "The Company" determine the code that is in "The Work". You understand that the decision to include "The Contribution(s)" in any project or source repository is entirely that of "The Company", and this agreement does not guarantee that "The Contributions" will be included in any product.
|
||||||
|
- No Implied Warranties. "The Company" acknowledges that, except as explicitly described in this Agreement, the Contribution is provided on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
|
|
@ -37,6 +37,8 @@ Also: Our docs are currently in [a separate repo](https://github.com/manifoldmar
|
||||||
|
|
||||||
Since we are just now open-sourcing things, we will see how things go. Feel free to open issues, submit PRs, and chat about the process on [Discord][discord]. We would prefer [small PRs][small-prs] that we can effectively evaluate and review -- maybe check in with us first if you are thinking to work on a big change.
|
Since we are just now open-sourcing things, we will see how things go. Feel free to open issues, submit PRs, and chat about the process on [Discord][discord]. We would prefer [small PRs][small-prs] that we can effectively evaluate and review -- maybe check in with us first if you are thinking to work on a big change.
|
||||||
|
|
||||||
|
By contributing to this codebase, you are agreeing to the terms of the [Manifold CLA](https://github.com/manifoldmarkets/manifold/blob/main/.github/CONTRIBUTING.md).
|
||||||
|
|
||||||
If you need additional access to any infrastructure in order to work on something (e.g. Vercel, Firebase) let us know about that on Discord as well.
|
If you need additional access to any infrastructure in order to work on something (e.g. Vercel, Firebase) let us know about that on Discord as well.
|
||||||
|
|
||||||
[vercel]: https://vercel.com/
|
[vercel]: https://vercel.com/
|
||||||
|
|
|
@ -5,14 +5,16 @@ import { User } from './user'
|
||||||
import { LiquidityProvision } from './liquidity-provision'
|
import { LiquidityProvision } from './liquidity-provision'
|
||||||
import { noFees } from './fees'
|
import { noFees } from './fees'
|
||||||
|
|
||||||
export const FIXED_ANTE = 50
|
export const FIXED_ANTE = 100
|
||||||
|
|
||||||
// deprecated
|
// deprecated
|
||||||
export const PHANTOM_ANTE = 0.001
|
export const PHANTOM_ANTE = 0.001
|
||||||
export const MINIMUM_ANTE = 50
|
export const MINIMUM_ANTE = 50
|
||||||
|
|
||||||
|
export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id
|
||||||
|
|
||||||
export function getCpmmInitialLiquidity(
|
export function getCpmmInitialLiquidity(
|
||||||
creator: User,
|
providerId: string,
|
||||||
contract: FullContract<CPMM, Binary>,
|
contract: FullContract<CPMM, Binary>,
|
||||||
anteId: string,
|
anteId: string,
|
||||||
amount: number
|
amount: number
|
||||||
|
@ -21,7 +23,7 @@ export function getCpmmInitialLiquidity(
|
||||||
|
|
||||||
const lp: LiquidityProvision = {
|
const lp: LiquidityProvision = {
|
||||||
id: anteId,
|
id: anteId,
|
||||||
userId: creator.id,
|
userId: providerId,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
createdTime,
|
createdTime,
|
||||||
isAnte: true,
|
isAnte: true,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { chargeUser, getUser } from './utils'
|
import { chargeUser, getUser } from './utils'
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
|
@ -23,6 +22,7 @@ import {
|
||||||
getAnteBets,
|
getAnteBets,
|
||||||
getCpmmInitialLiquidity,
|
getCpmmInitialLiquidity,
|
||||||
getFreeAnswerAnte,
|
getFreeAnswerAnte,
|
||||||
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
MINIMUM_ANTE,
|
MINIMUM_ANTE,
|
||||||
} from '../../common/antes'
|
} from '../../common/antes'
|
||||||
import { getNoneAnswer } from '../../common/answer'
|
import { getNoneAnswer } from '../../common/answer'
|
||||||
|
@ -73,11 +73,19 @@ export const createContract = functions
|
||||||
return { status: 'error', message: 'Invalid initial probability' }
|
return { status: 'error', message: 'Invalid initial probability' }
|
||||||
|
|
||||||
const ante = FIXED_ANTE // data.ante
|
const ante = FIXED_ANTE // data.ante
|
||||||
|
// uses utc time on server:
|
||||||
|
const today = new Date().setHours(0, 0, 0, 0)
|
||||||
|
const userContractsCreatedTodaySnapshot = await firestore
|
||||||
|
.collection(`contracts`)
|
||||||
|
.where('creatorId', '==', userId)
|
||||||
|
.where('createdTime', '>=', today)
|
||||||
|
.get()
|
||||||
|
const isFree = userContractsCreatedTodaySnapshot.size === 0
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ante === undefined ||
|
ante === undefined ||
|
||||||
ante < MINIMUM_ANTE ||
|
ante < MINIMUM_ANTE ||
|
||||||
ante > creator.balance ||
|
(ante > creator.balance && !isFree) ||
|
||||||
isNaN(ante) ||
|
isNaN(ante) ||
|
||||||
!isFinite(ante)
|
!isFinite(ante)
|
||||||
)
|
)
|
||||||
|
@ -109,7 +117,7 @@ export const createContract = functions
|
||||||
tags ?? []
|
tags ?? []
|
||||||
)
|
)
|
||||||
|
|
||||||
if (ante) await chargeUser(creator.id, ante)
|
if (!isFree && ante) await chargeUser(creator.id, ante)
|
||||||
|
|
||||||
await contractRef.create(contract)
|
await contractRef.create(contract)
|
||||||
|
|
||||||
|
@ -137,8 +145,10 @@ export const createContract = functions
|
||||||
.collection(`contracts/${contract.id}/liquidity`)
|
.collection(`contracts/${contract.id}/liquidity`)
|
||||||
.doc()
|
.doc()
|
||||||
|
|
||||||
|
const providerId = isFree ? HOUSE_LIQUIDITY_PROVIDER_ID : creator.id
|
||||||
|
|
||||||
const lp = getCpmmInitialLiquidity(
|
const lp = getCpmmInitialLiquidity(
|
||||||
creator,
|
providerId,
|
||||||
contract as FullContract<CPMM, Binary>,
|
contract as FullContract<CPMM, Binary>,
|
||||||
liquidityDoc.id,
|
liquidityDoc.id,
|
||||||
ante
|
ante
|
||||||
|
|
|
@ -106,7 +106,7 @@ async function recalculateContract(contractRef: DocRef, isCommit = false) {
|
||||||
const liquidityDocRef = contractRef.collection('liquidity').doc()
|
const liquidityDocRef = contractRef.collection('liquidity').doc()
|
||||||
|
|
||||||
const lp = getCpmmInitialLiquidity(
|
const lp = getCpmmInitialLiquidity(
|
||||||
{ id: 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' } as User, // use @ManifoldMarkets' id
|
'IPTOzEqrpkWmEzh6hwvAyY9PqFb2', // use @ManifoldMarkets' id
|
||||||
{
|
{
|
||||||
...contract,
|
...contract,
|
||||||
...contractUpdate,
|
...contractUpdate,
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
useQueryAndSortParams,
|
useQueryAndSortParams,
|
||||||
} from '../../hooks/use-sort-and-query-params'
|
} from '../../hooks/use-sort-and-query-params'
|
||||||
import { Answer } from '../../../common/answer'
|
import { Answer } from '../../../common/answer'
|
||||||
|
import { LoadingIndicator } from '../loading-indicator'
|
||||||
|
|
||||||
export function ContractsGrid(props: {
|
export function ContractsGrid(props: {
|
||||||
contracts: Contract[]
|
contracts: Contract[]
|
||||||
|
@ -213,7 +214,7 @@ function TagContractsGrid(props: { contracts: Contract[] }) {
|
||||||
const MAX_CONTRACTS_DISPLAYED = 99
|
const MAX_CONTRACTS_DISPLAYED = 99
|
||||||
|
|
||||||
export function SearchableGrid(props: {
|
export function SearchableGrid(props: {
|
||||||
contracts: Contract[]
|
contracts: Contract[] | undefined
|
||||||
byOneCreator?: boolean
|
byOneCreator?: boolean
|
||||||
querySortOptions?: {
|
querySortOptions?: {
|
||||||
defaultSort: Sort
|
defaultSort: Sort
|
||||||
|
@ -230,7 +231,7 @@ export function SearchableGrid(props: {
|
||||||
return queryWords.every((word) => corpus.toLowerCase().includes(word))
|
return queryWords.every((word) => corpus.toLowerCase().includes(word))
|
||||||
}
|
}
|
||||||
|
|
||||||
let matches = contracts.filter(
|
let matches = (contracts ?? []).filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
check(c.question) ||
|
check(c.question) ||
|
||||||
check(c.description) ||
|
check(c.description) ||
|
||||||
|
@ -324,7 +325,9 @@ export function SearchableGrid(props: {
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sort === 'tag' ? (
|
{contracts === undefined ? (
|
||||||
|
<LoadingIndicator />
|
||||||
|
) : sort === 'tag' ? (
|
||||||
<TagContractsGrid contracts={matches} />
|
<TagContractsGrid contracts={matches} />
|
||||||
) : !byOneCreator && sort === 'creator' ? (
|
) : !byOneCreator && sort === 'creator' ? (
|
||||||
<CreatorContractsGrid contracts={matches} />
|
<CreatorContractsGrid contracts={matches} />
|
||||||
|
|
|
@ -141,7 +141,7 @@ export default function FeedCreate(props: {
|
||||||
{/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
|
{/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
|
||||||
{!isExpanded && (
|
{!isExpanded && (
|
||||||
<div className="flex justify-end sm:-mt-4">
|
<div className="flex justify-end sm:-mt-4">
|
||||||
<button className="btn btn-sm" disabled>
|
<button className="btn btn-sm capitalize" disabled>
|
||||||
Create Market
|
Create Market
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,6 +31,8 @@ type BaseActivityItem = {
|
||||||
|
|
||||||
export type CommentInputItem = BaseActivityItem & {
|
export type CommentInputItem = BaseActivityItem & {
|
||||||
type: 'commentInput'
|
type: 'commentInput'
|
||||||
|
betsByCurrentUser: Bet[]
|
||||||
|
comments: Comment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DescriptionItem = BaseActivityItem & {
|
export type DescriptionItem = BaseActivityItem & {
|
||||||
|
@ -48,12 +50,13 @@ export type BetItem = BaseActivityItem & {
|
||||||
bet: Bet
|
bet: Bet
|
||||||
hideOutcome: boolean
|
hideOutcome: boolean
|
||||||
smallAvatar: boolean
|
smallAvatar: boolean
|
||||||
|
hideComment?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommentItem = BaseActivityItem & {
|
export type CommentItem = BaseActivityItem & {
|
||||||
type: 'comment'
|
type: 'comment'
|
||||||
comment: Comment
|
comment: Comment
|
||||||
bet: Bet | undefined
|
betsBySameUser: Bet[]
|
||||||
hideOutcome: boolean
|
hideOutcome: boolean
|
||||||
truncate: boolean
|
truncate: boolean
|
||||||
smallAvatar: boolean
|
smallAvatar: boolean
|
||||||
|
@ -129,7 +132,7 @@ function groupBets(
|
||||||
type: 'comment' as const,
|
type: 'comment' as const,
|
||||||
id: bet.id,
|
id: bet.id,
|
||||||
comment,
|
comment,
|
||||||
bet,
|
betsBySameUser: [bet],
|
||||||
contract,
|
contract,
|
||||||
hideOutcome,
|
hideOutcome,
|
||||||
truncate: abbreviated,
|
truncate: abbreviated,
|
||||||
|
@ -280,7 +283,7 @@ function groupBetsAndComments(
|
||||||
id: comment.id,
|
id: comment.id,
|
||||||
contract: contract,
|
contract: contract,
|
||||||
comment,
|
comment,
|
||||||
bet: undefined,
|
betsBySameUser: [],
|
||||||
truncate: abbreviated,
|
truncate: abbreviated,
|
||||||
hideOutcome: true,
|
hideOutcome: true,
|
||||||
smallAvatar,
|
smallAvatar,
|
||||||
|
@ -308,6 +311,27 @@ function groupBetsAndComments(
|
||||||
return abbrItems
|
return abbrItems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCommentsWithPositions(
|
||||||
|
bets: Bet[],
|
||||||
|
comments: Comment[],
|
||||||
|
contract: Contract
|
||||||
|
) {
|
||||||
|
const betsByUserId = _.groupBy(bets, (bet) => bet.userId)
|
||||||
|
|
||||||
|
const items = comments.map((comment) => ({
|
||||||
|
type: 'comment' as const,
|
||||||
|
id: comment.id,
|
||||||
|
contract: contract,
|
||||||
|
comment,
|
||||||
|
betsBySameUser: bets.length === 0 ? [] : betsByUserId[comment.userId] ?? [],
|
||||||
|
truncate: true,
|
||||||
|
hideOutcome: false,
|
||||||
|
smallAvatar: false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
export function getAllContractActivityItems(
|
export function getAllContractActivityItems(
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
|
@ -361,6 +385,8 @@ export function getAllContractActivityItems(
|
||||||
type: 'commentInput',
|
type: 'commentInput',
|
||||||
id: 'commentInput',
|
id: 'commentInput',
|
||||||
contract,
|
contract,
|
||||||
|
betsByCurrentUser: [],
|
||||||
|
comments: [],
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
items.push(
|
items.push(
|
||||||
|
@ -385,6 +411,8 @@ export function getAllContractActivityItems(
|
||||||
type: 'commentInput',
|
type: 'commentInput',
|
||||||
id: 'commentInput',
|
id: 'commentInput',
|
||||||
contract,
|
contract,
|
||||||
|
betsByCurrentUser: [],
|
||||||
|
comments: [],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,24 +460,13 @@ export function getRecentContractActivityItems(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const onlyUsersBetsOrBetsWithComments = bets.filter((bet) =>
|
|
||||||
comments.some(
|
|
||||||
(comment) => comment.betId === bet.id || bet.userId === user?.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
items.push(
|
items.push(
|
||||||
...groupBetsAndComments(
|
...groupBetsAndComments(bets, comments, contract, user?.id, {
|
||||||
onlyUsersBetsOrBetsWithComments,
|
hideOutcome: false,
|
||||||
comments,
|
abbreviated: true,
|
||||||
contract,
|
smallAvatar: false,
|
||||||
user?.id,
|
reversed: true,
|
||||||
{
|
})
|
||||||
hideOutcome: false,
|
|
||||||
abbreviated: true,
|
|
||||||
smallAvatar: false,
|
|
||||||
reversed: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -471,37 +488,29 @@ export function getSpecificContractActivityItems(
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'bets':
|
case 'bets':
|
||||||
items.push(
|
items.push(
|
||||||
...groupBets(bets, comments, contract, user?.id, {
|
...bets.map((bet) => ({
|
||||||
|
type: 'bet' as const,
|
||||||
|
id: bet.id,
|
||||||
|
bet,
|
||||||
|
contract,
|
||||||
hideOutcome: false,
|
hideOutcome: false,
|
||||||
abbreviated: false,
|
|
||||||
smallAvatar: false,
|
smallAvatar: false,
|
||||||
reversed: false,
|
hideComment: true,
|
||||||
})
|
}))
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'comments':
|
case 'comments':
|
||||||
const onlyBetsWithComments = bets.filter((bet) =>
|
items.push(...getCommentsWithPositions(bets, comments, contract))
|
||||||
comments.some((comment) => comment.betId === bet.id)
|
|
||||||
)
|
|
||||||
items.push(
|
|
||||||
...groupBetsAndComments(
|
|
||||||
onlyBetsWithComments,
|
|
||||||
comments,
|
|
||||||
contract,
|
|
||||||
user?.id,
|
|
||||||
{
|
|
||||||
hideOutcome: false,
|
|
||||||
abbreviated: false,
|
|
||||||
smallAvatar: false,
|
|
||||||
reversed: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
items.push({
|
items.push({
|
||||||
type: 'commentInput',
|
type: 'commentInput',
|
||||||
id: 'commentInput',
|
id: 'commentInput',
|
||||||
contract,
|
contract,
|
||||||
|
betsByCurrentUser: user
|
||||||
|
? bets.filter((bet) => bet.userId === user.id)
|
||||||
|
: [],
|
||||||
|
comments: comments,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,13 @@ import BetRow from '../bet-row'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
import { Answer } from '../../../common/answer'
|
import { Answer } from '../../../common/answer'
|
||||||
import { ActivityItem } from './activity-items'
|
import { ActivityItem } from './activity-items'
|
||||||
import { FreeResponse, FullContract } from '../../../common/contract'
|
import {
|
||||||
|
Binary,
|
||||||
|
CPMM,
|
||||||
|
DPM,
|
||||||
|
FreeResponse,
|
||||||
|
FullContract,
|
||||||
|
} from '../../../common/contract'
|
||||||
import { BuyButton } from '../yes-no-selector'
|
import { BuyButton } from '../yes-no-selector'
|
||||||
import { getDpmOutcomeProbability } from '../../../common/calculate-dpm'
|
import { getDpmOutcomeProbability } from '../../../common/calculate-dpm'
|
||||||
import { AnswerBetPanel } from '../answers/answer-bet-panel'
|
import { AnswerBetPanel } from '../answers/answer-bet-panel'
|
||||||
|
@ -50,6 +56,7 @@ import { trackClick } from '../../lib/firebase/tracking'
|
||||||
import { firebaseLogin } from '../../lib/firebase/users'
|
import { firebaseLogin } from '../../lib/firebase/users'
|
||||||
import { DAY_MS } from '../../../common/util/time'
|
import { DAY_MS } from '../../../common/util/time'
|
||||||
import NewContractBadge from '../new-contract-badge'
|
import NewContractBadge from '../new-contract-badge'
|
||||||
|
import { calculateCpmmSale } from '../../../common/calculate-cpmm'
|
||||||
|
|
||||||
export function FeedItems(props: {
|
export function FeedItems(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -123,21 +130,38 @@ function FeedItem(props: { item: ActivityItem }) {
|
||||||
export function FeedComment(props: {
|
export function FeedComment(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
comment: Comment
|
comment: Comment
|
||||||
bet: Bet | undefined
|
betsBySameUser: Bet[]
|
||||||
hideOutcome: boolean
|
hideOutcome: boolean
|
||||||
truncate: boolean
|
truncate: boolean
|
||||||
smallAvatar: boolean
|
smallAvatar: boolean
|
||||||
}) {
|
}) {
|
||||||
const { contract, comment, bet, hideOutcome, truncate, smallAvatar } = props
|
const {
|
||||||
let money: string | undefined
|
contract,
|
||||||
let outcome: string | undefined
|
comment,
|
||||||
let bought: string | undefined
|
betsBySameUser,
|
||||||
if (bet) {
|
hideOutcome,
|
||||||
outcome = bet.outcome
|
truncate,
|
||||||
bought = bet.amount >= 0 ? 'bought' : 'sold'
|
smallAvatar,
|
||||||
money = formatMoney(Math.abs(bet.amount))
|
} = props
|
||||||
}
|
|
||||||
const { text, userUsername, userName, userAvatarUrl, createdTime } = comment
|
const { text, userUsername, userName, userAvatarUrl, createdTime } = comment
|
||||||
|
let outcome: string | undefined,
|
||||||
|
bought: string | undefined,
|
||||||
|
money: string | undefined
|
||||||
|
|
||||||
|
const matchedBet = betsBySameUser.find((bet) => bet.id === comment.betId)
|
||||||
|
if (matchedBet) {
|
||||||
|
outcome = matchedBet.outcome
|
||||||
|
bought = matchedBet.amount >= 0 ? 'bought' : 'sold'
|
||||||
|
money = formatMoney(Math.abs(matchedBet.amount))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only calculated if they don't have a matching bet
|
||||||
|
const { userPosition, userPositionMoney, yesFloorShares, noFloorShares } =
|
||||||
|
getBettorsPosition(
|
||||||
|
contract,
|
||||||
|
comment.createdTime,
|
||||||
|
matchedBet ? [] : betsBySameUser
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -155,18 +179,33 @@ export function FeedComment(props: {
|
||||||
username={userUsername}
|
username={userUsername}
|
||||||
name={userName}
|
name={userName}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{bought} {money}
|
{!matchedBet && userPosition > 0 && (
|
||||||
{!hideOutcome && (
|
|
||||||
<>
|
<>
|
||||||
{' '}
|
{'with ' + userPositionMoney + ' '}
|
||||||
of{' '}
|
<>
|
||||||
<OutcomeLabel
|
{' of '}
|
||||||
outcome={outcome ? outcome : ''}
|
<OutcomeLabel
|
||||||
contract={contract}
|
outcome={yesFloorShares > noFloorShares ? 'YES' : 'NO'}
|
||||||
truncate="short"
|
contract={contract}
|
||||||
/>
|
truncate="short"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<>
|
||||||
|
{bought} {money}
|
||||||
|
{outcome && !hideOutcome && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
of{' '}
|
||||||
|
<OutcomeLabel
|
||||||
|
outcome={outcome ? outcome : ''}
|
||||||
|
contract={contract}
|
||||||
|
truncate="short"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
<RelativeTimestamp time={createdTime} />
|
<RelativeTimestamp time={createdTime} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -180,20 +219,12 @@ export function FeedComment(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RelativeTimestamp(props: { time: number }) {
|
export function CommentInput(props: {
|
||||||
const { time } = props
|
contract: Contract
|
||||||
return (
|
betsByCurrentUser: Bet[]
|
||||||
<DateTimeTooltip time={time}>
|
comments: Comment[]
|
||||||
<span className="ml-1 whitespace-nowrap text-gray-400">
|
}) {
|
||||||
{fromNow(time)}
|
const { contract, betsByCurrentUser, comments } = props
|
||||||
</span>
|
|
||||||
</DateTimeTooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CommentInput(props: { contract: Contract }) {
|
|
||||||
// see if we can comment input on any bet:
|
|
||||||
const { contract } = props
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const [comment, setComment] = useState('')
|
const [comment, setComment] = useState('')
|
||||||
|
|
||||||
|
@ -206,14 +237,50 @@ export function CommentInput(props: { contract: Contract }) {
|
||||||
setComment('')
|
setComment('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should this be oldest bet or most recent bet?
|
||||||
|
const mostRecentCommentableBet = betsByCurrentUser
|
||||||
|
.filter(
|
||||||
|
(bet) =>
|
||||||
|
canCommentOnBet(bet.userId, bet.createdTime, user) &&
|
||||||
|
!comments.some((comment) => comment.betId == bet.id)
|
||||||
|
)
|
||||||
|
.sort((b1, b2) => b1.createdTime - b2.createdTime)
|
||||||
|
.pop()
|
||||||
|
|
||||||
|
if (mostRecentCommentableBet) {
|
||||||
|
return (
|
||||||
|
<FeedBet
|
||||||
|
contract={contract}
|
||||||
|
bet={mostRecentCommentableBet}
|
||||||
|
hideOutcome={false}
|
||||||
|
smallAvatar={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const { userPosition, userPositionMoney, yesFloorShares, noFloorShares } =
|
||||||
|
getBettorsPosition(contract, Date.now(), betsByCurrentUser)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row className={'flex w-full gap-2 pt-5'}>
|
<Row className={'flex w-full gap-2 pt-3'}>
|
||||||
<div>
|
<div>
|
||||||
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} />
|
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} />
|
||||||
</div>
|
</div>
|
||||||
<div className={'min-w-0 flex-1 py-1.5'}>
|
<div className={'min-w-0 flex-1 py-1.5'}>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
|
{user && userPosition > 0 && (
|
||||||
|
<>
|
||||||
|
{'You with ' + userPositionMoney + ' '}
|
||||||
|
<>
|
||||||
|
{' of '}
|
||||||
|
<OutcomeLabel
|
||||||
|
outcome={yesFloorShares > noFloorShares ? 'YES' : 'NO'}
|
||||||
|
contract={contract}
|
||||||
|
truncate="short"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<Textarea
|
<Textarea
|
||||||
value={comment}
|
value={comment}
|
||||||
|
@ -244,18 +311,76 @@ export function CommentInput(props: { contract: Contract }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RelativeTimestamp(props: { time: number }) {
|
||||||
|
const { time } = props
|
||||||
|
return (
|
||||||
|
<DateTimeTooltip time={time}>
|
||||||
|
<span className="ml-1 whitespace-nowrap text-gray-400">
|
||||||
|
{fromNow(time)}
|
||||||
|
</span>
|
||||||
|
</DateTimeTooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBettorsPosition(
|
||||||
|
contract: Contract,
|
||||||
|
createdTime: number,
|
||||||
|
bets: Bet[]
|
||||||
|
) {
|
||||||
|
let yesFloorShares = 0,
|
||||||
|
yesShares = 0,
|
||||||
|
noShares = 0,
|
||||||
|
noFloorShares = 0
|
||||||
|
|
||||||
|
const emptyReturn = {
|
||||||
|
userPosition: 0,
|
||||||
|
userPositionMoney: 0,
|
||||||
|
yesFloorShares,
|
||||||
|
noFloorShares,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: show which of the answers was their majority stake at time of comment for FR?
|
||||||
|
if (contract.outcomeType != 'BINARY') {
|
||||||
|
return emptyReturn
|
||||||
|
}
|
||||||
|
if (bets.length === 0) {
|
||||||
|
return emptyReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the majority shares they had when they made the comment
|
||||||
|
const betsBefore = bets.filter((prevBet) => prevBet.createdTime < createdTime)
|
||||||
|
const [yesBets, noBets] = _.partition(
|
||||||
|
betsBefore ?? [],
|
||||||
|
(bet) => bet.outcome === 'YES'
|
||||||
|
)
|
||||||
|
yesShares = _.sumBy(yesBets, (bet) => bet.shares)
|
||||||
|
noShares = _.sumBy(noBets, (bet) => bet.shares)
|
||||||
|
yesFloorShares = Math.floor(yesShares)
|
||||||
|
noFloorShares = Math.floor(noShares)
|
||||||
|
|
||||||
|
const userPosition = yesFloorShares || noFloorShares
|
||||||
|
const { saleValue } = calculateCpmmSale(
|
||||||
|
contract as FullContract<CPMM, Binary>,
|
||||||
|
yesShares || noShares,
|
||||||
|
yesFloorShares > noFloorShares ? 'YES' : 'NO'
|
||||||
|
)
|
||||||
|
const userPositionMoney = formatMoney(Math.abs(saleValue))
|
||||||
|
return { userPosition, userPositionMoney, yesFloorShares, noFloorShares }
|
||||||
|
}
|
||||||
|
|
||||||
export function FeedBet(props: {
|
export function FeedBet(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bet: Bet
|
bet: Bet
|
||||||
hideOutcome: boolean
|
hideOutcome: boolean
|
||||||
smallAvatar: boolean
|
smallAvatar: boolean
|
||||||
|
hideComment?: boolean
|
||||||
bettor?: User // If set: reveal bettor identity
|
bettor?: User // If set: reveal bettor identity
|
||||||
}) {
|
}) {
|
||||||
const { contract, bet, hideOutcome, smallAvatar, bettor } = props
|
const { contract, bet, hideOutcome, smallAvatar, bettor, hideComment } = props
|
||||||
const { id, amount, outcome, createdTime, userId } = bet
|
const { id, amount, outcome, createdTime, userId } = bet
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const isSelf = user?.id === userId
|
const isSelf = user?.id === userId
|
||||||
const canComment = canCommentOnBet(userId, createdTime, user)
|
const canComment = canCommentOnBet(userId, createdTime, user) && !hideComment
|
||||||
|
|
||||||
const [comment, setComment] = useState('')
|
const [comment, setComment] = useState('')
|
||||||
async function submitComment() {
|
async function submitComment() {
|
||||||
|
@ -268,71 +393,76 @@ export function FeedBet(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<Row className={'flex w-full gap-2 pt-3'}>
|
||||||
{isSelf ? (
|
<div>
|
||||||
<Avatar
|
{isSelf ? (
|
||||||
className={clsx(smallAvatar && 'ml-1')}
|
<Avatar
|
||||||
size={smallAvatar ? 'sm' : undefined}
|
className={clsx(smallAvatar && 'ml-1')}
|
||||||
avatarUrl={user.avatarUrl}
|
size={smallAvatar ? 'sm' : undefined}
|
||||||
username={user.username}
|
avatarUrl={user.avatarUrl}
|
||||||
/>
|
username={user.username}
|
||||||
) : bettor ? (
|
/>
|
||||||
<Avatar
|
) : bettor ? (
|
||||||
className={clsx(smallAvatar && 'ml-1')}
|
<Avatar
|
||||||
size={smallAvatar ? 'sm' : undefined}
|
className={clsx(smallAvatar && 'ml-1')}
|
||||||
avatarUrl={bettor.avatarUrl}
|
size={smallAvatar ? 'sm' : undefined}
|
||||||
username={bettor.username}
|
avatarUrl={bettor.avatarUrl}
|
||||||
/>
|
username={bettor.username}
|
||||||
) : (
|
/>
|
||||||
<div className="relative px-1">
|
) : (
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200">
|
<div className="relative px-1">
|
||||||
<UserIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200">
|
||||||
</div>
|
<UserIcon
|
||||||
</div>
|
className="h-5 w-5 text-gray-500"
|
||||||
)}
|
aria-hidden="true"
|
||||||
</div>
|
/>
|
||||||
<div className={'min-w-0 flex-1 py-1.5'}>
|
</div>
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
<span>{isSelf ? 'You' : bettor ? bettor.name : 'A trader'}</span>{' '}
|
|
||||||
{bought} {money}
|
|
||||||
{!hideOutcome && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
of{' '}
|
|
||||||
<OutcomeLabel
|
|
||||||
outcome={outcome}
|
|
||||||
contract={contract}
|
|
||||||
truncate="short"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<RelativeTimestamp time={createdTime} />
|
|
||||||
{(canComment || comment) && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<Textarea
|
|
||||||
value={comment}
|
|
||||||
onChange={(e) => setComment(e.target.value)}
|
|
||||||
className="textarea textarea-bordered w-full resize-none"
|
|
||||||
placeholder="Add a comment..."
|
|
||||||
rows={3}
|
|
||||||
maxLength={MAX_COMMENT_LENGTH}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
||||||
submitComment()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline btn-sm text-transform: mt-1 capitalize"
|
|
||||||
onClick={submitComment}
|
|
||||||
disabled={!canComment}
|
|
||||||
>
|
|
||||||
Comment
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className={'min-w-0 flex-1 py-1.5'}>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
<span>{isSelf ? 'You' : bettor ? bettor.name : 'A trader'}</span>{' '}
|
||||||
|
{bought} {money}
|
||||||
|
{!hideOutcome && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
of{' '}
|
||||||
|
<OutcomeLabel
|
||||||
|
outcome={outcome}
|
||||||
|
contract={contract}
|
||||||
|
truncate="short"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<RelativeTimestamp time={createdTime} />
|
||||||
|
{(canComment || comment) && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<Textarea
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
className="textarea textarea-bordered w-full resize-none"
|
||||||
|
placeholder="Add a comment..."
|
||||||
|
rows={3}
|
||||||
|
maxLength={MAX_COMMENT_LENGTH}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
submitComment()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline btn-sm text-transform: mt-1 capitalize"
|
||||||
|
onClick={submitComment}
|
||||||
|
disabled={!canComment}
|
||||||
|
>
|
||||||
|
Comment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { firebaseLogin, firebaseLogout } from '../../lib/firebase/users'
|
||||||
import { ManifoldLogo } from './manifold-logo'
|
import { ManifoldLogo } from './manifold-logo'
|
||||||
import { MenuButton } from './menu'
|
import { MenuButton } from './menu'
|
||||||
import { getNavigationOptions, ProfileSummary } from './profile-menu'
|
import { getNavigationOptions, ProfileSummary } from './profile-menu'
|
||||||
|
import { useHasCreatedContractToday } from '../../hooks/use-has-created-contract-today'
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: 'Home', href: '/home', icon: HomeIcon },
|
{ name: 'Home', href: '/home', icon: HomeIcon },
|
||||||
|
@ -96,6 +97,7 @@ export default function Sidebar() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
let folds = useFollowedFolds(user) || []
|
let folds = useFollowedFolds(user) || []
|
||||||
folds = _.sortBy(folds, 'followCount').reverse()
|
folds = _.sortBy(folds, 'followCount').reverse()
|
||||||
|
const deservesDailyFreeMarket = !useHasCreatedContractToday(user)
|
||||||
|
|
||||||
const navigationOptions = user === null ? signedOutNavigation : navigation
|
const navigationOptions = user === null ? signedOutNavigation : navigation
|
||||||
const mobileNavigationOptions =
|
const mobileNavigationOptions =
|
||||||
|
@ -159,10 +161,22 @@ export default function Sidebar() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{deservesDailyFreeMarket ? (
|
||||||
|
<div className=" text-primary mt-4 text-center">
|
||||||
|
Use your daily free market! 🎉
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<Link href={'/create'}>
|
<div className={'aligncenter flex justify-center'}>
|
||||||
<button className="btn btn-primary btn-md mt-4">Create Market</button>
|
<Link href={'/create'}>
|
||||||
</Link>
|
<button className="btn btn-primary btn-md mt-4 capitalize">
|
||||||
|
Create Market
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
import { getProbability } from '../../common/calculate'
|
import { getProbability } from '../../common/calculate'
|
||||||
import {
|
import {
|
||||||
|
@ -11,6 +10,7 @@ import {
|
||||||
FullContract,
|
FullContract,
|
||||||
} from '../../common/contract'
|
} from '../../common/contract'
|
||||||
import { formatPercent } from '../../common/util/format'
|
import { formatPercent } from '../../common/util/format'
|
||||||
|
import { ClientRender } from './client-render'
|
||||||
|
|
||||||
export function OutcomeLabel(props: {
|
export function OutcomeLabel(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -72,11 +72,13 @@ export function FreeResponseOutcomeLabel(props: {
|
||||||
const chosen = answers?.find((answer) => answer.id === resolution)
|
const chosen = answers?.find((answer) => answer.id === resolution)
|
||||||
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
||||||
return (
|
return (
|
||||||
<AnswerLabel
|
<FreeResponseAnswerToolTip text={chosen.text}>
|
||||||
answer={chosen}
|
<AnswerLabel
|
||||||
truncate={truncate}
|
answer={chosen}
|
||||||
className={answerClassName}
|
truncate={truncate}
|
||||||
/>
|
className={answerClassName}
|
||||||
|
/>
|
||||||
|
</FreeResponseAnswerToolTip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,3 +128,23 @@ export function AnswerLabel(props: {
|
||||||
|
|
||||||
return <span className={className}>{truncated}</span>
|
return <span className={className}>{truncated}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FreeResponseAnswerToolTip(props: {
|
||||||
|
text: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const { text } = props
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ClientRender>
|
||||||
|
<span
|
||||||
|
className="tooltip hidden cursor-default sm:inline-block"
|
||||||
|
data-tip={text}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</span>
|
||||||
|
</ClientRender>
|
||||||
|
<span className="whitespace-nowrap sm:hidden">{props.children}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
27
web/hooks/use-has-created-contract-today.ts
Normal file
27
web/hooks/use-has-created-contract-today.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { listContracts } from '../lib/firebase/contracts'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
|
||||||
|
export const useHasCreatedContractToday = (user: User | null | undefined) => {
|
||||||
|
const [hasCreatedContractToday, setHasCreatedContractToday] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Uses utc time like the server.
|
||||||
|
const utcTimeString = new Date().toISOString()
|
||||||
|
const todayAtMidnight = new Date(utcTimeString).setUTCHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
async function listUserContractsForToday() {
|
||||||
|
if (!user) return
|
||||||
|
|
||||||
|
const contracts = await listContracts(user.id)
|
||||||
|
const todayContracts = contracts.filter(
|
||||||
|
(contract) => contract.createdTime > todayAtMidnight
|
||||||
|
)
|
||||||
|
setHasCreatedContractToday(todayContracts.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
listUserContractsForToday()
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
return hasCreatedContractToday
|
||||||
|
}
|
|
@ -115,6 +115,18 @@ export async function listContracts(creatorId: string): Promise<Contract[]> {
|
||||||
return snapshot.docs.map((doc) => doc.data() as Contract)
|
return snapshot.docs.map((doc) => doc.data() as Contract)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listTaggedContractsCaseInsensitive(
|
||||||
|
tag: string
|
||||||
|
): Promise<Contract[]> {
|
||||||
|
const q = query(
|
||||||
|
contractCollection,
|
||||||
|
where('lowercaseTags', 'array-contains', tag.toLowerCase()),
|
||||||
|
orderBy('createdTime', 'desc')
|
||||||
|
)
|
||||||
|
const snapshot = await getDocs(q)
|
||||||
|
return snapshot.docs.map((doc) => doc.data() as Contract)
|
||||||
|
}
|
||||||
|
|
||||||
export async function listAllContracts(): Promise<Contract[]> {
|
export async function listAllContracts(): Promise<Contract[]> {
|
||||||
const q = query(contractCollection, orderBy('createdTime', 'desc'))
|
const q = query(contractCollection, orderBy('createdTime', 'desc'))
|
||||||
const snapshot = await getDocs(q)
|
const snapshot = await getDocs(q)
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"next": "12.1.2",
|
"next": "12.1.2",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
"react-confetti": "^6.0.1",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-expanding-textarea": "2.3.5"
|
"react-expanding-textarea": "2.3.5"
|
||||||
},
|
},
|
||||||
|
|
|
@ -33,6 +33,8 @@ import { ContractTabs } from '../../components/contract/contract-tabs'
|
||||||
import { FirstArgument } from '../../../common/util/types'
|
import { FirstArgument } from '../../../common/util/types'
|
||||||
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
||||||
import { contractTextDetails } from '../../components/contract/contract-details'
|
import { contractTextDetails } from '../../components/contract/contract-details'
|
||||||
|
import { useWindowSize } from '../../hooks/use-window-size'
|
||||||
|
import Confetti from 'react-confetti'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: {
|
export async function getStaticPropz(props: {
|
||||||
|
@ -86,9 +88,21 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
|
||||||
const { backToHome } = props
|
const { backToHome } = props
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
const { width, height } = useWindowSize()
|
||||||
|
|
||||||
const contract = useContractWithPreload(props.contract)
|
const contract = useContractWithPreload(props.contract)
|
||||||
const { bets, comments } = props
|
const { bets, comments } = props
|
||||||
|
const [showConfetti, setShowConfetti] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldSeeConfetti = !!(
|
||||||
|
user &&
|
||||||
|
contract &&
|
||||||
|
contract.creatorId === user.id &&
|
||||||
|
Date.now() - contract.createdTime < 10 * 1000
|
||||||
|
)
|
||||||
|
setShowConfetti(shouldSeeConfetti)
|
||||||
|
}, [contract, user])
|
||||||
|
|
||||||
// Sort for now to see if bug is fixed.
|
// Sort for now to see if bug is fixed.
|
||||||
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||||
|
@ -119,6 +133,15 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page rightSidebar={rightSidebar}>
|
<Page rightSidebar={rightSidebar}>
|
||||||
|
{showConfetti && (
|
||||||
|
<Confetti
|
||||||
|
width={width ? width : 500}
|
||||||
|
height={height ? height : 500}
|
||||||
|
recycle={false}
|
||||||
|
numberOfPieces={300}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{ogCardProps && (
|
{ogCardProps && (
|
||||||
<SEO
|
<SEO
|
||||||
title={question}
|
title={question}
|
||||||
|
@ -264,7 +287,7 @@ function ContractTopTrades(props: {
|
||||||
<FeedComment
|
<FeedComment
|
||||||
contract={contract}
|
contract={contract}
|
||||||
comment={commentsById[topCommentId]}
|
comment={commentsById[topCommentId]}
|
||||||
bet={betsById[topCommentId]}
|
betsBySameUser={[betsById[topCommentId]]}
|
||||||
hideOutcome={false}
|
hideOutcome={false}
|
||||||
truncate={false}
|
truncate={false}
|
||||||
smallAvatar={false}
|
smallAvatar={false}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { TagsList } from '../components/tags-list'
|
||||||
import { Row } from '../components/layout/row'
|
import { Row } from '../components/layout/row'
|
||||||
import { MAX_DESCRIPTION_LENGTH, outcomeType } from '../../common/contract'
|
import { MAX_DESCRIPTION_LENGTH, outcomeType } from '../../common/contract'
|
||||||
import { formatMoney } from '../../common/util/format'
|
import { formatMoney } from '../../common/util/format'
|
||||||
|
import { useHasCreatedContractToday } from '../hooks/use-has-created-contract-today'
|
||||||
|
|
||||||
export default function Create() {
|
export default function Create() {
|
||||||
const [question, setQuestion] = useState('')
|
const [question, setQuestion] = useState('')
|
||||||
|
@ -70,6 +71,9 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
const tags = parseWordsAsTags(tagText)
|
const tags = parseWordsAsTags(tagText)
|
||||||
|
|
||||||
const [ante, setAnte] = useState(FIXED_ANTE)
|
const [ante, setAnte] = useState(FIXED_ANTE)
|
||||||
|
|
||||||
|
const deservesDailyFreeMarket = !useHasCreatedContractToday(creator)
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// if (ante === null && creator) {
|
// if (ante === null && creator) {
|
||||||
// const initialAnte = creator.balance < 100 ? MINIMUM_ANTE : 100
|
// const initialAnte = creator.balance < 100 ? MINIMUM_ANTE : 100
|
||||||
|
@ -95,7 +99,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
ante !== undefined &&
|
ante !== undefined &&
|
||||||
ante !== null &&
|
ante !== null &&
|
||||||
ante >= MINIMUM_ANTE &&
|
ante >= MINIMUM_ANTE &&
|
||||||
ante <= balance &&
|
(ante <= balance || deservesDailyFreeMarket) &&
|
||||||
// closeTime must be in the future
|
// closeTime must be in the future
|
||||||
closeTime &&
|
closeTime &&
|
||||||
closeTime > Date.now()
|
closeTime > Date.now()
|
||||||
|
@ -246,10 +250,14 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
text={`Cost to create your market. This amount is used to subsidize trading.`}
|
text={`Cost to create your market. This amount is used to subsidize trading.`}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
{deservesDailyFreeMarket ? (
|
||||||
<div className="label-text text-neutral pl-1">{formatMoney(ante)}</div>
|
<div className="label-text text-primary pl-1">FREE</div>
|
||||||
|
) : (
|
||||||
{ante > balance && (
|
<div className="label-text text-neutral pl-1">
|
||||||
|
{formatMoney(ante)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!deservesDailyFreeMarket && ante > balance && (
|
||||||
<div className="mb-2 mt-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide">
|
<div className="mb-2 mt-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide">
|
||||||
<span className="mr-2 text-red-500">Insufficient balance</span>
|
<span className="mr-2 text-red-500">Insufficient balance</span>
|
||||||
<button
|
<button
|
||||||
|
@ -278,7 +286,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn btn-primary',
|
'btn btn-primary capitalize',
|
||||||
isSubmitting && 'loading disabled'
|
isSubmitting && 'loading disabled'
|
||||||
)}
|
)}
|
||||||
disabled={isSubmitting || !isValid}
|
disabled={isSubmitting || !isValid}
|
||||||
|
|
|
@ -6,22 +6,11 @@ import { Page } from '../components/page'
|
||||||
import { SEO } from '../components/SEO'
|
import { SEO } from '../components/SEO'
|
||||||
import { Title } from '../components/title'
|
import { Title } from '../components/title'
|
||||||
import { useContracts } from '../hooks/use-contracts'
|
import { useContracts } from '../hooks/use-contracts'
|
||||||
import { Contract, listAllContracts } from '../lib/firebase/contracts'
|
import { Contract } from '../lib/firebase/contracts'
|
||||||
|
|
||||||
export async function getStaticProps() {
|
|
||||||
const contracts = await listAllContracts().catch((_) => [])
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
contracts,
|
|
||||||
},
|
|
||||||
|
|
||||||
revalidate: 60, // regenerate after a minute
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Rename endpoint to "Explore"
|
// TODO: Rename endpoint to "Explore"
|
||||||
export default function Markets(props: { contracts: Contract[] }) {
|
export default function Markets() {
|
||||||
const contracts = useContracts() ?? props.contracts ?? []
|
const contracts = useContracts()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
@ -30,11 +19,6 @@ export default function Markets(props: { contracts: Contract[] }) {
|
||||||
description="Discover what's new, trending, or soon-to-close. Or search among our hundreds of markets."
|
description="Discover what's new, trending, or soon-to-close. Or search among our hundreds of markets."
|
||||||
url="/markets"
|
url="/markets"
|
||||||
/>
|
/>
|
||||||
{/* <HotMarkets contracts={hotContracts} />
|
|
||||||
<Spacer h={10} />
|
|
||||||
<ClosingSoonMarkets contracts={closingSoonContracts} />
|
|
||||||
<Spacer h={10} /> */}
|
|
||||||
|
|
||||||
<SearchableGrid contracts={contracts} />
|
<SearchableGrid contracts={contracts} />
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,39 +1,30 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { SearchableGrid } from '../../components/contract/contracts-list'
|
import { SearchableGrid } from '../../components/contract/contracts-list'
|
||||||
import { Page } from '../../components/page'
|
import { Page } from '../../components/page'
|
||||||
import { Title } from '../../components/title'
|
import { Title } from '../../components/title'
|
||||||
import { useContracts } from '../../hooks/use-contracts'
|
import {
|
||||||
import { Contract, listAllContracts } from '../../lib/firebase/contracts'
|
Contract,
|
||||||
|
listTaggedContractsCaseInsensitive,
|
||||||
|
} from '../../lib/firebase/contracts'
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export default function TagPage() {
|
||||||
const contracts = await listAllContracts().catch((_) => [])
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
contracts,
|
|
||||||
},
|
|
||||||
|
|
||||||
revalidate: 60, // regenerate after a minute
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
return { paths: [], fallback: 'blocking' }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TagPage(props: { contracts: Contract[] }) {
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { tag } = router.query as { tag: string }
|
const { tag } = router.query as { tag: string }
|
||||||
|
|
||||||
const contracts = useContracts()
|
// mqp: i wrote this in a panic to make the page literally work at all so if you
|
||||||
|
// want to e.g. listen for new contracts you may want to fix it up
|
||||||
const taggedContracts = (contracts ?? props.contracts).filter((contract) =>
|
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
||||||
contract.lowercaseTags.includes(tag.toLowerCase())
|
useEffect(() => {
|
||||||
)
|
if (tag != null) {
|
||||||
|
listTaggedContractsCaseInsensitive(tag).then(setContracts)
|
||||||
|
}
|
||||||
|
}, [tag])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Title text={`#${tag}`} />
|
<Title text={`#${tag}`} />
|
||||||
<SearchableGrid contracts={taggedContracts} />
|
<SearchableGrid contracts={contracts} />
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -4446,6 +4446,13 @@ raw-body@2.4.2, raw-body@^2.2.0:
|
||||||
iconv-lite "0.4.24"
|
iconv-lite "0.4.24"
|
||||||
unpipe "1.0.0"
|
unpipe "1.0.0"
|
||||||
|
|
||||||
|
react-confetti@^6.0.1:
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.0.1.tgz#d4f57b5a021dd908a6243b8f63b6009b00818d10"
|
||||||
|
integrity sha512-ZpOTBrqSNhWE4rRXCZ6E6U+wGd7iYHF5MGrqwikoiBpgBq9Akdu0DcLW+FdFnLjyZYC+VfAiV2KeFgYRMyMrkA==
|
||||||
|
dependencies:
|
||||||
|
tween-functions "^1.2.0"
|
||||||
|
|
||||||
react-dom@17.0.2:
|
react-dom@17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||||
|
@ -5159,6 +5166,11 @@ tsutils@^3.21.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.8.1"
|
tslib "^1.8.1"
|
||||||
|
|
||||||
|
tween-functions@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
|
||||||
|
integrity sha1-GuOlDnxguz3vd06scHrLynO7w/8=
|
||||||
|
|
||||||
type-check@^0.4.0, type-check@~0.4.0:
|
type-check@^0.4.0, type-check@~0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user