Merge branch 'manifoldmarkets:main' into main
This commit is contained in:
commit
195fcf5a67
|
@ -1,4 +1,4 @@
|
||||||
import { Dictionary, groupBy, last, sum, sumBy, uniq } from 'lodash'
|
import { Dictionary, groupBy, last, partition, sum, sumBy, uniq } from 'lodash'
|
||||||
import { calculatePayout, getContractBetMetrics } from './calculate'
|
import { calculatePayout, getContractBetMetrics } from './calculate'
|
||||||
import { Bet, LimitBet } from './bet'
|
import { Bet, LimitBet } from './bet'
|
||||||
import {
|
import {
|
||||||
|
@ -266,7 +266,9 @@ export const calculateMetricsByContract = (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ContractMetrics = ReturnType<typeof calculateMetricsByContract>[number]
|
export type ContractMetrics = ReturnType<
|
||||||
|
typeof calculateMetricsByContract
|
||||||
|
>[number]
|
||||||
|
|
||||||
const calculatePeriodProfit = (
|
const calculatePeriodProfit = (
|
||||||
contract: CPMMBinaryContract,
|
contract: CPMMBinaryContract,
|
||||||
|
@ -275,7 +277,10 @@ const calculatePeriodProfit = (
|
||||||
) => {
|
) => {
|
||||||
const days = period === 'day' ? 1 : period === 'week' ? 7 : 30
|
const days = period === 'day' ? 1 : period === 'week' ? 7 : 30
|
||||||
const fromTime = Date.now() - days * DAY_MS
|
const fromTime = Date.now() - days * DAY_MS
|
||||||
const previousBets = bets.filter((b) => b.createdTime < fromTime)
|
const [previousBets, recentBets] = partition(
|
||||||
|
bets,
|
||||||
|
(b) => b.createdTime < fromTime
|
||||||
|
)
|
||||||
|
|
||||||
const prevProb = contract.prob - contract.probChanges[period]
|
const prevProb = contract.prob - contract.probChanges[period]
|
||||||
const prob = contract.resolutionProbability
|
const prob = contract.resolutionProbability
|
||||||
|
@ -292,13 +297,18 @@ const calculatePeriodProfit = (
|
||||||
contract,
|
contract,
|
||||||
prob
|
prob
|
||||||
)
|
)
|
||||||
const profit = currentBetsValue - previousBetsValue
|
|
||||||
const profitPercent =
|
const { profit: recentProfit, invested: recentInvested } =
|
||||||
previousBetsValue === 0 ? 0 : 100 * (profit / previousBetsValue)
|
getContractBetMetrics(contract, recentBets)
|
||||||
|
|
||||||
|
const profit = currentBetsValue - previousBetsValue + recentProfit
|
||||||
|
const invested = previousBetsValue + recentInvested
|
||||||
|
const profitPercent = invested === 0 ? 0 : 100 * (profit / invested)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
profit,
|
profit,
|
||||||
profitPercent,
|
profitPercent,
|
||||||
|
invested,
|
||||||
prevValue: previousBetsValue,
|
prevValue: previousBetsValue,
|
||||||
value: currentBetsValue,
|
value: currentBetsValue,
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ Adapted from https://firebase.google.com/docs/functions/get-started
|
||||||
3. `$ firebase login` to authenticate the CLI tools to Firebase
|
3. `$ firebase login` to authenticate the CLI tools to Firebase
|
||||||
4. `$ firebase use dev` to choose the dev project
|
4. `$ firebase use dev` to choose the dev project
|
||||||
|
|
||||||
### For local development
|
#### (Installing) For local development
|
||||||
|
|
||||||
0. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI
|
0. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI
|
||||||
1. If you don't have java (or see the error `Error: Process java -version has exited with code 1. Please make sure Java is installed and on your system PATH.`):
|
1. If you don't have java (or see the error `Error: Process java -version has exited with code 1. Please make sure Java is installed and on your system PATH.`):
|
||||||
|
@ -35,10 +35,10 @@ Adapted from https://firebase.google.com/docs/functions/get-started
|
||||||
|
|
||||||
## Developing locally
|
## Developing locally
|
||||||
|
|
||||||
0. `$ firebase use dev` if you haven't already
|
0. `$ ./dev.sh localdb` to start the local emulator and front end
|
||||||
1. `$ yarn serve` to spin up the emulators 0. The Emulator UI is at http://localhost:4000; the functions are hosted on :5001.
|
1. If you change db trigger code, you have to start (doesn't have to complete) the deploy of it to dev to cause a hard emulator code refresh `$ firebase deploy --only functions:dbTriggerNameHere`
|
||||||
Note: You have to kill and restart emulators when you change code; no hot reload =(
|
- There's surely a better way to cause/react to a db trigger update but just adding this here for now as it works
|
||||||
2. `$ yarn dev:emulate` in `/web` to connect to emulators with the frontend 0. Note: emulated database is cleared after every shutdown
|
2. If you want to test a scheduled function replace your function in `test-scheduled-function.ts` and send a GET to `http://localhost:8088/testscheduledfunction` (Best user experience is via [Postman](https://www.postman.com/downloads/)!)
|
||||||
|
|
||||||
## Firestore Commands
|
## Firestore Commands
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,9 @@ async function main() {
|
||||||
user.achievements = await awardBettingStreakBadges(user)
|
user.achievements = await awardBettingStreakBadges(user)
|
||||||
console.log('Added achievements to user', user.id)
|
console.log('Added achievements to user', user.id)
|
||||||
// going to ignore backfilling the proven correct badges for now
|
// going to ignore backfilling the proven correct badges for now
|
||||||
|
} else {
|
||||||
|
// Make corrections to existing achievements
|
||||||
|
await awardMarketCreatorBadges(user)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -67,12 +70,11 @@ async function removeErrorBadges(user: User) {
|
||||||
|
|
||||||
async function awardMarketCreatorBadges(user: User) {
|
async function awardMarketCreatorBadges(user: User) {
|
||||||
// Award market maker badges
|
// Award market maker badges
|
||||||
const contracts = await getValues<Contract>(
|
const contracts = (
|
||||||
firestore
|
await getValues<Contract>(
|
||||||
.collection(`contracts`)
|
firestore.collection(`contracts`).where('creatorId', '==', user.id)
|
||||||
.where('creatorId', '==', user.id)
|
)
|
||||||
.where('resolution', '!=', 'CANCEL')
|
).filter((c) => !c.resolution || c.resolution != 'CANCEL')
|
||||||
)
|
|
||||||
|
|
||||||
const achievements = {
|
const achievements = {
|
||||||
...user.achievements,
|
...user.achievements,
|
||||||
|
@ -81,7 +83,12 @@ async function awardMarketCreatorBadges(user: User) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for (const threshold of marketCreatorBadgeRarityThresholds) {
|
for (const threshold of marketCreatorBadgeRarityThresholds) {
|
||||||
|
const alreadyHasBadge = user.achievements.marketCreator?.badges.some(
|
||||||
|
(b) => b.data.totalContractsCreated === threshold
|
||||||
|
)
|
||||||
|
if (alreadyHasBadge) continue
|
||||||
if (contracts.length >= threshold) {
|
if (contracts.length >= threshold) {
|
||||||
|
console.log(`User ${user.id} has at least ${threshold} contracts`)
|
||||||
const badge = {
|
const badge = {
|
||||||
type: 'MARKET_CREATOR',
|
type: 'MARKET_CREATOR',
|
||||||
name: 'Market Creator',
|
name: 'Market Creator',
|
||||||
|
|
|
@ -20,7 +20,6 @@ import { getProbability } from 'common/calculate'
|
||||||
import { createMarket } from 'web/lib/firebase/api'
|
import { createMarket } from 'web/lib/firebase/api'
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
import { FIXED_ANTE } from 'common/economy'
|
import { FIXED_ANTE } from 'common/economy'
|
||||||
import { useTextEditor } from 'web/components/editor'
|
|
||||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { CopyLinkButton } from '../copy-link-button'
|
import { CopyLinkButton } from '../copy-link-button'
|
||||||
|
@ -43,7 +42,6 @@ export function CreateChallengeModal(props: {
|
||||||
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 [loading, setLoading] = useState(false)
|
||||||
const { editor } = useTextEditor({ placeholder: '' })
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isOpen} setOpen={setOpen}>
|
<Modal open={isOpen} setOpen={setOpen}>
|
||||||
|
@ -64,7 +62,6 @@ export function CreateChallengeModal(props: {
|
||||||
question: newChallenge.question,
|
question: newChallenge.question,
|
||||||
outcomeType: 'BINARY',
|
outcomeType: 'BINARY',
|
||||||
initialProb: 50,
|
initialProb: 50,
|
||||||
description: editor?.getJSON(),
|
|
||||||
ante: FIXED_ANTE,
|
ante: FIXED_ANTE,
|
||||||
closeTime: dayjs().add(30, 'day').valueOf(),
|
closeTime: dayjs().add(30, 'day').valueOf(),
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,13 +17,21 @@ export function CommentInput(props: {
|
||||||
// Reply to another comment
|
// Reply to another comment
|
||||||
parentCommentId?: string
|
parentCommentId?: string
|
||||||
onSubmitComment?: (editor: Editor) => void
|
onSubmitComment?: (editor: Editor) => void
|
||||||
|
// unique id for autosave
|
||||||
|
pageId: string
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { parentAnswerOutcome, parentCommentId, replyTo, onSubmitComment } =
|
const {
|
||||||
props
|
parentAnswerOutcome,
|
||||||
|
parentCommentId,
|
||||||
|
replyTo,
|
||||||
|
onSubmitComment,
|
||||||
|
pageId,
|
||||||
|
} = props
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const { editor, upload } = useTextEditor({
|
const { editor, upload } = useTextEditor({
|
||||||
|
key: `comment ${pageId} ${parentCommentId ?? parentAnswerOutcome ?? ''}`,
|
||||||
simple: true,
|
simple: true,
|
||||||
max: MAX_COMMENT_LENGTH,
|
max: MAX_COMMENT_LENGTH,
|
||||||
placeholder:
|
placeholder:
|
||||||
|
@ -80,7 +88,7 @@ export function CommentInputTextArea(props: {
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
submitComment()
|
submitComment()
|
||||||
editor?.commands?.clearContent()
|
editor?.commands?.clearContent(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -107,7 +115,7 @@ export function CommentInputTextArea(props: {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// insert at mention and focus
|
// insert at mention and focus
|
||||||
if (replyTo) {
|
if (replyTo && editor.isEmpty) {
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.setContent({
|
.setContent({
|
||||||
|
|
36
web/components/contract/add-liquidity-button.tsx
Normal file
36
web/components/contract/add-liquidity-button.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
import { buttonClass } from 'web/components/button'
|
||||||
|
import { CPMMContract } from 'common/contract'
|
||||||
|
import { LiquidityModal } from './liquidity-modal'
|
||||||
|
|
||||||
|
export function AddLiquidityButton(props: {
|
||||||
|
contract: CPMMContract
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, className } = props
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const disabled =
|
||||||
|
contract.isResolved || (contract.closeTime ?? Infinity) < Date.now()
|
||||||
|
|
||||||
|
if (disabled) return <></>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={clsx(
|
||||||
|
buttonClass('2xs', 'override'),
|
||||||
|
'cursor-pointer',
|
||||||
|
'gap-1 border-2 border-blue-400 text-blue-400 hover:bg-blue-400 hover:text-white',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div>💧 Add liquidity</div>
|
||||||
|
<LiquidityModal contract={contract} isOpen={open} setOpen={setOpen} />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
|
@ -21,7 +21,6 @@ import {
|
||||||
import {
|
import {
|
||||||
AnswerLabel,
|
AnswerLabel,
|
||||||
BinaryContractOutcomeLabel,
|
BinaryContractOutcomeLabel,
|
||||||
BinaryOutcomeLabel,
|
|
||||||
CancelLabel,
|
CancelLabel,
|
||||||
FreeResponseOutcomeLabel,
|
FreeResponseOutcomeLabel,
|
||||||
} from '../outcome-label'
|
} from '../outcome-label'
|
||||||
|
@ -430,17 +429,16 @@ export function ContractCardProbChange(props: {
|
||||||
'items-center justify-between gap-4 pl-6 pr-4 pb-2 text-sm'
|
'items-center justify-between gap-4 pl-6 pr-4 pb-2 text-sm'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Row className="gap-1">
|
<Row className="gap-1 text-gray-700">
|
||||||
<div className="text-gray-500">Position</div>
|
<div className="text-gray-500">Position</div>
|
||||||
{formatMoney(metrics.payout)}
|
{formatMoney(metrics.payout)} {outcome}
|
||||||
<BinaryOutcomeLabel outcome={outcome} />
|
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{dayMetrics && (
|
{dayMetrics && (
|
||||||
<>
|
<>
|
||||||
<Row className="items-center">
|
<Row className="items-center">
|
||||||
<div className="mr-1 text-gray-500">Daily profit</div>
|
<div className="mr-1 text-gray-500">Daily profit</div>
|
||||||
<ProfitBadgeMana amount={dayMetrics.profit} />
|
<ProfitBadgeMana amount={dayMetrics.profit} gray />
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -48,6 +48,7 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) {
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
const { editor, upload } = useTextEditor({
|
const { editor, upload } = useTextEditor({
|
||||||
|
// key: `description ${contract.id}`,
|
||||||
max: MAX_DESCRIPTION_LENGTH,
|
max: MAX_DESCRIPTION_LENGTH,
|
||||||
defaultValue: contract.description,
|
defaultValue: contract.description,
|
||||||
disabled: isSubmitting,
|
disabled: isSubmitting,
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { DuplicateContractButton } from '../duplicate-contract-button'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { BETTORS, User } from 'common/user'
|
import { BETTORS, User } from 'common/user'
|
||||||
import { Button } from '../button'
|
import { Button } from '../button'
|
||||||
|
import { AddLiquidityButton } from './add-liquidity-button'
|
||||||
|
|
||||||
export const contractDetailsButtonClassName =
|
export const contractDetailsButtonClassName =
|
||||||
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
||||||
|
@ -241,6 +242,9 @@ export function ContractInfoDialog(props: {
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<Row className="flex-wrap">
|
<Row className="flex-wrap">
|
||||||
|
{mechanism === 'cpmm-1' && (
|
||||||
|
<AddLiquidityButton contract={contract} className="mr-2" />
|
||||||
|
)}
|
||||||
<DuplicateContractButton contract={contract} />
|
<DuplicateContractButton contract={contract} />
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { FollowMarketButton } from 'web/components/follow-market-button'
|
||||||
import { LikeMarketButton } from 'web/components/contract/like-market-button'
|
import { LikeMarketButton } from 'web/components/contract/like-market-button'
|
||||||
import { ContractInfoDialog } from 'web/components/contract/contract-info-dialog'
|
import { ContractInfoDialog } from 'web/components/contract/contract-info-dialog'
|
||||||
import { Tooltip } from '../tooltip'
|
import { Tooltip } from '../tooltip'
|
||||||
import { LiquidityButton } from './liquidity-button'
|
|
||||||
|
|
||||||
export function ExtraContractActionsRow(props: { contract: Contract }) {
|
export function ExtraContractActionsRow(props: { contract: Contract }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
|
@ -19,10 +18,9 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<FollowMarketButton contract={contract} user={user} />
|
<FollowMarketButton contract={contract} user={user} />
|
||||||
{contract.mechanism === 'cpmm-1' && (
|
|
||||||
<LiquidityButton contract={contract} user={user} />
|
|
||||||
)}
|
|
||||||
<LikeMarketButton contract={contract} user={user} />
|
<LikeMarketButton contract={contract} user={user} />
|
||||||
|
|
||||||
<Tooltip text="Share" placement="bottom" noTap noFade>
|
<Tooltip text="Share" placement="bottom" noTap noFade>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -39,6 +37,7 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<ContractInfoDialog contract={contract} user={user} />
|
<ContractInfoDialog contract={contract} user={user} />
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
|
|
|
@ -23,7 +23,7 @@ export function LiquidityModal(props: {
|
||||||
return (
|
return (
|
||||||
<Modal open={isOpen} setOpen={setOpen} size="sm">
|
<Modal open={isOpen} setOpen={setOpen} size="sm">
|
||||||
<Col className="gap-2.5 rounded bg-white p-4 pb-8 sm:gap-4">
|
<Col className="gap-2.5 rounded bg-white p-4 pb-8 sm:gap-4">
|
||||||
<Title className="!mt-0 !mb-2" text="💧 Add a subsidy" />
|
<Title className="!mt-0 !mb-2" text="💧 Add liquidity" />
|
||||||
|
|
||||||
<div>Total liquidity subsidies: {formatMoney(totalLiquidity)}</div>
|
<div>Total liquidity subsidies: {formatMoney(totalLiquidity)}</div>
|
||||||
<AddLiquidityPanel contract={contract as CPMMContract} />
|
<AddLiquidityPanel contract={contract as CPMMContract} />
|
||||||
|
@ -91,7 +91,7 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) {
|
||||||
label="M$"
|
label="M$"
|
||||||
error={error}
|
error={error}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
inputClassName="w-16 mr-4"
|
inputClassName="w-28 mr-4"
|
||||||
/>
|
/>
|
||||||
<Button size="md" color="blue" onClick={submit} disabled={isLoading}>
|
<Button size="md" color="blue" onClick={submit} disabled={isLoading}>
|
||||||
Add
|
Add
|
||||||
|
|
|
@ -21,6 +21,7 @@ export function CreatePost(props: { group?: Group }) {
|
||||||
const { group } = props
|
const { group } = props
|
||||||
|
|
||||||
const { editor, upload } = useTextEditor({
|
const { editor, upload } = useTextEditor({
|
||||||
|
key: `post ${group?.id || ''}`,
|
||||||
disabled: isSubmitting,
|
disabled: isSubmitting,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ export function CreatePost(props: { group?: Group }) {
|
||||||
return e
|
return e
|
||||||
})
|
})
|
||||||
if (result.post) {
|
if (result.post) {
|
||||||
|
editor.commands.clearContent(true)
|
||||||
await Router.push(postPath(result.post.slug))
|
await Router.push(postPath(result.post.slug))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import StarterKit from '@tiptap/starter-kit'
|
||||||
import { Image } from '@tiptap/extension-image'
|
import { Image } from '@tiptap/extension-image'
|
||||||
import { Link } from '@tiptap/extension-link'
|
import { Link } from '@tiptap/extension-link'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { Linkify } from './linkify'
|
import { Linkify } from './linkify'
|
||||||
import { uploadImage } from 'web/lib/firebase/storage'
|
import { uploadImage } from 'web/lib/firebase/storage'
|
||||||
import { useMutation } from 'react-query'
|
import { useMutation } from 'react-query'
|
||||||
|
@ -41,6 +41,12 @@ import ItalicIcon from 'web/lib/icons/italic-icon'
|
||||||
import LinkIcon from 'web/lib/icons/link-icon'
|
import LinkIcon from 'web/lib/icons/link-icon'
|
||||||
import { getUrl } from 'common/util/parse'
|
import { getUrl } from 'common/util/parse'
|
||||||
import { TiptapSpoiler } from 'common/util/tiptap-spoiler'
|
import { TiptapSpoiler } from 'common/util/tiptap-spoiler'
|
||||||
|
import {
|
||||||
|
storageStore,
|
||||||
|
usePersistentState,
|
||||||
|
} from 'web/hooks/use-persistent-state'
|
||||||
|
import { safeLocalStorage } from 'web/lib/util/local'
|
||||||
|
import { debounce } from 'lodash'
|
||||||
|
|
||||||
const DisplayImage = Image.configure({
|
const DisplayImage = Image.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
|
@ -90,19 +96,34 @@ export function useTextEditor(props: {
|
||||||
defaultValue?: Content
|
defaultValue?: Content
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
simple?: boolean
|
simple?: boolean
|
||||||
|
key?: string // unique key for autosave. If set, plz call `clearContent(true)` on submit to clear autosave
|
||||||
}) {
|
}) {
|
||||||
const { placeholder, max, defaultValue = '', disabled, simple } = props
|
const { placeholder, max, defaultValue, disabled, simple, key } = props
|
||||||
|
|
||||||
|
const [content, saveContent] = usePersistentState<JSONContent | undefined>(
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
key: `text ${key}`,
|
||||||
|
store: storageStore(safeLocalStorage()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const save = useCallback(debounce(saveContent, 500), [])
|
||||||
|
|
||||||
const editorClass = clsx(
|
const editorClass = clsx(
|
||||||
proseClass,
|
proseClass,
|
||||||
!simple && 'min-h-[6em]',
|
!simple && 'min-h-[6em]',
|
||||||
'outline-none pt-2 px-4',
|
'outline-none pt-2 px-4',
|
||||||
'prose-img:select-auto',
|
'prose-img:select-auto',
|
||||||
'[&_.ProseMirror-selectednode]:outline-dotted [&_*]:outline-indigo-300' // selected img, emebeds
|
'[&_.ProseMirror-selectednode]:outline-dotted [&_*]:outline-indigo-300' // selected img, embeds
|
||||||
)
|
)
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
editorProps: { attributes: { class: editorClass } },
|
editorProps: {
|
||||||
|
attributes: { class: editorClass, spellcheck: simple ? 'true' : 'false' },
|
||||||
|
},
|
||||||
|
onUpdate: key ? ({ editor }) => save(editor.getJSON()) : undefined,
|
||||||
extensions: [
|
extensions: [
|
||||||
...editorExtensions(simple),
|
...editorExtensions(simple),
|
||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
|
@ -112,7 +133,7 @@ export function useTextEditor(props: {
|
||||||
}),
|
}),
|
||||||
CharacterCount.configure({ limit: max }),
|
CharacterCount.configure({ limit: max }),
|
||||||
],
|
],
|
||||||
content: defaultValue,
|
content: defaultValue ?? (key && content ? content : ''),
|
||||||
})
|
})
|
||||||
|
|
||||||
const upload = useUploadMutation(editor)
|
const upload = useUploadMutation(editor)
|
||||||
|
|
|
@ -268,6 +268,7 @@ export function ContractCommentInput(props: {
|
||||||
parentAnswerOutcome={parentAnswerOutcome}
|
parentAnswerOutcome={parentAnswerOutcome}
|
||||||
parentCommentId={parentCommentId}
|
parentCommentId={parentCommentId}
|
||||||
onSubmitComment={onSubmitComment}
|
onSubmitComment={onSubmitComment}
|
||||||
|
pageId={contract.id}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
goldClassName,
|
goldClassName,
|
||||||
silverClassName,
|
silverClassName,
|
||||||
} from 'web/components/badge-display'
|
} from 'web/components/badge-display'
|
||||||
|
import { formatMoney } from 'common/util/format'
|
||||||
|
|
||||||
export function BadgesModal(props: {
|
export function BadgesModal(props: {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
@ -132,7 +133,9 @@ function ProvenCorrectBadgeItem(props: {
|
||||||
<Col className={'text-center'}>
|
<Col className={'text-center'}>
|
||||||
<Medal rarity={rarity} />
|
<Medal rarity={rarity} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={`Make a comment attached to a winning bet worth ${betAmount}`}
|
text={`Make a comment attached to a winning bet worth ${formatMoney(
|
||||||
|
betAmount
|
||||||
|
)}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
|
|
|
@ -28,10 +28,17 @@ export function ProfitBadge(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfitBadgeMana(props: { amount: number; className?: string }) {
|
export function ProfitBadgeMana(props: {
|
||||||
const { amount, className } = props
|
amount: number
|
||||||
const colors =
|
gray?: boolean
|
||||||
amount > 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { amount, gray, className } = props
|
||||||
|
const colors = gray
|
||||||
|
? 'bg-gray-100 text-gray-700'
|
||||||
|
: amount > 0
|
||||||
|
? 'bg-gray-100 text-green-800'
|
||||||
|
: 'bg-gray-100 text-red-800'
|
||||||
|
|
||||||
const formatted =
|
const formatted =
|
||||||
ENV_CONFIG.moneyMoniker + (amount > 0 ? '+' : '') + amount.toFixed(0)
|
ENV_CONFIG.moneyMoniker + (amount > 0 ? '+' : '') + amount.toFixed(0)
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||||
import { getGroup, groupPath } from 'web/lib/firebase/groups'
|
import { getGroup, groupPath } from 'web/lib/firebase/groups'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
|
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { GroupSelector } from 'web/components/groups/group-selector'
|
import { GroupSelector } from 'web/components/groups/group-selector'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
|
@ -228,6 +227,7 @@ export function NewContract(props: {
|
||||||
: `e.g. I will choose the answer according to...`
|
: `e.g. I will choose the answer according to...`
|
||||||
|
|
||||||
const { editor, upload } = useTextEditor({
|
const { editor, upload } = useTextEditor({
|
||||||
|
key: 'create market',
|
||||||
max: MAX_DESCRIPTION_LENGTH,
|
max: MAX_DESCRIPTION_LENGTH,
|
||||||
placeholder: descriptionPlaceholder,
|
placeholder: descriptionPlaceholder,
|
||||||
disabled: isSubmitting,
|
disabled: isSubmitting,
|
||||||
|
@ -236,9 +236,6 @@ export function NewContract(props: {
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const isEditorFilled = editor != null && !editor.isEmpty
|
|
||||||
useWarnUnsavedChanges(!isSubmitting && (Boolean(question) || isEditorFilled))
|
|
||||||
|
|
||||||
function setCloseDateInDays(days: number) {
|
function setCloseDateInDays(days: number) {
|
||||||
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD')
|
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD')
|
||||||
setCloseDate(newCloseDate)
|
setCloseDate(newCloseDate)
|
||||||
|
@ -272,6 +269,7 @@ export function NewContract(props: {
|
||||||
selectedGroup: selectedGroup?.id,
|
selectedGroup: selectedGroup?.id,
|
||||||
isFree: false,
|
isFree: false,
|
||||||
})
|
})
|
||||||
|
editor?.commands.clearContent(true)
|
||||||
await router.push(contractPath(result as Contract))
|
await router.push(contractPath(result as Contract))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('error creating contract', e, (e as any).details)
|
console.error('error creating contract', e, (e as any).details)
|
||||||
|
|
|
@ -99,10 +99,10 @@ export async function getStaticPaths() {
|
||||||
const groupSubpages = [
|
const groupSubpages = [
|
||||||
undefined,
|
undefined,
|
||||||
GROUP_CHAT_SLUG,
|
GROUP_CHAT_SLUG,
|
||||||
|
'overview',
|
||||||
'markets',
|
'markets',
|
||||||
'leaderboards',
|
'leaderboards',
|
||||||
'about',
|
'about',
|
||||||
'posts',
|
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export default function GroupPage(props: {
|
export default function GroupPage(props: {
|
||||||
|
@ -131,8 +131,8 @@ export default function GroupPage(props: {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { slugs } = router.query as { slugs: string[] }
|
const { slugs } = router.query as { slugs: string[] }
|
||||||
const page = slugs?.[1] as typeof groupSubpages[number]
|
const page = slugs?.[1] as typeof groupSubpages[number]
|
||||||
const tabIndex = ['markets', 'leaderboard', 'about', 'posts'].indexOf(
|
const tabIndex = ['overview', 'markets', 'leaderboards'].indexOf(
|
||||||
page ?? 'markets'
|
page === 'about' ? 'overview' : page ?? 'markets'
|
||||||
)
|
)
|
||||||
|
|
||||||
const group = useGroup(props.group?.id) ?? props.group
|
const group = useGroup(props.group?.id) ?? props.group
|
||||||
|
|
|
@ -1220,10 +1220,9 @@ function getSourceUrl(notification: Notification) {
|
||||||
sourceType
|
sourceType
|
||||||
)}`
|
)}`
|
||||||
else if (sourceSlug)
|
else if (sourceSlug)
|
||||||
return `/${sourceSlug}#${getSourceIdForLinkComponent(
|
return `${
|
||||||
sourceId ?? '',
|
sourceSlug.startsWith('/') ? sourceSlug : '/' + sourceSlug
|
||||||
sourceType
|
}#${getSourceIdForLinkComponent(sourceId ?? '', sourceType)}`
|
||||||
)}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSourceIdForLinkComponent(
|
function getSourceIdForLinkComponent(
|
||||||
|
|
|
@ -94,6 +94,7 @@ export function PostCommentInput(props: {
|
||||||
replyTo={replyToUser}
|
replyTo={replyToUser}
|
||||||
parentCommentId={parentCommentId}
|
parentCommentId={parentCommentId}
|
||||||
onSubmitComment={onSubmitComment}
|
onSubmitComment={onSubmitComment}
|
||||||
|
pageId={post.id}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user