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