Merge branch 'main' into editor-tweet
This commit is contained in:
commit
fb75aa73ba
|
@ -25,6 +25,10 @@ export function isAdmin(email: string) {
|
||||||
return ENV_CONFIG.adminEmails.includes(email)
|
return ENV_CONFIG.adminEmails.includes(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isManifoldId(userId: string) {
|
||||||
|
return userId === 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2'
|
||||||
|
}
|
||||||
|
|
||||||
export const DOMAIN = ENV_CONFIG.domain
|
export const DOMAIN = ENV_CONFIG.domain
|
||||||
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
|
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
|
||||||
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
|
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { EnvConfig, PROD_CONFIG } from './prod'
|
||||||
|
|
||||||
export const DEV_CONFIG: EnvConfig = {
|
export const DEV_CONFIG: EnvConfig = {
|
||||||
...PROD_CONFIG,
|
...PROD_CONFIG,
|
||||||
|
domain: 'dev.manifold.markets',
|
||||||
firebaseConfig: {
|
firebaseConfig: {
|
||||||
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
||||||
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
||||||
|
|
|
@ -528,6 +528,10 @@ $ curl https://manifold.markets/api/v0/bet -X POST -H 'Content-Type: application
|
||||||
"contractId":"{...}"}'
|
"contractId":"{...}"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `POST /v0/bet/cancel/[id]`
|
||||||
|
|
||||||
|
Cancel the limit order of a bet with the specified id. If the bet was unfilled, it will be cancelled so that no other bets will match with it. This is action irreversable.
|
||||||
|
|
||||||
### `POST /v0/market`
|
### `POST /v0/market`
|
||||||
|
|
||||||
Creates a new market on behalf of the authorized user.
|
Creates a new market on behalf of the authorized user.
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
groupPayoutsByUser,
|
groupPayoutsByUser,
|
||||||
Payout,
|
Payout,
|
||||||
} from '../../common/payouts'
|
} from '../../common/payouts'
|
||||||
import { isAdmin } from '../../common/envs/constants'
|
import { isManifoldId } from '../../common/envs/constants'
|
||||||
import { removeUndefinedProps } from '../../common/util/object'
|
import { removeUndefinedProps } from '../../common/util/object'
|
||||||
import { LiquidityProvision } from '../../common/liquidity-provision'
|
import { LiquidityProvision } from '../../common/liquidity-provision'
|
||||||
import { APIError, newEndpoint, validate } from './api'
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
|
@ -82,7 +82,7 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => {
|
||||||
req.body
|
req.body
|
||||||
)
|
)
|
||||||
|
|
||||||
if (creatorId !== auth.uid && !isAdmin(auth.uid))
|
if (creatorId !== auth.uid && !isManifoldId(auth.uid))
|
||||||
throw new APIError(403, 'User is not creator of contract')
|
throw new APIError(403, 'User is not creator of contract')
|
||||||
|
|
||||||
if (contract.resolution) throw new APIError(400, 'Contract already resolved')
|
if (contract.resolution) throw new APIError(400, 'Contract already resolved')
|
||||||
|
|
|
@ -1,26 +1,23 @@
|
||||||
import { MAX_ANSWER_LENGTH } from 'common/answer'
|
import { MAX_ANSWER_LENGTH } from 'common/answer'
|
||||||
import { useState } from 'react'
|
|
||||||
import Textarea from 'react-expanding-textarea'
|
import Textarea from 'react-expanding-textarea'
|
||||||
import { XIcon } from '@heroicons/react/solid'
|
import { XIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
|
|
||||||
export function MultipleChoiceAnswers(props: {
|
export function MultipleChoiceAnswers(props: {
|
||||||
|
answers: string[]
|
||||||
setAnswers: (answers: string[]) => void
|
setAnswers: (answers: string[]) => void
|
||||||
}) {
|
}) {
|
||||||
const [answers, setInternalAnswers] = useState(['', '', ''])
|
const { answers, setAnswers } = props
|
||||||
|
|
||||||
const setAnswer = (i: number, answer: string) => {
|
const setAnswer = (i: number, answer: string) => {
|
||||||
const newAnswers = setElement(answers, i, answer)
|
const newAnswers = setElement(answers, i, answer)
|
||||||
setInternalAnswers(newAnswers)
|
setAnswers(newAnswers)
|
||||||
props.setAnswers(newAnswers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeAnswer = (i: number) => {
|
const removeAnswer = (i: number) => {
|
||||||
const newAnswers = answers.slice(0, i).concat(answers.slice(i + 1))
|
const newAnswers = answers.slice(0, i).concat(answers.slice(i + 1))
|
||||||
setInternalAnswers(newAnswers)
|
setAnswers(newAnswers)
|
||||||
props.setAnswers(newAnswers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addAnswer = () => setAnswer(answers.length, '')
|
const addAnswer = () => setAnswer(answers.length, '')
|
||||||
|
@ -40,10 +37,10 @@ export function MultipleChoiceAnswers(props: {
|
||||||
/>
|
/>
|
||||||
{answers.length > 2 && (
|
{answers.length > 2 && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-xs btn-outline ml-2"
|
className="-mr-2 rounded p-2"
|
||||||
onClick={() => removeAnswer(i)}
|
onClick={() => removeAnswer(i)}
|
||||||
>
|
>
|
||||||
<XIcon className="h-4 w-4 flex-shrink-0" />
|
<XIcon className="h-5 w-5 flex-shrink-0" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -22,7 +22,10 @@ export function ChoicesToggleGroup(props: {
|
||||||
} = props
|
} = props
|
||||||
return (
|
return (
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className={clsx(className, 'flex flex-row flex-wrap items-center gap-3')}
|
className={clsx(
|
||||||
|
className,
|
||||||
|
'flex flex-row flex-wrap items-center gap-2 sm:gap-3'
|
||||||
|
)}
|
||||||
value={currentChoice.toString()}
|
value={currentChoice.toString()}
|
||||||
onChange={setChoice}
|
onChange={setChoice}
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,14 +3,17 @@ import algoliasearch from 'algoliasearch/lite'
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params'
|
import {
|
||||||
|
QuerySortOptions,
|
||||||
|
Sort,
|
||||||
|
useQueryAndSortParams,
|
||||||
|
} from '../hooks/use-sort-and-query-params'
|
||||||
import {
|
import {
|
||||||
ContractHighlightOptions,
|
ContractHighlightOptions,
|
||||||
ContractsGrid,
|
ContractsGrid,
|
||||||
} from './contract/contracts-grid'
|
} from './contract/contracts-grid'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Spacer } from './layout/spacer'
|
|
||||||
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
||||||
import { useFollows } from 'web/hooks/use-follows'
|
import { useFollows } from 'web/hooks/use-follows'
|
||||||
import { track, trackCallback } from 'web/lib/service/analytics'
|
import { track, trackCallback } from 'web/lib/service/analytics'
|
||||||
|
@ -21,6 +24,7 @@ import { PillButton } from './buttons/pill-button'
|
||||||
import { range, sortBy } from 'lodash'
|
import { range, sortBy } from 'lodash'
|
||||||
import { DEFAULT_CATEGORY_GROUPS } from 'common/categories'
|
import { DEFAULT_CATEGORY_GROUPS } from 'common/categories'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
const searchClient = algoliasearch(
|
const searchClient = algoliasearch(
|
||||||
'GJQPAYENIF',
|
'GJQPAYENIF',
|
||||||
|
@ -45,12 +49,8 @@ export const DEFAULT_SORT = 'score'
|
||||||
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
|
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
|
||||||
|
|
||||||
export function ContractSearch(props: {
|
export function ContractSearch(props: {
|
||||||
user: User | null | undefined
|
user?: User | null
|
||||||
querySortOptions?: {
|
querySortOptions?: { defaultFilter?: filter } & QuerySortOptions
|
||||||
defaultSort: Sort
|
|
||||||
defaultFilter?: filter
|
|
||||||
shouldLoadFromStorage?: boolean
|
|
||||||
}
|
|
||||||
additionalFilter?: {
|
additionalFilter?: {
|
||||||
creatorId?: string
|
creatorId?: string
|
||||||
tag?: string
|
tag?: string
|
||||||
|
@ -66,6 +66,7 @@ export function ContractSearch(props: {
|
||||||
hideGroupLink?: boolean
|
hideGroupLink?: boolean
|
||||||
hideQuickBet?: boolean
|
hideQuickBet?: boolean
|
||||||
}
|
}
|
||||||
|
headerClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
|
@ -77,6 +78,7 @@ export function ContractSearch(props: {
|
||||||
showPlaceHolder,
|
showPlaceHolder,
|
||||||
cardHideOptions,
|
cardHideOptions,
|
||||||
highlightOptions,
|
highlightOptions,
|
||||||
|
headerClassName,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const memberGroups = (useMemberGroups(user?.id) ?? []).filter(
|
const memberGroups = (useMemberGroups(user?.id) ?? []).filter(
|
||||||
|
@ -99,11 +101,8 @@ export function ContractSearch(props: {
|
||||||
|
|
||||||
const follows = useFollows(user?.id)
|
const follows = useFollows(user?.id)
|
||||||
|
|
||||||
const { shouldLoadFromStorage, defaultSort } = querySortOptions ?? {}
|
const { query, setQuery, sort, setSort } =
|
||||||
const { query, setQuery, sort, setSort } = useQueryAndSortParams({
|
useQueryAndSortParams(querySortOptions)
|
||||||
defaultSort,
|
|
||||||
shouldLoadFromStorage,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [filter, setFilter] = useState<filter>(
|
const [filter, setFilter] = useState<filter>(
|
||||||
querySortOptions?.defaultFilter ?? 'open'
|
querySortOptions?.defaultFilter ?? 'open'
|
||||||
|
@ -257,87 +256,90 @@ export function ContractSearch(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col className="h-full">
|
||||||
<Row className="gap-1 sm:gap-2">
|
<Col
|
||||||
<input
|
className={clsx(
|
||||||
type="text"
|
'bg-base-200 sticky top-0 z-20 gap-3 pb-3',
|
||||||
value={query}
|
headerClassName
|
||||||
onChange={(e) => updateQuery(e.target.value)}
|
|
||||||
onBlur={trackCallback('search', { query })}
|
|
||||||
placeholder={showPlaceHolder ? `Search ${filter} markets` : ''}
|
|
||||||
className="input input-bordered w-full"
|
|
||||||
/>
|
|
||||||
{!query && (
|
|
||||||
<select
|
|
||||||
className="select select-bordered"
|
|
||||||
value={filter}
|
|
||||||
onChange={(e) => selectFilter(e.target.value as filter)}
|
|
||||||
>
|
|
||||||
<option value="open">Open</option>
|
|
||||||
<option value="closed">Closed</option>
|
|
||||||
<option value="resolved">Resolved</option>
|
|
||||||
<option value="all">All</option>
|
|
||||||
</select>
|
|
||||||
)}
|
)}
|
||||||
{!hideOrderSelector && !query && (
|
>
|
||||||
<select
|
<Row className="gap-1 sm:gap-2">
|
||||||
className="select select-bordered"
|
<input
|
||||||
value={sort}
|
type="text"
|
||||||
onChange={(e) => selectSort(e.target.value as Sort)}
|
value={query}
|
||||||
>
|
onChange={(e) => updateQuery(e.target.value)}
|
||||||
{sortOptions.map((option) => (
|
onBlur={trackCallback('search', { query })}
|
||||||
<option key={option.value} value={option.value}>
|
placeholder={showPlaceHolder ? `Search ${filter} markets` : ''}
|
||||||
{option.label}
|
className="input input-bordered w-full"
|
||||||
</option>
|
/>
|
||||||
))}
|
{!query && (
|
||||||
</select>
|
<select
|
||||||
)}
|
className="select select-bordered"
|
||||||
</Row>
|
value={filter}
|
||||||
|
onChange={(e) => selectFilter(e.target.value as filter)}
|
||||||
<Spacer h={3} />
|
|
||||||
|
|
||||||
{pillsEnabled && (
|
|
||||||
<Row className="scrollbar-hide items-start gap-2 overflow-x-auto">
|
|
||||||
<PillButton
|
|
||||||
key={'all'}
|
|
||||||
selected={pillFilter === undefined}
|
|
||||||
onSelect={selectPill(undefined)}
|
|
||||||
>
|
|
||||||
All
|
|
||||||
</PillButton>
|
|
||||||
<PillButton
|
|
||||||
key={'personal'}
|
|
||||||
selected={pillFilter === 'personal'}
|
|
||||||
onSelect={selectPill('personal')}
|
|
||||||
>
|
|
||||||
{user ? 'For you' : 'Featured'}
|
|
||||||
</PillButton>
|
|
||||||
|
|
||||||
{user && (
|
|
||||||
<PillButton
|
|
||||||
key={'your-bets'}
|
|
||||||
selected={pillFilter === 'your-bets'}
|
|
||||||
onSelect={selectPill('your-bets')}
|
|
||||||
>
|
>
|
||||||
Your bets
|
<option value="open">Open</option>
|
||||||
</PillButton>
|
<option value="closed">Closed</option>
|
||||||
|
<option value="resolved">Resolved</option>
|
||||||
|
<option value="all">All</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
{!hideOrderSelector && !query && (
|
||||||
|
<select
|
||||||
|
className="select select-bordered"
|
||||||
|
value={sort}
|
||||||
|
onChange={(e) => selectSort(e.target.value as Sort)}
|
||||||
|
>
|
||||||
|
{sortOptions.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{pillGroups.map(({ name, slug }) => {
|
|
||||||
return (
|
|
||||||
<PillButton
|
|
||||||
key={slug}
|
|
||||||
selected={pillFilter === slug}
|
|
||||||
onSelect={selectPill(slug)}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</PillButton>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
|
||||||
|
|
||||||
<Spacer h={3} />
|
{pillsEnabled && (
|
||||||
|
<Row className="scrollbar-hide items-start gap-2 overflow-x-auto">
|
||||||
|
<PillButton
|
||||||
|
key={'all'}
|
||||||
|
selected={pillFilter === undefined}
|
||||||
|
onSelect={selectPill(undefined)}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</PillButton>
|
||||||
|
<PillButton
|
||||||
|
key={'personal'}
|
||||||
|
selected={pillFilter === 'personal'}
|
||||||
|
onSelect={selectPill('personal')}
|
||||||
|
>
|
||||||
|
{user ? 'For you' : 'Featured'}
|
||||||
|
</PillButton>
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<PillButton
|
||||||
|
key={'your-bets'}
|
||||||
|
selected={pillFilter === 'your-bets'}
|
||||||
|
onSelect={selectPill('your-bets')}
|
||||||
|
>
|
||||||
|
Your bets
|
||||||
|
</PillButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pillGroups.map(({ name, slug }) => {
|
||||||
|
return (
|
||||||
|
<PillButton
|
||||||
|
key={slug}
|
||||||
|
selected={pillFilter === slug}
|
||||||
|
onSelect={selectPill(slug)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</PillButton>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
|
||||||
{filter === 'personal' &&
|
{filter === 'personal' &&
|
||||||
(follows ?? []).length === 0 &&
|
(follows ?? []).length === 0 &&
|
||||||
|
|
|
@ -76,7 +76,8 @@ export function ContractCard(props: {
|
||||||
<Col className="relative flex-1 gap-3 pr-1">
|
<Col className="relative flex-1 gap-3 pr-1">
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'peer absolute -left-6 -top-4 -bottom-4 right-0 z-10'
|
'peer absolute -left-6 -top-4 -bottom-4 z-10',
|
||||||
|
hideQuickBet ? '-right-20' : 'right-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{onClick ? (
|
{onClick ? (
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { TextEditor, useTextEditor } from 'web/components/editor'
|
||||||
import { Button } from '../button'
|
import { Button } from '../button'
|
||||||
import { Spacer } from '../layout/spacer'
|
import { Spacer } from '../layout/spacer'
|
||||||
import { Editor, Content as ContentType } from '@tiptap/react'
|
import { Editor, Content as ContentType } from '@tiptap/react'
|
||||||
import { appendToEditor } from '../editor/utils'
|
import { insertContent } from '../editor/utils'
|
||||||
|
|
||||||
export function ContractDescription(props: {
|
export function ContractDescription(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -95,7 +95,8 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) {
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditing(true)
|
setEditing(true)
|
||||||
appendToEditor(editor, `<p>${editTimestamp()}</p>`)
|
editor?.commands.focus('end')
|
||||||
|
insertContent(editor, `<p>${editTimestamp()}</p>`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Edit description
|
Edit description
|
||||||
|
@ -127,7 +128,7 @@ function EditQuestion(props: {
|
||||||
|
|
||||||
function joinContent(oldContent: ContentType, newContent: string) {
|
function joinContent(oldContent: ContentType, newContent: string) {
|
||||||
const editor = new Editor({ content: oldContent, extensions: exhibitExts })
|
const editor = new Editor({ content: oldContent, extensions: exhibitExts })
|
||||||
appendToEditor(editor, newContent)
|
insertContent(editor, newContent)
|
||||||
return editor.getJSON()
|
return editor.getJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { Col } from 'web/components/layout/col'
|
||||||
import { ContractGroupsList } from 'web/components/groups/contract-groups-list'
|
import { ContractGroupsList } from 'web/components/groups/contract-groups-list'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
import { appendToEditor } from '../editor/utils'
|
import { insertContent } from '../editor/utils'
|
||||||
|
|
||||||
export type ShowTime = 'resolve-date' | 'close-date'
|
export type ShowTime = 'resolve-date' | 'close-date'
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ export function ContractDetails(props: {
|
||||||
const groupInfo = (
|
const groupInfo = (
|
||||||
<Row>
|
<Row>
|
||||||
<UserGroupIcon className="mx-1 inline h-5 w-5 shrink-0" />
|
<UserGroupIcon className="mx-1 inline h-5 w-5 shrink-0" />
|
||||||
<span className={'line-clamp-1'}>
|
<span className="truncate">
|
||||||
{groupToDisplay ? groupToDisplay.name : 'No group'}
|
{groupToDisplay ? groupToDisplay.name : 'No group'}
|
||||||
</span>
|
</span>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -283,7 +283,8 @@ function EditableCloseDate(props: {
|
||||||
const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a')
|
const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a')
|
||||||
|
|
||||||
const editor = new Editor({ content, extensions: exhibitExts })
|
const editor = new Editor({ content, extensions: exhibitExts })
|
||||||
appendToEditor(
|
editor.commands.focus('end')
|
||||||
|
insertContent(
|
||||||
editor,
|
editor,
|
||||||
`<br><p>Close date updated to ${formattedCloseDate}</p>`
|
`<br><p>Close date updated to ${formattedCloseDate}</p>`
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,18 +8,17 @@ import { Spacer } from '../layout/spacer'
|
||||||
import { Tabs } from '../layout/tabs'
|
import { Tabs } from '../layout/tabs'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||||
import { LiquidityProvision } from 'common/liquidity-provision'
|
|
||||||
import { useComments } from 'web/hooks/use-comments'
|
import { useComments } from 'web/hooks/use-comments'
|
||||||
|
import { useLiquidity } from 'web/hooks/use-liquidity'
|
||||||
|
|
||||||
export function ContractTabs(props: {
|
export function ContractTabs(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
liquidityProvisions: LiquidityProvision[]
|
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
}) {
|
}) {
|
||||||
const { contract, user, bets, tips, liquidityProvisions } = props
|
const { contract, user, bets, tips } = props
|
||||||
const { outcomeType } = contract
|
const { outcomeType } = contract
|
||||||
|
|
||||||
const userBets = user && bets.filter((bet) => bet.userId === user.id)
|
const userBets = user && bets.filter((bet) => bet.userId === user.id)
|
||||||
|
@ -27,6 +26,9 @@ export function ContractTabs(props: {
|
||||||
(bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0
|
(bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const liquidityProvisions =
|
||||||
|
useLiquidity(contract.id)?.filter((l) => !l.isAnte && l.amount > 0) ?? []
|
||||||
|
|
||||||
// Load comments here, so the badge count will be correct
|
// Load comments here, so the badge count will be correct
|
||||||
const updatedComments = useComments(contract.id)
|
const updatedComments = useComments(contract.id)
|
||||||
const comments = updatedComments ?? props.comments
|
const comments = updatedComments ?? props.comments
|
||||||
|
|
|
@ -22,8 +22,14 @@ import { mentionSuggestion } from './editor/mention-suggestion'
|
||||||
import { DisplayMention } from './editor/mention'
|
import { DisplayMention } from './editor/mention'
|
||||||
import Iframe from 'common/util/tiptap-iframe'
|
import Iframe from 'common/util/tiptap-iframe'
|
||||||
import TiptapTweet from './editor/tiptap-tweet'
|
import TiptapTweet from './editor/tiptap-tweet'
|
||||||
import { CodeIcon, PhotographIcon } from '@heroicons/react/solid'
|
|
||||||
import { EmbedModal } from './editor/embed-modal'
|
import { EmbedModal } from './editor/embed-modal'
|
||||||
|
import {
|
||||||
|
CodeIcon,
|
||||||
|
PhotographIcon,
|
||||||
|
PresentationChartLineIcon,
|
||||||
|
} from '@heroicons/react/solid'
|
||||||
|
import { MarketModal } from './editor/market-modal'
|
||||||
|
import { insertContent } from './editor/utils'
|
||||||
|
|
||||||
const DisplayImage = Image.configure({
|
const DisplayImage = Image.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
|
@ -103,7 +109,7 @@ export function useTextEditor(props: {
|
||||||
// If the pasted content is iframe code, directly inject it
|
// If the pasted content is iframe code, directly inject it
|
||||||
const text = event.clipboardData?.getData('text/plain').trim() ?? ''
|
const text = event.clipboardData?.getData('text/plain').trim() ?? ''
|
||||||
if (isValidIframe(text)) {
|
if (isValidIframe(text)) {
|
||||||
editor.chain().insertContent(text).run()
|
insertContent(editor, text)
|
||||||
return true // Prevent the code from getting pasted as text
|
return true // Prevent the code from getting pasted as text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,6 +136,7 @@ export function TextEditor(props: {
|
||||||
}) {
|
}) {
|
||||||
const { editor, upload, children } = props
|
const { editor, upload, children } = props
|
||||||
const [iframeOpen, setIframeOpen] = useState(false)
|
const [iframeOpen, setIframeOpen] = useState(false)
|
||||||
|
const [marketOpen, setMarketOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -139,16 +146,15 @@ export function TextEditor(props: {
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
{/* Toolbar, with buttons for images and embeds */}
|
{/* Toolbar, with buttons for images and embeds */}
|
||||||
<div className="flex h-9 items-center gap-5 pl-4 pr-1">
|
<div className="flex h-9 items-center gap-5 pl-4 pr-1">
|
||||||
<div className="flex items-center">
|
<div className="tooltip flex items-center" data-tip="Add image">
|
||||||
<FileUploadButton
|
<FileUploadButton
|
||||||
onFiles={upload.mutate}
|
onFiles={upload.mutate}
|
||||||
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
|
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
|
||||||
>
|
>
|
||||||
<PhotographIcon className="h-5 w-5" aria-hidden="true" />
|
<PhotographIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
<span className="sr-only">Upload an image</span>
|
|
||||||
</FileUploadButton>
|
</FileUploadButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="tooltip flex items-center" data-tip="Add embed">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIframeOpen(true)}
|
onClick={() => setIframeOpen(true)}
|
||||||
|
@ -160,7 +166,23 @@ export function TextEditor(props: {
|
||||||
setOpen={setIframeOpen}
|
setOpen={setIframeOpen}
|
||||||
/>
|
/>
|
||||||
<CodeIcon className="h-5 w-5" aria-hidden="true" />
|
<CodeIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
<span className="sr-only">Embed an iframe</span>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="tooltip flex items-center" data-tip="Add market">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMarketOpen(true)}
|
||||||
|
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
|
||||||
|
>
|
||||||
|
<MarketModal
|
||||||
|
editor={editor}
|
||||||
|
open={marketOpen}
|
||||||
|
setOpen={setMarketOpen}
|
||||||
|
/>
|
||||||
|
<PresentationChartLineIcon
|
||||||
|
className="h-5 w-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/* Spacer that also focuses editor on click */}
|
{/* Spacer that also focuses editor on click */}
|
||||||
|
|
86
web/components/editor/market-modal.tsx
Normal file
86
web/components/editor/market-modal.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { Editor } from '@tiptap/react'
|
||||||
|
import { Contract } from 'common/contract'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Button } from '../button'
|
||||||
|
import { ContractSearch } from '../contract-search'
|
||||||
|
import { Col } from '../layout/col'
|
||||||
|
import { Modal } from '../layout/modal'
|
||||||
|
import { Row } from '../layout/row'
|
||||||
|
import { LoadingIndicator } from '../loading-indicator'
|
||||||
|
import { embedCode } from '../share-embed-button'
|
||||||
|
import { insertContent } from './utils'
|
||||||
|
|
||||||
|
export function MarketModal(props: {
|
||||||
|
editor: Editor | null
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { editor, open, setOpen } = props
|
||||||
|
|
||||||
|
const [contracts, setContracts] = useState<Contract[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
async function addContract(contract: Contract) {
|
||||||
|
if (contracts.map((c) => c.id).includes(contract.id)) {
|
||||||
|
setContracts(contracts.filter((c) => c.id !== contract.id))
|
||||||
|
} else setContracts([...contracts, contract])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doneAddingContracts() {
|
||||||
|
setLoading(true)
|
||||||
|
insertContent(editor, ...contracts.map(embedCode))
|
||||||
|
setLoading(false)
|
||||||
|
setOpen(false)
|
||||||
|
setContracts([])
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}>
|
||||||
|
<Col className="h-[85vh] w-full gap-4 rounded-md bg-white">
|
||||||
|
<Row className="p-8 pb-0">
|
||||||
|
<div className={'text-xl text-indigo-700'}>Embed a market</div>
|
||||||
|
|
||||||
|
{!loading && (
|
||||||
|
<Row className="grow justify-end gap-4">
|
||||||
|
{contracts.length > 0 && (
|
||||||
|
<Button onClick={doneAddingContracts} color={'indigo'}>
|
||||||
|
Embed {contracts.length} question
|
||||||
|
{contracts.length > 1 && 's'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button onClick={() => setContracts([])} color="gray">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div className="w-full justify-center">
|
||||||
|
<LoadingIndicator />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="overflow-y-scroll sm:px-8">
|
||||||
|
<ContractSearch
|
||||||
|
hideOrderSelector
|
||||||
|
onContractClick={addContract}
|
||||||
|
overrideGridClassName={
|
||||||
|
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
|
||||||
|
}
|
||||||
|
showPlaceHolder
|
||||||
|
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
||||||
|
querySortOptions={{ disableQueryString: true }}
|
||||||
|
highlightOptions={{
|
||||||
|
contractIds: contracts.map((c) => c.id),
|
||||||
|
highlightClassName:
|
||||||
|
'!bg-indigo-100 outline outline-2 outline-indigo-300',
|
||||||
|
}}
|
||||||
|
additionalFilter={{}} /* hide pills */
|
||||||
|
headerClassName="bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
import { Editor, Content } from '@tiptap/react'
|
import { Editor, Content } from '@tiptap/react'
|
||||||
|
|
||||||
export function appendToEditor(editor: Editor | null, content: Content) {
|
export function insertContent(editor: Editor | null, ...contents: Content[]) {
|
||||||
editor
|
if (!editor) {
|
||||||
?.chain()
|
return
|
||||||
.focus('end')
|
}
|
||||||
.createParagraphNear()
|
|
||||||
.insertContent(content)
|
let e = editor.chain()
|
||||||
.run()
|
for (const content of contents) {
|
||||||
|
e = e.createParagraphNear().insertContent(content)
|
||||||
|
}
|
||||||
|
e.run()
|
||||||
}
|
}
|
||||||
|
|
7
web/components/fullscreen-confetti.tsx
Normal file
7
web/components/fullscreen-confetti.tsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import Confetti, { Props as ConfettiProps } from 'react-confetti'
|
||||||
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
|
|
||||||
|
export function FullscreenConfetti(props: ConfettiProps) {
|
||||||
|
const { width, height } = useWindowSize()
|
||||||
|
return <Confetti {...props} width={width} height={height} />
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import { Content, useTextEditor } from 'web/components/editor'
|
||||||
import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications'
|
import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications'
|
||||||
import { ChevronDownIcon, UsersIcon } from '@heroicons/react/outline'
|
import { ChevronDownIcon, UsersIcon } from '@heroicons/react/outline'
|
||||||
import { setNotificationsAsSeen } from 'web/pages/notifications'
|
import { setNotificationsAsSeen } from 'web/pages/notifications'
|
||||||
|
import { usePrivateUser } from 'web/hooks/use-user'
|
||||||
|
|
||||||
export function GroupChat(props: {
|
export function GroupChat(props: {
|
||||||
messages: Comment[]
|
messages: Comment[]
|
||||||
|
@ -29,6 +30,9 @@ export function GroupChat(props: {
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
}) {
|
}) {
|
||||||
const { messages, user, group, tips } = props
|
const { messages, user, group, tips } = props
|
||||||
|
|
||||||
|
const privateUser = usePrivateUser(user?.id)
|
||||||
|
|
||||||
const { editor, upload } = useTextEditor({
|
const { editor, upload } = useTextEditor({
|
||||||
simple: true,
|
simple: true,
|
||||||
placeholder: 'Send a message',
|
placeholder: 'Send a message',
|
||||||
|
@ -175,6 +179,15 @@ export function GroupChat(props: {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{privateUser && (
|
||||||
|
<GroupChatNotificationsIcon
|
||||||
|
group={group}
|
||||||
|
privateUser={privateUser}
|
||||||
|
shouldSetAsSeen={true}
|
||||||
|
hidden={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -248,6 +261,7 @@ export function GroupChatInBubble(props: {
|
||||||
group={group}
|
group={group}
|
||||||
privateUser={privateUser}
|
privateUser={privateUser}
|
||||||
shouldSetAsSeen={shouldShowChat}
|
shouldSetAsSeen={shouldShowChat}
|
||||||
|
hidden={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
@ -259,8 +273,9 @@ function GroupChatNotificationsIcon(props: {
|
||||||
group: Group
|
group: Group
|
||||||
privateUser: PrivateUser
|
privateUser: PrivateUser
|
||||||
shouldSetAsSeen: boolean
|
shouldSetAsSeen: boolean
|
||||||
|
hidden: boolean
|
||||||
}) {
|
}) {
|
||||||
const { privateUser, group, shouldSetAsSeen } = props
|
const { privateUser, group, shouldSetAsSeen, hidden } = props
|
||||||
const preferredNotificationsForThisGroup = useUnseenPreferredNotifications(
|
const preferredNotificationsForThisGroup = useUnseenPreferredNotifications(
|
||||||
privateUser,
|
privateUser,
|
||||||
{
|
{
|
||||||
|
@ -282,7 +297,9 @@ function GroupChatNotificationsIcon(props: {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
preferredNotificationsForThisGroup.length > 0 && !shouldSetAsSeen
|
!hidden &&
|
||||||
|
preferredNotificationsForThisGroup.length > 0 &&
|
||||||
|
!shouldSetAsSeen
|
||||||
? 'absolute right-4 top-4 h-3 w-3 rounded-full border-2 border-white bg-red-500'
|
? 'absolute right-4 top-4 h-3 w-3 rounded-full border-2 border-white bg-red-500'
|
||||||
: 'hidden'
|
: 'hidden'
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@ export function ControlledTabs(props: TabProps & { activeIndex: number }) {
|
||||||
className,
|
className,
|
||||||
currentPageForAnalytics,
|
currentPageForAnalytics,
|
||||||
} = props
|
} = props
|
||||||
const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav
|
<nav
|
||||||
|
@ -64,7 +63,11 @@ export function ControlledTabs(props: TabProps & { activeIndex: number }) {
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
{activeTab?.content}
|
{tabs.map((tab, i) => (
|
||||||
|
<div key={i} className={i === activeIndex ? 'block' : 'hidden'}>
|
||||||
|
{tab.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,11 @@ import { copyToClipboard } from 'web/lib/util/copy'
|
||||||
import { ToastClipboard } from 'web/components/toast-clipboard'
|
import { ToastClipboard } from 'web/components/toast-clipboard'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
|
||||||
function copyEmbedCode(contract: Contract) {
|
export function embedCode(contract: Contract) {
|
||||||
const title = contract.question
|
const title = contract.question
|
||||||
const src = `https://${DOMAIN}/embed${contractPath(contract)}`
|
const src = `https://${DOMAIN}/embed${contractPath(contract)}`
|
||||||
|
|
||||||
const embedCode = `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>`
|
return `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>`
|
||||||
|
|
||||||
copyToClipboard(embedCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShareEmbedButton(props: {
|
export function ShareEmbedButton(props: {
|
||||||
|
@ -29,7 +27,7 @@ export function ShareEmbedButton(props: {
|
||||||
as="div"
|
as="div"
|
||||||
className="relative z-10 flex-shrink-0"
|
className="relative z-10 flex-shrink-0"
|
||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
copyEmbedCode(contract)
|
copyToClipboard(embedCode(contract))
|
||||||
track('copy embed code')
|
track('copy embed code')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,14 +4,8 @@ import { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { LinkIcon } from '@heroicons/react/solid'
|
import { LinkIcon } from '@heroicons/react/solid'
|
||||||
import { PencilIcon } from '@heroicons/react/outline'
|
import { PencilIcon } from '@heroicons/react/outline'
|
||||||
import Confetti from 'react-confetti'
|
|
||||||
|
|
||||||
import {
|
import { getPortfolioHistory, User } from 'web/lib/firebase/users'
|
||||||
follow,
|
|
||||||
getPortfolioHistory,
|
|
||||||
unfollow,
|
|
||||||
User,
|
|
||||||
} from 'web/lib/firebase/users'
|
|
||||||
import { CreatorContractsList } from './contract/contracts-grid'
|
import { CreatorContractsList } from './contract/contracts-grid'
|
||||||
import { SEO } from './SEO'
|
import { SEO } from './SEO'
|
||||||
import { Page } from './page'
|
import { Page } from './page'
|
||||||
|
@ -24,15 +18,14 @@ import { Row } from './layout/row'
|
||||||
import { genHash } from 'common/util/random'
|
import { genHash } from 'common/util/random'
|
||||||
import { QueryUncontrolledTabs } from './layout/tabs'
|
import { QueryUncontrolledTabs } from './layout/tabs'
|
||||||
import { UserCommentsList } from './comments-list'
|
import { UserCommentsList } from './comments-list'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
|
||||||
import { Comment, getUsersComments } from 'web/lib/firebase/comments'
|
import { Comment, getUsersComments } from 'web/lib/firebase/comments'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { getContractFromId, listContracts } from 'web/lib/firebase/contracts'
|
import { getContractFromId, listContracts } from 'web/lib/firebase/contracts'
|
||||||
import { LoadingIndicator } from './loading-indicator'
|
import { LoadingIndicator } from './loading-indicator'
|
||||||
|
import { FullscreenConfetti } from 'web/components/fullscreen-confetti'
|
||||||
import { BetsList } from './bets-list'
|
import { BetsList } from './bets-list'
|
||||||
import { FollowersButton, FollowingButton } from './following-button'
|
import { FollowersButton, FollowingButton } from './following-button'
|
||||||
import { useFollows } from 'web/hooks/use-follows'
|
import { UserFollowButton } from './follow-button'
|
||||||
import { FollowButton } from './follow-button'
|
|
||||||
import { PortfolioMetrics } from 'common/user'
|
import { PortfolioMetrics } from 'common/user'
|
||||||
import { GroupsButton } from 'web/components/groups/groups-button'
|
import { GroupsButton } from 'web/components/groups/groups-button'
|
||||||
import { PortfolioValueSection } from './portfolio/portfolio-value-section'
|
import { PortfolioValueSection } from './portfolio/portfolio-value-section'
|
||||||
|
@ -88,7 +81,6 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
||||||
Dictionary<Contract> | undefined
|
Dictionary<Contract> | undefined
|
||||||
>()
|
>()
|
||||||
const [showConfetti, setShowConfetti] = useState(false)
|
const [showConfetti, setShowConfetti] = useState(false)
|
||||||
const { width, height } = useWindowSize()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const claimedMana = router.query['claimed-mana'] === 'yes'
|
const claimedMana = router.query['claimed-mana'] === 'yes'
|
||||||
|
@ -120,19 +112,8 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
||||||
}
|
}
|
||||||
}, [userBets, usersComments])
|
}, [userBets, usersComments])
|
||||||
|
|
||||||
const yourFollows = useFollows(currentUser?.id)
|
|
||||||
const isFollowing = yourFollows?.includes(user.id)
|
|
||||||
const profit = user.profitCached.allTime
|
const profit = user.profitCached.allTime
|
||||||
|
|
||||||
const onFollow = () => {
|
|
||||||
if (!currentUser) return
|
|
||||||
follow(currentUser.id, user.id)
|
|
||||||
}
|
|
||||||
const onUnfollow = () => {
|
|
||||||
if (!currentUser) return
|
|
||||||
unfollow(currentUser.id, user.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page key={user.id}>
|
<Page key={user.id}>
|
||||||
<SEO
|
<SEO
|
||||||
|
@ -141,12 +122,7 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
||||||
url={`/${user.username}`}
|
url={`/${user.username}`}
|
||||||
/>
|
/>
|
||||||
{showConfetti && (
|
{showConfetti && (
|
||||||
<Confetti
|
<FullscreenConfetti recycle={false} numberOfPieces={300} />
|
||||||
width={width ? width : 500}
|
|
||||||
height={height ? height : 500}
|
|
||||||
recycle={false}
|
|
||||||
numberOfPieces={300}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{/* Banner image up top, with an circle avatar overlaid */}
|
{/* Banner image up top, with an circle avatar overlaid */}
|
||||||
<div
|
<div
|
||||||
|
@ -167,13 +143,7 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
||||||
|
|
||||||
{/* 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-4 mr-4">
|
||||||
{!isCurrentUser && (
|
{!isCurrentUser && <UserFollowButton userId={user.id} />}
|
||||||
<FollowButton
|
|
||||||
isFollowing={isFollowing}
|
|
||||||
onFollow={onFollow}
|
|
||||||
onUnfollow={onUnfollow}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isCurrentUser && (
|
{isCurrentUser && (
|
||||||
<SiteLink className="btn" href="/profile">
|
<SiteLink className="btn" href="/profile">
|
||||||
<PencilIcon className="h-5 w-5" />{' '}
|
<PencilIcon className="h-5 w-5" />{' '}
|
||||||
|
|
|
@ -25,12 +25,18 @@ export function getSavedSort() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useQueryAndSortParams(options?: {
|
export interface QuerySortOptions {
|
||||||
defaultSort?: Sort
|
defaultSort?: Sort
|
||||||
shouldLoadFromStorage?: boolean
|
shouldLoadFromStorage?: boolean
|
||||||
}) {
|
/** Use normal react state instead of url query string */
|
||||||
const { defaultSort = DEFAULT_SORT, shouldLoadFromStorage = true } =
|
disableQueryString?: boolean
|
||||||
options ?? {}
|
}
|
||||||
|
|
||||||
|
export function useQueryAndSortParams({
|
||||||
|
defaultSort = DEFAULT_SORT,
|
||||||
|
shouldLoadFromStorage = true,
|
||||||
|
disableQueryString,
|
||||||
|
}: QuerySortOptions = {}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const { s: sort, q: query } = router.query as {
|
const { s: sort, q: query } = router.query as {
|
||||||
|
@ -68,7 +74,9 @@ export function useQueryAndSortParams(options?: {
|
||||||
|
|
||||||
const setQuery = (query: string | undefined) => {
|
const setQuery = (query: string | undefined) => {
|
||||||
setQueryState(query)
|
setQueryState(query)
|
||||||
pushQuery(query)
|
if (!disableQueryString) {
|
||||||
|
pushQuery(query)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -86,10 +94,13 @@ export function useQueryAndSortParams(options?: {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// use normal state if querydisableQueryString
|
||||||
|
const [sortState, setSortState] = useState(defaultSort)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sort: sort ?? defaultSort,
|
sort: disableQueryString ? sortState : sort ?? defaultSort,
|
||||||
query: queryState ?? '',
|
query: queryState ?? '',
|
||||||
setSort,
|
setSort: disableQueryString ? setSortState : setSort,
|
||||||
setQuery,
|
setQuery,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,7 @@ import { Leaderboard } from 'web/components/leaderboard'
|
||||||
import { resolvedPayout } from 'common/calculate'
|
import { resolvedPayout } from 'common/calculate'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { ContractTabs } from 'web/components/contract/contract-tabs'
|
import { ContractTabs } from 'web/components/contract/contract-tabs'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { FullscreenConfetti } from 'web/components/fullscreen-confetti'
|
||||||
import Confetti from 'react-confetti'
|
|
||||||
import { NumericBetPanel } from 'web/components/numeric-bet-panel'
|
import { NumericBetPanel } from 'web/components/numeric-bet-panel'
|
||||||
import { NumericResolutionPanel } from 'web/components/numeric-resolution-panel'
|
import { NumericResolutionPanel } from 'web/components/numeric-resolution-panel'
|
||||||
import { useIsIframe } from 'web/hooks/use-is-iframe'
|
import { useIsIframe } from 'web/hooks/use-is-iframe'
|
||||||
|
@ -36,7 +35,6 @@ import { CPMMBinaryContract } from 'common/contract'
|
||||||
import { AlertBox } from 'web/components/alert-box'
|
import { AlertBox } from 'web/components/alert-box'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
||||||
import { useLiquidity } from 'web/hooks/use-liquidity'
|
|
||||||
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'
|
||||||
|
@ -161,15 +159,12 @@ export function ContractPageContent(
|
||||||
})
|
})
|
||||||
|
|
||||||
const bets = useBets(contract.id) ?? props.bets
|
const bets = useBets(contract.id) ?? props.bets
|
||||||
const liquidityProvisions =
|
|
||||||
useLiquidity(contract.id)?.filter((l) => !l.isAnte && l.amount > 0) ?? []
|
|
||||||
// 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)
|
||||||
|
|
||||||
const tips = useTipTxns({ contractId: contract.id })
|
const tips = useTipTxns({ contractId: contract.id })
|
||||||
|
|
||||||
const { width, height } = useWindowSize()
|
|
||||||
|
|
||||||
const [showConfetti, setShowConfetti] = useState(false)
|
const [showConfetti, setShowConfetti] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -196,12 +191,7 @@ export function ContractPageContent(
|
||||||
return (
|
return (
|
||||||
<Page rightSidebar={rightSidebar}>
|
<Page rightSidebar={rightSidebar}>
|
||||||
{showConfetti && (
|
{showConfetti && (
|
||||||
<Confetti
|
<FullscreenConfetti recycle={false} numberOfPieces={300} />
|
||||||
width={width ? width : 500}
|
|
||||||
height={height ? height : 500}
|
|
||||||
recycle={false}
|
|
||||||
numberOfPieces={300}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ogCardProps && (
|
{ogCardProps && (
|
||||||
|
@ -267,7 +257,6 @@ export function ContractPageContent(
|
||||||
<ContractTabs
|
<ContractTabs
|
||||||
contract={contract}
|
contract={contract}
|
||||||
user={user}
|
user={user}
|
||||||
liquidityProvisions={liquidityProvisions}
|
|
||||||
bets={bets}
|
bets={bets}
|
||||||
tips={tips}
|
tips={tips}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { sortBy } from 'lodash'
|
||||||
import { ContractsGrid } from 'web/components/contract/contracts-grid'
|
import { ContractsGrid } from 'web/components/contract/contracts-grid'
|
||||||
import { useContracts } from 'web/hooks/use-contracts'
|
import { useContracts } from 'web/hooks/use-contracts'
|
||||||
import {
|
import {
|
||||||
|
QuerySortOptions,
|
||||||
Sort,
|
Sort,
|
||||||
useQueryAndSortParams,
|
useQueryAndSortParams,
|
||||||
} from 'web/hooks/use-sort-and-query-params'
|
} from 'web/hooks/use-sort-and-query-params'
|
||||||
|
@ -11,10 +12,7 @@ import {
|
||||||
const MAX_CONTRACTS_RENDERED = 100
|
const MAX_CONTRACTS_RENDERED = 100
|
||||||
|
|
||||||
export default function ContractSearchFirestore(props: {
|
export default function ContractSearchFirestore(props: {
|
||||||
querySortOptions?: {
|
querySortOptions?: QuerySortOptions
|
||||||
defaultSort: Sort
|
|
||||||
shouldLoadFromStorage?: boolean
|
|
||||||
}
|
|
||||||
additionalFilter?: {
|
additionalFilter?: {
|
||||||
creatorId?: string
|
creatorId?: string
|
||||||
tag?: string
|
tag?: string
|
||||||
|
|
|
@ -120,7 +120,8 @@ export function NewContract(props: {
|
||||||
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
||||||
const [initialValueString, setInitialValueString] = useState(initValue)
|
const [initialValueString, setInitialValueString] = useState(initValue)
|
||||||
|
|
||||||
const [answers, setAnswers] = useState<string[]>([]) // for multiple choice
|
// for multiple choice, init to 3 empty answers
|
||||||
|
const [answers, setAnswers] = useState(['', '', ''])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (groupId)
|
if (groupId)
|
||||||
|
@ -285,7 +286,7 @@ export function NewContract(props: {
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
{outcomeType === 'MULTIPLE_CHOICE' && (
|
{outcomeType === 'MULTIPLE_CHOICE' && (
|
||||||
<MultipleChoiceAnswers setAnswers={setAnswers} />
|
<MultipleChoiceAnswers answers={answers} setAnswers={setAnswers} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{outcomeType === 'PSEUDO_NUMERIC' && (
|
{outcomeType === 'PSEUDO_NUMERIC' && (
|
||||||
|
@ -299,7 +300,7 @@ export function NewContract(props: {
|
||||||
<Row className="gap-2">
|
<Row className="gap-2">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="input input-bordered"
|
className="input input-bordered w-32"
|
||||||
placeholder="MIN"
|
placeholder="MIN"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => setMinString(e.target.value)}
|
onChange={(e) => setMinString(e.target.value)}
|
||||||
|
@ -310,7 +311,7 @@ export function NewContract(props: {
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="input input-bordered"
|
className="input input-bordered w-32"
|
||||||
placeholder="MAX"
|
placeholder="MAX"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => setMaxString(e.target.value)}
|
onChange={(e) => setMaxString(e.target.value)}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user