Merge branch 'main' into user-profile
This commit is contained in:
commit
7dcdfdaec6
|
@ -15,4 +15,6 @@ export type Fold = {
|
||||||
// Default: creatorIds: undefined, excludedCreatorIds: []
|
// Default: creatorIds: undefined, excludedCreatorIds: []
|
||||||
creatorIds?: string[]
|
creatorIds?: string[]
|
||||||
excludedCreatorIds?: string[]
|
excludedCreatorIds?: string[]
|
||||||
|
|
||||||
|
followCount: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,15 @@ export function getNewContract(
|
||||||
description: string,
|
description: string,
|
||||||
initialProb: number,
|
initialProb: number,
|
||||||
ante: number,
|
ante: number,
|
||||||
closeTime: number
|
closeTime: number,
|
||||||
|
extraTags: string[]
|
||||||
) {
|
) {
|
||||||
const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } =
|
const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } =
|
||||||
calcStartPool(initialProb, ante)
|
calcStartPool(initialProb, ante)
|
||||||
|
|
||||||
const tags = parseTags(`${question} ${description}`)
|
const tags = parseTags(
|
||||||
|
`${extraTags.map((tag) => `#${tag}`).join(' ')} ${question} ${description}`
|
||||||
|
)
|
||||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||||
|
|
||||||
const contract: Contract = {
|
const contract: Contract = {
|
||||||
|
|
|
@ -21,7 +21,7 @@ service cloud.firestore {
|
||||||
|
|
||||||
match /contracts/{contractId} {
|
match /contracts/{contractId} {
|
||||||
allow read;
|
allow read;
|
||||||
allow update: if resource.data.creatorId == request.auth.uid && request.resource.data.diff(resource.data).affectedKeys()
|
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['description', 'tags', 'lowercaseTags']);
|
.hasOnly(['description', 'tags', 'lowercaseTags']);
|
||||||
allow delete: if resource.data.creatorId == request.auth.uid;
|
allow delete: if resource.data.creatorId == request.auth.uid;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,12 @@ service cloud.firestore {
|
||||||
|
|
||||||
match /folds/{foldId} {
|
match /folds/{foldId} {
|
||||||
allow read;
|
allow read;
|
||||||
allow update: if request.auth.uid == resource.data.curatorId;
|
allow update, delete: if request.auth.uid == resource.data.curatorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
match /folds/{foldId}/followers/{userId} {
|
||||||
|
allow read;
|
||||||
|
allow write: if request.auth.uid == userId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,6 +18,7 @@ export const createContract = functions
|
||||||
initialProb: number
|
initialProb: number
|
||||||
ante: number
|
ante: number
|
||||||
closeTime: number
|
closeTime: number
|
||||||
|
tags?: string[]
|
||||||
},
|
},
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
|
@ -27,7 +28,7 @@ export const createContract = functions
|
||||||
const creator = await getUser(userId)
|
const creator = await getUser(userId)
|
||||||
if (!creator) return { status: 'error', message: 'User not found' }
|
if (!creator) return { status: 'error', message: 'User not found' }
|
||||||
|
|
||||||
const { question, description, initialProb, ante, closeTime } = data
|
const { question, description, initialProb, ante, closeTime, tags } = data
|
||||||
|
|
||||||
if (!question || !initialProb)
|
if (!question || !initialProb)
|
||||||
return { status: 'error', message: 'Missing contract attributes' }
|
return { status: 'error', message: 'Missing contract attributes' }
|
||||||
|
@ -65,7 +66,8 @@ export const createContract = functions
|
||||||
description,
|
description,
|
||||||
initialProb,
|
initialProb,
|
||||||
ante,
|
ante,
|
||||||
closeTime
|
closeTime,
|
||||||
|
tags ?? []
|
||||||
)
|
)
|
||||||
|
|
||||||
if (ante) await chargeUser(creator.id, ante)
|
if (ante) await chargeUser(creator.id, ante)
|
||||||
|
|
|
@ -23,11 +23,17 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
const creator = await getUser(userId)
|
const creator = await getUser(userId)
|
||||||
if (!creator) return { status: 'error', message: 'User not found' }
|
if (!creator) return { status: 'error', message: 'User not found' }
|
||||||
|
|
||||||
const { name, about, tags } = data
|
let { name, about, tags } = data
|
||||||
|
|
||||||
if (!name || typeof name !== 'string')
|
if (!name || typeof name !== 'string')
|
||||||
return { status: 'error', message: 'Name must be a non-empty string' }
|
return { status: 'error', message: 'Name must be a non-empty string' }
|
||||||
|
|
||||||
|
name = name.trim().slice(0, 140)
|
||||||
|
|
||||||
|
if (typeof about !== 'string')
|
||||||
|
return { status: 'error', message: 'About must be a string' }
|
||||||
|
about = about.trim().slice(0, 140)
|
||||||
|
|
||||||
if (!_.isArray(tags))
|
if (!_.isArray(tags))
|
||||||
return { status: 'error', message: 'Tags must be an array of strings' }
|
return { status: 'error', message: 'Tags must be an array of strings' }
|
||||||
|
|
||||||
|
@ -57,10 +63,13 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
contractIds: [],
|
contractIds: [],
|
||||||
excludedContractIds: [],
|
excludedContractIds: [],
|
||||||
excludedCreatorIds: [],
|
excludedCreatorIds: [],
|
||||||
|
followCount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
await foldRef.create(fold)
|
await foldRef.create(fold)
|
||||||
|
|
||||||
|
await foldRef.collection('followers').doc(userId).set({ userId })
|
||||||
|
|
||||||
return { status: 'success', fold }
|
return { status: 'success', fold }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,6 +11,8 @@ export * from './sell-bet'
|
||||||
export * from './create-contract'
|
export * from './create-contract'
|
||||||
export * from './create-user'
|
export * from './create-user'
|
||||||
export * from './create-fold'
|
export * from './create-fold'
|
||||||
|
export * from './on-fold-follow'
|
||||||
|
export * from './on-fold-delete'
|
||||||
export * from './unsubscribe'
|
export * from './unsubscribe'
|
||||||
export * from './update-contract-metrics'
|
export * from './update-contract-metrics'
|
||||||
export * from './update-user-metrics'
|
export * from './update-user-metrics'
|
||||||
|
|
10
functions/src/on-fold-delete.ts
Normal file
10
functions/src/on-fold-delete.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
|
||||||
|
export const onFoldDelete = functions.firestore
|
||||||
|
.document('folds/{foldId}')
|
||||||
|
.onDelete(async (change, context) => {
|
||||||
|
const snapshot = await change.ref.collection('followers').get()
|
||||||
|
|
||||||
|
// Delete followers sub-collection.
|
||||||
|
await Promise.all(snapshot.docs.map((doc) => doc.ref.delete()))
|
||||||
|
})
|
17
functions/src/on-fold-follow.ts
Normal file
17
functions/src/on-fold-follow.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
export const onFoldFollow = functions.firestore
|
||||||
|
.document('folds/{foldId}/followers/{userId}')
|
||||||
|
.onWrite(async (change, context) => {
|
||||||
|
const { foldId } = context.params
|
||||||
|
|
||||||
|
const snapshot = await firestore
|
||||||
|
.collection(`folds/${foldId}/followers`)
|
||||||
|
.get()
|
||||||
|
const followCount = snapshot.size
|
||||||
|
|
||||||
|
await firestore.doc(`folds/${foldId}`).update({ followCount })
|
||||||
|
})
|
|
@ -14,6 +14,8 @@ export function AmountInput(props: {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
inputClassName?: string
|
inputClassName?: string
|
||||||
|
// Needed to focus the amount input
|
||||||
|
inputRef?: React.MutableRefObject<any>
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
amount,
|
amount,
|
||||||
|
@ -24,6 +26,7 @@ export function AmountInput(props: {
|
||||||
className,
|
className,
|
||||||
inputClassName,
|
inputClassName,
|
||||||
minimumAmount,
|
minimumAmount,
|
||||||
|
inputRef,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -56,6 +59,7 @@ export function AmountInput(props: {
|
||||||
error && 'input-error',
|
error && 'input-error',
|
||||||
inputClassName
|
inputClassName
|
||||||
)}
|
)}
|
||||||
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
maxLength={9}
|
maxLength={9}
|
||||||
|
|
34
web/components/avatar.tsx
Normal file
34
web/components/avatar.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import Router from 'next/router'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
export function Avatar(props: {
|
||||||
|
username?: string
|
||||||
|
avatarUrl?: string
|
||||||
|
noLink?: boolean
|
||||||
|
}) {
|
||||||
|
const { username, avatarUrl, noLink } = props
|
||||||
|
|
||||||
|
const onClick =
|
||||||
|
noLink && username
|
||||||
|
? undefined
|
||||||
|
: (e: any) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
Router.push(`/${username}`)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="rounded-full bg-gray-400 w-10 h-10">
|
||||||
|
<img
|
||||||
|
className={clsx(
|
||||||
|
'rounded-full bg-gray-400 flex items-center justify-center',
|
||||||
|
!noLink && 'cursor-pointer',
|
||||||
|
!avatarUrl && 'hidden'
|
||||||
|
)}
|
||||||
|
src={avatarUrl}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
onClick={onClick}
|
||||||
|
alt={username}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
|
@ -26,18 +26,34 @@ import { AmountInput } from './amount-input'
|
||||||
import { InfoTooltip } from './info-tooltip'
|
import { InfoTooltip } from './info-tooltip'
|
||||||
import { OutcomeLabel } from './outcome-label'
|
import { OutcomeLabel } from './outcome-label'
|
||||||
|
|
||||||
export function BetPanel(props: { contract: Contract; className?: string }) {
|
// Focus helper from https://stackoverflow.com/a/54159564/1222351
|
||||||
|
function useFocus(): [React.RefObject<HTMLElement>, () => void] {
|
||||||
|
const htmlElRef = useRef<HTMLElement>(null)
|
||||||
|
const setFocus = () => {
|
||||||
|
htmlElRef.current && htmlElRef.current.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
return [htmlElRef, setFocus]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BetPanel(props: {
|
||||||
|
contract: Contract
|
||||||
|
className?: string
|
||||||
|
title?: string // Set if BetPanel is on a feed modal
|
||||||
|
selected?: 'YES' | 'NO'
|
||||||
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// warm up cloud function
|
// warm up cloud function
|
||||||
placeBet({}).catch()
|
placeBet({}).catch()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { contract, className } = props
|
const { contract, className, title, selected } = props
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const [betChoice, setBetChoice] = useState<'YES' | 'NO'>('YES')
|
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
|
||||||
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
||||||
|
const [inputRef, focusAmountInput] = useFocus()
|
||||||
|
|
||||||
const [error, setError] = useState<string | undefined>()
|
const [error, setError] = useState<string | undefined>()
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
@ -46,11 +62,15 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
||||||
function onBetChoice(choice: 'YES' | 'NO') {
|
function onBetChoice(choice: 'YES' | 'NO') {
|
||||||
setBetChoice(choice)
|
setBetChoice(choice)
|
||||||
setWasSubmitted(false)
|
setWasSubmitted(false)
|
||||||
|
focusAmountInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBetChange(newAmount: number | undefined) {
|
function onBetChange(newAmount: number | undefined) {
|
||||||
setWasSubmitted(false)
|
setWasSubmitted(false)
|
||||||
setBetAmount(newAmount)
|
setBetAmount(newAmount)
|
||||||
|
if (!betChoice) {
|
||||||
|
setBetChoice('YES')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitBet() {
|
async function submitBet() {
|
||||||
|
@ -88,14 +108,14 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
||||||
|
|
||||||
const resultProb = getProbabilityAfterBet(
|
const resultProb = getProbabilityAfterBet(
|
||||||
contract.totalShares,
|
contract.totalShares,
|
||||||
betChoice,
|
betChoice || 'YES',
|
||||||
betAmount ?? 0
|
betAmount ?? 0
|
||||||
)
|
)
|
||||||
|
|
||||||
const shares = calculateShares(
|
const shares = calculateShares(
|
||||||
contract.totalShares,
|
contract.totalShares,
|
||||||
betAmount ?? 0,
|
betAmount ?? 0,
|
||||||
betChoice
|
betChoice || 'YES'
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentPayout = betAmount
|
const currentPayout = betAmount
|
||||||
|
@ -108,19 +128,21 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
||||||
|
|
||||||
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
||||||
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
||||||
|
const panelTitle = title ?? 'Place a trade'
|
||||||
|
if (title) {
|
||||||
|
focusAmountInput()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col className={clsx('bg-white px-8 py-6 rounded-md', className)}>
|
||||||
className={clsx('bg-gray-100 shadow-md px-8 py-6 rounded-md', className)}
|
|
||||||
>
|
|
||||||
<Title
|
<Title
|
||||||
className="mt-0 whitespace-nowrap text-neutral"
|
className={clsx('!mt-0', title ? '!text-xl' : '')}
|
||||||
text={`Buy ${betChoice}`}
|
text={panelTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-2 mb-1 text-sm text-gray-500">Outcome</div>
|
{/* <div className="mt-2 mb-1 text-sm text-gray-500">Outcome</div> */}
|
||||||
<YesNoSelector
|
<YesNoSelector
|
||||||
className="my-2"
|
className="mb-4"
|
||||||
selected={betChoice}
|
selected={betChoice}
|
||||||
onSelect={(choice) => onBetChoice(choice)}
|
onSelect={(choice) => onBetChoice(choice)}
|
||||||
/>
|
/>
|
||||||
|
@ -133,6 +155,7 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
||||||
error={error}
|
error={error}
|
||||||
setError={setError}
|
setError={setError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
inputRef={inputRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
@ -144,22 +167,27 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
||||||
<div>{formatPercent(resultProb)}</div>
|
<div>{formatPercent(resultProb)}</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
{betChoice && (
|
||||||
Payout if <OutcomeLabel outcome={betChoice} />
|
<>
|
||||||
<InfoTooltip
|
<Spacer h={4} />
|
||||||
text={`Current payout for ${formatWithCommas(
|
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
||||||
shares
|
Payout if <OutcomeLabel outcome={betChoice} />
|
||||||
)} / ${formatWithCommas(
|
<InfoTooltip
|
||||||
shares +
|
text={`Current payout for ${formatWithCommas(
|
||||||
contract.totalShares[betChoice] -
|
shares
|
||||||
contract.phantomShares[betChoice]
|
)} / ${formatWithCommas(
|
||||||
)} ${betChoice} shares`}
|
shares +
|
||||||
/>
|
contract.totalShares[betChoice] -
|
||||||
</Row>
|
contract.phantomShares[betChoice]
|
||||||
<div>
|
)} ${betChoice} shares`}
|
||||||
{formatMoney(currentPayout)}
|
/>
|
||||||
<span>(+{currentReturnPercent})</span>
|
</Row>
|
||||||
</div>
|
<div>
|
||||||
|
{formatMoney(currentPayout)}
|
||||||
|
<span>(+{currentReturnPercent})</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
|
|
100
web/components/bet-row.tsx
Normal file
100
web/components/bet-row.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { Fragment, useState } from 'react'
|
||||||
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
|
import { Contract } from '../lib/firebase/contracts'
|
||||||
|
import { BetPanel } from './bet-panel'
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
import { YesNoSelector } from './yes-no-selector'
|
||||||
|
|
||||||
|
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
||||||
|
export default function BetRow(props: {
|
||||||
|
contract: Contract
|
||||||
|
className?: string
|
||||||
|
labelClassName?: string
|
||||||
|
}) {
|
||||||
|
const { className, labelClassName } = props
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={className}>
|
||||||
|
<Row className="items-center gap-2 justify-end">
|
||||||
|
<div className={clsx('text-gray-400 mr-2', labelClassName)}>
|
||||||
|
Place a trade
|
||||||
|
</div>
|
||||||
|
<YesNoSelector
|
||||||
|
btnClassName="btn-sm w-20"
|
||||||
|
onSelect={(choice) => {
|
||||||
|
setOpen(true)
|
||||||
|
setBetChoice(choice)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Modal open={open} setOpen={setOpen}>
|
||||||
|
<BetPanel
|
||||||
|
contract={props.contract}
|
||||||
|
title={props.contract.question}
|
||||||
|
selected={betChoice}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://tailwindui.com/components/application-ui/overlays/modals
|
||||||
|
export function Modal(props: {
|
||||||
|
children: React.ReactNode
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { children, open, setOpen } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={open} as={Fragment}>
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
className="fixed z-40 inset-0 overflow-y-auto"
|
||||||
|
onClose={setOpen}
|
||||||
|
>
|
||||||
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
{/* This element is to trick the browser into centering the modal contents. */}
|
||||||
|
<span
|
||||||
|
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
​
|
||||||
|
</span>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<div className="inline-block align-bottom text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle sm:max-w-md sm:w-full sm:p-6">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
)
|
||||||
|
}
|
|
@ -94,7 +94,7 @@ export function BetsList(props: { user: User }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="mt-6 gap-6">
|
<Col className="mt-6 gap-6">
|
||||||
<Row className="gap-8">
|
<Row className="mx-4 md:mx-0 gap-8">
|
||||||
<Col>
|
<Col>
|
||||||
<div className="text-sm text-gray-500">Currently invested</div>
|
<div className="text-sm text-gray-500">Currently invested</div>
|
||||||
<div>{formatMoney(currentBets)}</div>
|
<div>{formatMoney(currentBets)}</div>
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { parseTags } from '../../common/util/parse'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { TrendingUpIcon } from '@heroicons/react/solid'
|
import { TrendingUpIcon } from '@heroicons/react/solid'
|
||||||
import { DateTimeTooltip } from './datetime-tooltip'
|
import { DateTimeTooltip } from './datetime-tooltip'
|
||||||
import { CompactTagsList } from './tags-list'
|
|
||||||
import { ClockIcon } from '@heroicons/react/outline'
|
import { ClockIcon } from '@heroicons/react/outline'
|
||||||
import { fromNow } from '../lib/util/time'
|
import { fromNow } from '../lib/util/time'
|
||||||
|
|
||||||
|
@ -194,13 +193,6 @@ export function ContractDetails(props: { contract: Contract }) {
|
||||||
<div className="">•</div>
|
<div className="">•</div>
|
||||||
<div className="whitespace-nowrap">{formatMoney(truePool)} pool</div>
|
<div className="whitespace-nowrap">{formatMoney(truePool)} pool</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{tags.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className="hidden sm:block">•</div>
|
|
||||||
<CompactTagsList tags={tags} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,16 @@ import {
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@heroicons/react/solid'
|
} from '@heroicons/react/solid'
|
||||||
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
import { OutcomeLabel } from './outcome-label'
|
import { OutcomeLabel } from './outcome-label'
|
||||||
import {
|
import {
|
||||||
contractMetrics,
|
contractMetrics,
|
||||||
Contract,
|
Contract,
|
||||||
contractPath,
|
contractPath,
|
||||||
updateContract,
|
updateContract,
|
||||||
|
tradingAllowed,
|
||||||
} from '../lib/firebase/contracts'
|
} from '../lib/firebase/contracts'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { Linkify } from './linkify'
|
import { Linkify } from './linkify'
|
||||||
|
@ -38,22 +40,9 @@ import { JoinSpans } from './join-spans'
|
||||||
import Textarea from 'react-expanding-textarea'
|
import Textarea from 'react-expanding-textarea'
|
||||||
import { outcome } from '../../common/contract'
|
import { outcome } from '../../common/contract'
|
||||||
import { fromNow } from '../lib/util/time'
|
import { fromNow } from '../lib/util/time'
|
||||||
|
import BetRow from './bet-row'
|
||||||
import { parseTags } from '../../common/util/parse'
|
import { parseTags } from '../../common/util/parse'
|
||||||
|
import { Avatar } from './avatar'
|
||||||
export function AvatarWithIcon(props: { username: string; avatarUrl: string }) {
|
|
||||||
const { username, avatarUrl } = props
|
|
||||||
return (
|
|
||||||
<SiteLink className="relative" href={`/${username}`}>
|
|
||||||
<img
|
|
||||||
className="rounded-full bg-gray-400 flex items-center justify-center"
|
|
||||||
src={avatarUrl}
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</SiteLink>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FeedComment(props: {
|
function FeedComment(props: {
|
||||||
activityItem: any
|
activityItem: any
|
||||||
|
@ -68,7 +57,7 @@ function FeedComment(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AvatarWithIcon username={person.username} avatarUrl={person.avatarUrl} />
|
<Avatar username={person.username} avatarUrl={person.avatarUrl} />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div>
|
<div>
|
||||||
<p className="mt-0.5 text-sm text-gray-500">
|
<p className="mt-0.5 text-sm text-gray-500">
|
||||||
|
@ -283,7 +272,7 @@ function FeedQuestion(props: { contract: Contract }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{contract.creatorAvatarUrl ? (
|
{contract.creatorAvatarUrl ? (
|
||||||
<AvatarWithIcon
|
<Avatar
|
||||||
username={contract.creatorUsername}
|
username={contract.creatorUsername}
|
||||||
avatarUrl={contract.creatorAvatarUrl}
|
avatarUrl={contract.creatorAvatarUrl}
|
||||||
/>
|
/>
|
||||||
|
@ -337,7 +326,7 @@ function FeedDescription(props: { contract: Contract }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{contract.creatorAvatarUrl ? (
|
{contract.creatorAvatarUrl ? (
|
||||||
<AvatarWithIcon
|
<Avatar
|
||||||
username={contract.creatorUsername}
|
username={contract.creatorUsername}
|
||||||
avatarUrl={contract.creatorAvatarUrl}
|
avatarUrl={contract.creatorAvatarUrl}
|
||||||
/>
|
/>
|
||||||
|
@ -619,8 +608,9 @@ export function ContractFeed(props: {
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
// Feed types: 'activity' = Activity feed, 'market' = Comments feed on a market
|
// Feed types: 'activity' = Activity feed, 'market' = Comments feed on a market
|
||||||
feedType: 'activity' | 'market'
|
feedType: 'activity' | 'market'
|
||||||
|
betRowClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, feedType } = props
|
const { contract, feedType, betRowClassName } = props
|
||||||
const { id } = contract
|
const { id } = contract
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -654,8 +644,8 @@ export function ContractFeed(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flow-root">
|
<div className="flow-root pr-2 md:pr-0">
|
||||||
<ul role="list" className="-mb-8">
|
<ul role="list" className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
|
||||||
{items.map((activityItem, activityItemIdx) => (
|
{items.map((activityItem, activityItemIdx) => (
|
||||||
<li key={activityItem.id}>
|
<li key={activityItem.id}>
|
||||||
<div className="relative pb-8">
|
<div className="relative pb-8">
|
||||||
|
@ -694,6 +684,12 @@ export function ContractFeed(props: {
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
{tradingAllowed(contract) && (
|
||||||
|
<BetRow
|
||||||
|
contract={contract}
|
||||||
|
className={clsx('-mt-4', betRowClassName)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ import { ContractFeed } from './contract-feed'
|
||||||
import { TweetButton } from './tweet-button'
|
import { TweetButton } from './tweet-button'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { Comment } from '../../common/comment'
|
import { Comment } from '../../common/comment'
|
||||||
|
import { TagsInput } from './tags-input'
|
||||||
|
import BetRow from './bet-row'
|
||||||
|
|
||||||
export const ContractOverview = (props: {
|
export const ContractOverview = (props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -35,9 +37,7 @@ export const ContractOverview = (props: {
|
||||||
? contract.question
|
? contract.question
|
||||||
: `${creatorName}: ${contract.question}`
|
: `${creatorName}: ${contract.question}`
|
||||||
const tweetDescription = resolution
|
const tweetDescription = resolution
|
||||||
? isCreator
|
? `Resolved ${resolution}!`
|
||||||
? `Resolved ${resolution}!`
|
|
||||||
: `Resolved ${resolution} by ${creatorName}:`
|
|
||||||
: `Currently ${probPercent} chance, place your bets here:`
|
: `Currently ${probPercent} chance, place your bets here:`
|
||||||
const url = `https://manifold.markets${contractPath(contract)}`
|
const url = `https://manifold.markets${contractPath(contract)}`
|
||||||
const tweetText = `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
|
const tweetText = `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
|
||||||
|
@ -50,15 +50,22 @@ export const ContractOverview = (props: {
|
||||||
<Linkify text={contract.question} />
|
<Linkify text={contract.question} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ResolutionOrChance
|
<Row className="justify-between items-center gap-4">
|
||||||
className="md:hidden"
|
<ResolutionOrChance
|
||||||
resolution={resolution}
|
className="md:hidden"
|
||||||
probPercent={probPercent}
|
resolution={resolution}
|
||||||
large
|
probPercent={probPercent}
|
||||||
/>
|
large
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BetRow
|
||||||
|
contract={contract}
|
||||||
|
className="md:hidden"
|
||||||
|
labelClassName="hidden"
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<ContractDetails contract={contract} />
|
<ContractDetails contract={contract} />
|
||||||
<TweetButton className="self-end md:hidden" tweetText={tweetText} />
|
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col className="hidden md:flex justify-between items-end">
|
<Col className="hidden md:flex justify-between items-end">
|
||||||
|
@ -68,7 +75,6 @@ export const ContractOverview = (props: {
|
||||||
probPercent={probPercent}
|
probPercent={probPercent}
|
||||||
large
|
large
|
||||||
/>
|
/>
|
||||||
<TweetButton className="mt-6" tweetText={tweetText} />
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
@ -76,6 +82,11 @@ export const ContractOverview = (props: {
|
||||||
|
|
||||||
<ContractProbGraph contract={contract} />
|
<ContractProbGraph contract={contract} />
|
||||||
|
|
||||||
|
<Row className="justify-between mt-6 ml-4 gap-4">
|
||||||
|
<TagsInput contract={contract} />
|
||||||
|
<TweetButton tweetText={tweetText} />
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Spacer h={12} />
|
<Spacer h={12} />
|
||||||
|
|
||||||
{/* Show a delete button for contracts without any trading */}
|
{/* Show a delete button for contracts without any trading */}
|
||||||
|
@ -100,6 +111,7 @@ export const ContractOverview = (props: {
|
||||||
bets={bets}
|
bets={bets}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
feedType="market"
|
feedType="market"
|
||||||
|
betRowClassName="md:hidden !mt-0"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
|
|
@ -27,7 +27,7 @@ export function ContractProbGraph(props: { contract: Contract }) {
|
||||||
if (!resolutionTime) {
|
if (!resolutionTime) {
|
||||||
// Add a fake datapoint in future so the line continues horizontally
|
// Add a fake datapoint in future so the line continues horizontally
|
||||||
// to the right.
|
// to the right.
|
||||||
times.push(latestTime.add(1, 'day').toDate())
|
times.push(latestTime.add(1, 'month').toDate())
|
||||||
probs.push(probs[probs.length - 1])
|
probs.push(probs[probs.length - 1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,14 +33,8 @@ export function ContractsGrid(props: {
|
||||||
|
|
||||||
if (contracts.length === 0) {
|
if (contracts.length === 0) {
|
||||||
return (
|
return (
|
||||||
<p className="mx-4">
|
<p className="text-gray-500 mx-2">
|
||||||
No markets found. Would you like to{' '}
|
No markets found. Why not create one?
|
||||||
<Link href="/create">
|
|
||||||
<a className="text-green-500 hover:underline hover:decoration-2">
|
|
||||||
create one
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
?
|
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,15 +57,16 @@ export function CreateFoldButton() {
|
||||||
}}
|
}}
|
||||||
submitBtn={{
|
submitBtn={{
|
||||||
label: 'Create',
|
label: 'Create',
|
||||||
className: clsx(name && about ? 'btn-primary' : 'btn-disabled'),
|
className: clsx(name ? 'btn-primary' : 'btn-disabled'),
|
||||||
}}
|
}}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
<Title className="!mt-0" text="Create a fold" />
|
<Title className="!mt-0" text="Create a fold" />
|
||||||
|
|
||||||
<Col className="text-gray-500 gap-1">
|
<Col className="text-gray-500 gap-1">
|
||||||
<div>A fold is a sub-community of markets organized on a topic.</div>
|
<div>
|
||||||
<div>Markets are included if they match one or more tags.</div>
|
Markets are included in a fold if they match one or more tags.
|
||||||
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
@ -81,6 +82,7 @@ export function CreateFoldButton() {
|
||||||
className="input input-bordered resize-none"
|
className="input input-bordered resize-none"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
value={name}
|
value={name}
|
||||||
|
maxLength={140}
|
||||||
onChange={(e) => updateName(e.target.value || '')}
|
onChange={(e) => updateName(e.target.value || '')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,7 +95,7 @@ export function CreateFoldButton() {
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
placeholder="Short description (140 characters max)"
|
placeholder="Short description (140 characters max, optional)"
|
||||||
className="input input-bordered resize-none"
|
className="input input-bordered resize-none"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
value={about}
|
value={about}
|
||||||
|
|
|
@ -5,13 +5,16 @@ import { PencilIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
import { Fold } from '../../common/fold'
|
import { Fold } from '../../common/fold'
|
||||||
import { parseWordsAsTags } from '../../common/util/parse'
|
import { parseWordsAsTags } from '../../common/util/parse'
|
||||||
import { updateFold } from '../lib/firebase/folds'
|
import { deleteFold, updateFold } from '../lib/firebase/folds'
|
||||||
import { toCamelCase } from '../lib/util/format'
|
import { toCamelCase } from '../lib/util/format'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { TagsList } from './tags-list'
|
import { TagsList } from './tags-list'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
export function EditFoldButton(props: { fold: Fold; className?: string }) {
|
||||||
|
const { fold, className } = props
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
export function EditFoldButton(props: { fold: Fold }) {
|
|
||||||
const { fold } = props
|
|
||||||
const [name, setName] = useState(fold.name)
|
const [name, setName] = useState(fold.name)
|
||||||
const [about, setAbout] = useState(fold.about ?? '')
|
const [about, setAbout] = useState(fold.about ?? '')
|
||||||
|
|
||||||
|
@ -41,7 +44,7 @@ export function EditFoldButton(props: { fold: Fold }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={clsx('p-1', className)}>
|
||||||
<label
|
<label
|
||||||
htmlFor="edit"
|
htmlFor="edit"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -106,6 +109,20 @@ export function EditFoldButton(props: { fold: Fold }) {
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
<div className="modal-action">
|
<div className="modal-action">
|
||||||
|
<label
|
||||||
|
htmlFor="edit"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm('Are you sure you want to delete this fold?')) {
|
||||||
|
deleteFold(fold)
|
||||||
|
router.replace('/folds')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={clsx(
|
||||||
|
'btn btn-sm btn-outline hover:bg-red-500 hover:border-red-500 mr-auto self-center'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</label>
|
||||||
<label htmlFor="edit" className={clsx('btn')}>
|
<label htmlFor="edit" className={clsx('btn')}>
|
||||||
Cancel
|
Cancel
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -1,104 +1,117 @@
|
||||||
import { AvatarWithIcon } from './contract-feed'
|
import { Avatar } from './avatar'
|
||||||
import { Title } from './title'
|
import { useRef, useState } from 'react'
|
||||||
import Textarea from 'react-expanding-textarea'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { NewContract } from '../pages/create'
|
import { NewContract } from '../pages/create'
|
||||||
import { firebaseLogin, User } from '../lib/firebase/users'
|
import { firebaseLogin, User } from '../lib/firebase/users'
|
||||||
import { ContractsGrid } from './contracts-list'
|
import { ContractsGrid } from './contracts-list'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { TagsList } from './tags-list'
|
import { Col } from './layout/col'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
export function FeedPromo(props: { hotContracts: Contract[] }) {
|
export function FeedPromo(props: { hotContracts: Contract[] }) {
|
||||||
const { hotContracts } = props
|
const { hotContracts } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full bg-indigo-50 p-6 sm:border-2 sm:border-indigo-100 sm:rounded-lg">
|
<Col className="w-full bg-white p-6 sm:rounded-lg">
|
||||||
<Title
|
<h1 className="mt-4 text-4xl sm:mt-5 sm:text-6xl lg:mt-6 xl:text-6xl">
|
||||||
text="Bet on the future"
|
<div className="mb-2">Create your own</div>
|
||||||
className="!mt-2 text-gray-800 !text-4xl"
|
<div className="font-bold bg-clip-text text-transparent bg-gradient-to-r from-teal-400 to-green-400">
|
||||||
/>
|
prediction markets
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
<Spacer h={6} />
|
||||||
<div className="text-gray-500 mb-4">
|
<div className="text-gray-500 mb-4">
|
||||||
On Manifold Markets, you can find prediction markets run by your
|
Find prediction markets run by your favorite creators, or make your
|
||||||
favorite creators.
|
own.
|
||||||
<br />
|
<br />
|
||||||
<button
|
Sign up to get M$ 1,000 for free and start trading!
|
||||||
className="text-green-500 hover:underline hover:decoration-gray-300 hover:decoration-2"
|
|
||||||
onClick={firebaseLogin}
|
|
||||||
>
|
|
||||||
Sign up to get M$ 1000 for free
|
|
||||||
</button>{' '}
|
|
||||||
and start trading!
|
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
|
<Spacer h={6} />
|
||||||
|
<button
|
||||||
|
className="btn btn-lg self-center border-none bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600"
|
||||||
|
onClick={firebaseLogin}
|
||||||
|
>
|
||||||
|
Sign up now
|
||||||
|
</button>{' '}
|
||||||
|
</Col>
|
||||||
|
|
||||||
<TagsList
|
<Spacer h={6} />
|
||||||
className="mt-2"
|
{/*
|
||||||
tags={['#politics', '#crypto', '#covid', '#sports', '#meta']}
|
<TagsList
|
||||||
/>
|
className="mt-2"
|
||||||
|
tags={['#politics', '#crypto', '#covid', '#sports', '#meta']}
|
||||||
|
/>
|
||||||
|
<Spacer h={6} /> */}
|
||||||
|
|
||||||
<Spacer h={4} />
|
<ContractsGrid
|
||||||
|
contracts={hotContracts?.slice(0, 10) || []}
|
||||||
<ContractsGrid
|
showHotVolume
|
||||||
contracts={hotContracts?.slice(0, 6) || []}
|
/>
|
||||||
showHotVolume
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FeedCreate(props: { user?: User }) {
|
export default function FeedCreate(props: {
|
||||||
const { user } = props
|
user?: User
|
||||||
|
tag?: string
|
||||||
|
placeholder?: string
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { user, tag, className } = props
|
||||||
const [question, setQuestion] = useState('')
|
const [question, setQuestion] = useState('')
|
||||||
|
|
||||||
const placeholders = [
|
const placeholders = [
|
||||||
'Will I make a new friend this week?',
|
|
||||||
'Will we discover that the world is a simulation?',
|
|
||||||
'Will anyone I know get engaged this year?',
|
'Will anyone I know get engaged this year?',
|
||||||
'Will humans set foot on Mars by the end of 2030?',
|
'Will humans set foot on Mars by the end of 2030?',
|
||||||
'If I switch jobs, will I have more free time in 6 months than I do now?',
|
'Will any cryptocurrency eclipse Bitcoin by market cap this year?',
|
||||||
'Will any cryptocurrency eclipse Bitcoin by market cap?',
|
'Will the Democrats win the 2024 presidential election?',
|
||||||
]
|
]
|
||||||
// Rotate through a new placeholder each day
|
// Rotate through a new placeholder each day
|
||||||
// Easter egg idea: click your own name to shuffle the placeholder
|
// Easter egg idea: click your own name to shuffle the placeholder
|
||||||
const daysSinceEpoch = Math.floor(Date.now() / 1000 / 60 / 60 / 24)
|
// const daysSinceEpoch = Math.floor(Date.now() / 1000 / 60 / 60 / 24)
|
||||||
const placeholder = placeholders[daysSinceEpoch % placeholders.length]
|
|
||||||
|
const [randIndex] = useState(
|
||||||
|
Math.floor(Math.random() * 1e10) % placeholders.length
|
||||||
|
)
|
||||||
|
const placeholder = props.placeholder ?? `e.g. ${placeholders[randIndex]}`
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement | null>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-indigo-50 sm:rounded-md p-4">
|
<div
|
||||||
|
className={clsx('w-full bg-white p-4 border-b-2 mt-2', className)}
|
||||||
|
onClick={() => !question && inputRef.current?.focus()}
|
||||||
|
>
|
||||||
<div className="relative flex items-start space-x-3">
|
<div className="relative flex items-start space-x-3">
|
||||||
<AvatarWithIcon
|
<Avatar username={user?.username} avatarUrl={user?.avatarUrl} noLink />
|
||||||
username={user?.username || ''}
|
|
||||||
avatarUrl={user?.avatarUrl || ''}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
{/* TODO: Show focus, for accessibility */}
|
{/* TODO: Show focus, for accessibility */}
|
||||||
<div>
|
<div>
|
||||||
<p className="my-0.5 text-sm">Ask a question... </p>
|
<p className="my-0.5 text-sm">Ask a question... </p>
|
||||||
</div>
|
</div>
|
||||||
<Textarea
|
<textarea
|
||||||
|
ref={inputRef as any}
|
||||||
className="text-lg sm:text-xl text-indigo-700 w-full border-transparent focus:border-transparent bg-transparent p-0 appearance-none resize-none focus:ring-transparent placeholder:text-gray-400"
|
className="text-lg sm:text-xl text-indigo-700 w-full border-transparent focus:border-transparent bg-transparent p-0 appearance-none resize-none focus:ring-transparent placeholder:text-gray-400"
|
||||||
placeholder={`e.g. ${placeholder}`}
|
placeholder={placeholder}
|
||||||
value={question}
|
value={question}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => setQuestion(e.target.value || '')}
|
onChange={(e) => setQuestion(e.target.value.replace('\n', ''))}
|
||||||
/>
|
/>
|
||||||
<Spacer h={4} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hide component instead of deleting, so edits to NewContract don't get lost */}
|
{/* Hide component instead of deleting, so edits to NewContract don't get lost */}
|
||||||
<div className={question ? '' : 'hidden'}>
|
<div className={question ? '' : 'hidden'}>
|
||||||
<NewContract question={question} />
|
<NewContract question={question} tag={tag} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
|
{/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
|
||||||
{!question && (
|
{!question && (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button className="btn" disabled>
|
<button className="btn btn-sm" disabled>
|
||||||
Create Market
|
Create Market
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
47
web/components/follow-fold-button.tsx
Normal file
47
web/components/follow-fold-button.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { Fold } from '../../common/fold'
|
||||||
|
import { useFollowingFold } from '../hooks/use-fold'
|
||||||
|
import { useUser } from '../hooks/use-user'
|
||||||
|
import { followFold, unfollowFold } from '../lib/firebase/folds'
|
||||||
|
|
||||||
|
export function FollowFoldButton(props: { fold: Fold; className?: string }) {
|
||||||
|
const { fold, className } = props
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
const following = useFollowingFold(fold, user)
|
||||||
|
|
||||||
|
const onFollow = () => {
|
||||||
|
if (user) followFold(fold, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUnfollow = () => {
|
||||||
|
if (user) unfollowFold(fold, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user || following === undefined)
|
||||||
|
return (
|
||||||
|
<button className={clsx('btn btn-sm invisible', className)}>
|
||||||
|
Follow
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (following) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx('btn btn-outline btn-sm', className)}
|
||||||
|
onClick={onUnfollow}
|
||||||
|
>
|
||||||
|
Following
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx('btn btn-secondary bg-indigo-500 btn-sm', className)}
|
||||||
|
onClick={onFollow}
|
||||||
|
>
|
||||||
|
Follow
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
|
@ -17,43 +17,47 @@ export function Leaderboard(props: {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('w-full px-1', className)}>
|
<div className={clsx('w-full px-1', className)}>
|
||||||
<Title text={title} />
|
<Title text={title} />
|
||||||
<div className="overflow-x-auto">
|
{users.length === 0 ? (
|
||||||
<table className="table table-zebra table-compact text-gray-500 w-full">
|
<div className="text-gray-500 ml-2">None yet</div>
|
||||||
<thead>
|
) : (
|
||||||
<tr className="p-2">
|
<div className="overflow-x-auto">
|
||||||
<th>#</th>
|
<table className="table table-zebra table-compact text-gray-500 w-full">
|
||||||
<th>Name</th>
|
<thead>
|
||||||
{columns.map((column) => (
|
<tr className="p-2">
|
||||||
<th key={column.header}>{column.header}</th>
|
<th>#</th>
|
||||||
))}
|
<th>Name</th>
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{users.map((user, index) => (
|
|
||||||
<tr key={user.id}>
|
|
||||||
<td>{index + 1}</td>
|
|
||||||
<td>
|
|
||||||
<SiteLink className="relative" href={`/${user.username}`}>
|
|
||||||
<Row className="items-center gap-4">
|
|
||||||
<img
|
|
||||||
className="rounded-full bg-gray-400 flex-shrink-0 ring-8 ring-gray-50"
|
|
||||||
src={user.avatarUrl || ''}
|
|
||||||
alt=""
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
/>
|
|
||||||
<div className="truncate">{user.name}</div>
|
|
||||||
</Row>
|
|
||||||
</SiteLink>
|
|
||||||
</td>
|
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<td key={column.header}>{column.renderCell(user)}</td>
|
<th key={column.header}>{column.header}</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{users.map((user, index) => (
|
||||||
</div>
|
<tr key={user.id}>
|
||||||
|
<td>{index + 1}</td>
|
||||||
|
<td>
|
||||||
|
<SiteLink className="relative" href={`/${user.username}`}>
|
||||||
|
<Row className="items-center gap-4">
|
||||||
|
<img
|
||||||
|
className="rounded-full bg-gray-400 flex-shrink-0 ring-8 ring-gray-50"
|
||||||
|
src={user.avatarUrl || ''}
|
||||||
|
alt=""
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
<div className="truncate">{user.name}</div>
|
||||||
|
</Row>
|
||||||
|
</SiteLink>
|
||||||
|
</td>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<td key={column.header}>{column.renderCell(user)}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
|
@ -15,7 +14,7 @@ export function ManifoldLogo(props: {
|
||||||
return (
|
return (
|
||||||
<Link href={user ? '/home' : '/'}>
|
<Link href={user ? '/home' : '/'}>
|
||||||
<a className={clsx('flex flex-row gap-4 flex-shrink-0', className)}>
|
<a className={clsx('flex flex-row gap-4 flex-shrink-0', className)}>
|
||||||
<Image
|
<img
|
||||||
className="hover:rotate-12 transition-all"
|
className="hover:rotate-12 transition-all"
|
||||||
src={darkBackground ? '/logo-white.svg' : '/logo.svg'}
|
src={darkBackground ? '/logo-white.svg' : '/logo.svg'}
|
||||||
width={45}
|
width={45}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export function MenuButton(props: {
|
||||||
}) {
|
}) {
|
||||||
const { buttonContent, menuItems, className } = props
|
const { buttonContent, menuItems, className } = props
|
||||||
return (
|
return (
|
||||||
<Menu as="div" className={clsx('flex-shrink-0 relative z-10', className)}>
|
<Menu as="div" className={clsx('flex-shrink-0 relative z-50', className)}>
|
||||||
<div>
|
<div>
|
||||||
<Menu.Button className="rounded-full flex">
|
<Menu.Button className="rounded-full flex">
|
||||||
<span className="sr-only">Open user menu</span>
|
<span className="sr-only">Open user menu</span>
|
||||||
|
|
|
@ -10,9 +10,10 @@ import { ProfileMenu } from './profile-menu'
|
||||||
export function NavBar(props: {
|
export function NavBar(props: {
|
||||||
darkBackground?: boolean
|
darkBackground?: boolean
|
||||||
wide?: boolean
|
wide?: boolean
|
||||||
|
assertUser?: 'signed-in' | 'signed-out'
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { darkBackground, wide, className } = props
|
const { darkBackground, wide, assertUser, className } = props
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
|
@ -31,8 +32,12 @@ export function NavBar(props: {
|
||||||
<ManifoldLogo className="my-1" darkBackground={darkBackground} />
|
<ManifoldLogo className="my-1" darkBackground={darkBackground} />
|
||||||
|
|
||||||
<Row className="items-center gap-6 sm:gap-8 ml-6">
|
<Row className="items-center gap-6 sm:gap-8 ml-6">
|
||||||
{(user || user === null) && (
|
{(user || user === null || assertUser) && (
|
||||||
<NavOptions user={user} themeClasses={themeClasses} />
|
<NavOptions
|
||||||
|
user={user}
|
||||||
|
assertUser={assertUser}
|
||||||
|
themeClasses={themeClasses}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -40,11 +45,19 @@ export function NavBar(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavOptions(props: { user: User | null; themeClasses: string }) {
|
function NavOptions(props: {
|
||||||
const { user, themeClasses } = props
|
user: User | null | undefined
|
||||||
|
assertUser: 'signed-in' | 'signed-out' | undefined
|
||||||
|
themeClasses: string
|
||||||
|
}) {
|
||||||
|
const { user, assertUser, themeClasses } = props
|
||||||
|
const showSignedIn = assertUser === 'signed-in' || !!user
|
||||||
|
const showSignedOut =
|
||||||
|
!showSignedIn && (assertUser === 'signed-out' || user === null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{user === null && (
|
{showSignedOut && (
|
||||||
<Link href="/about">
|
<Link href="/about">
|
||||||
<a
|
<a
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -57,7 +70,7 @@ function NavOptions(props: { user: User | null; themeClasses: string }) {
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* <Link href="/folds">
|
<Link href="/folds">
|
||||||
<a
|
<a
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'text-base hidden md:block whitespace-nowrap',
|
'text-base hidden md:block whitespace-nowrap',
|
||||||
|
@ -66,7 +79,7 @@ function NavOptions(props: { user: User | null; themeClasses: string }) {
|
||||||
>
|
>
|
||||||
Folds
|
Folds
|
||||||
</a>
|
</a>
|
||||||
</Link> */}
|
</Link>
|
||||||
|
|
||||||
<Link href="/markets">
|
<Link href="/markets">
|
||||||
<a
|
<a
|
||||||
|
@ -75,35 +88,21 @@ function NavOptions(props: { user: User | null; themeClasses: string }) {
|
||||||
themeClasses
|
themeClasses
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
All markets
|
Markets
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{user === null ? (
|
{showSignedOut && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="btn border-none normal-case text-base font-medium px-6 bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600"
|
className="btn btn-sm btn-outline normal-case text-base font-medium px-6 bg-gradient-to-r"
|
||||||
onClick={firebaseLogin}
|
onClick={firebaseLogin}
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Link href="/leaderboards">
|
|
||||||
<a
|
|
||||||
className={clsx(
|
|
||||||
'text-base hidden md:block whitespace-nowrap',
|
|
||||||
themeClasses
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Leaderboards
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<ProfileMenu user={user} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
{showSignedIn && <ProfileMenu user={user ?? undefined} />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { NavBar } from './nav-bar'
|
import { NavBar } from './nav-bar'
|
||||||
|
|
||||||
export function Page(props: { wide?: boolean; children?: any }) {
|
export function Page(props: {
|
||||||
const { wide, children } = props
|
wide?: boolean
|
||||||
|
margin?: boolean
|
||||||
|
assertUser?: 'signed-in' | 'signed-out'
|
||||||
|
children?: any
|
||||||
|
}) {
|
||||||
|
const { wide, margin, assertUser, children } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<NavBar wide={wide} />
|
<NavBar wide={wide} assertUser={assertUser} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full px-2 pb-8 mx-auto',
|
'w-full mx-auto',
|
||||||
wide ? 'max-w-6xl' : 'max-w-4xl'
|
wide ? 'max-w-6xl' : 'max-w-4xl',
|
||||||
|
margin && 'px-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import Image from 'next/image'
|
|
||||||
import { firebaseLogout, User } from '../lib/firebase/users'
|
import { firebaseLogout, User } from '../lib/firebase/users'
|
||||||
import { formatMoney } from '../lib/util/format'
|
import { formatMoney } from '../lib/util/format'
|
||||||
|
import { Avatar } from './avatar'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { MenuButton } from './menu'
|
import { MenuButton } from './menu'
|
||||||
|
|
||||||
export function ProfileMenu(props: { user: User }) {
|
export function ProfileMenu(props: { user: User | undefined }) {
|
||||||
const { user } = props
|
const { user } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -24,12 +24,15 @@ export function ProfileMenu(props: { user: User }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNavigationOptions(user: User, options: { mobile: boolean }) {
|
function getNavigationOptions(
|
||||||
|
user: User | undefined,
|
||||||
|
options: { mobile: boolean }
|
||||||
|
) {
|
||||||
const { mobile } = options
|
const { mobile } = options
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
href: '/',
|
href: user ? '/home' : '/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
|
@ -38,9 +41,13 @@ function getNavigationOptions(user: User, options: { mobile: boolean }) {
|
||||||
...(mobile
|
...(mobile
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: 'All markets',
|
name: 'Markets',
|
||||||
href: '/markets',
|
href: '/markets',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Folds',
|
||||||
|
href: '/folds',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
|
@ -49,7 +56,11 @@ function getNavigationOptions(user: User, options: { mobile: boolean }) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Your markets',
|
name: 'Your markets',
|
||||||
href: `/${user.username}`,
|
href: `/${user?.username ?? ''}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Leaderboards',
|
||||||
|
href: '/leaderboards',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Discord',
|
name: 'Discord',
|
||||||
|
@ -67,17 +78,16 @@ function getNavigationOptions(user: User, options: { mobile: boolean }) {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProfileSummary(props: { user: User }) {
|
function ProfileSummary(props: { user: User | undefined }) {
|
||||||
const { user } = props
|
const { user } = props
|
||||||
return (
|
return (
|
||||||
<Col className="avatar items-center sm:flex-row gap-2 sm:gap-0">
|
<Col className="avatar items-center sm:flex-row gap-2 sm:gap-4">
|
||||||
<div className="rounded-full w-10 h-10 sm:mr-4">
|
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} noLink />
|
||||||
{user.avatarUrl && <img src={user.avatarUrl} width={40} height={40} />}
|
|
||||||
</div>
|
<div className="truncate text-left sm:w-32">
|
||||||
<div className="truncate text-left" style={{ maxWidth: 170 }}>
|
<div className="hidden sm:flex">{user?.name}</div>
|
||||||
<div className="hidden sm:flex">{user.name}</div>
|
|
||||||
<div className="text-gray-700 text-sm">
|
<div className="text-gray-700 text-sm">
|
||||||
{formatMoney(Math.floor(user.balance))}
|
{user ? formatMoney(Math.floor(user.balance)) : ' '}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -57,10 +57,8 @@ export function ResolutionPanel(props: {
|
||||||
: 'btn-disabled'
|
: 'btn-disabled'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col className={clsx('bg-white px-8 py-6 rounded-md', className)}>
|
||||||
className={clsx('bg-gray-100 shadow-md px-8 py-6 rounded-md', className)}
|
<Title className="mt-0" text="Your market" />
|
||||||
>
|
|
||||||
<Title className="mt-0 text-neutral" text="Your market" />
|
|
||||||
|
|
||||||
<div className="pt-2 pb-1 text-sm text-gray-500">Resolve outcome</div>
|
<div className="pt-2 pb-1 text-sm text-gray-500">Resolve outcome</div>
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const SiteLink = (props: {
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'hover:underline hover:decoration-indigo-400 hover:decoration-2',
|
'hover:underline hover:decoration-indigo-400 hover:decoration-2 z-10',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
@ -23,7 +23,7 @@ export const SiteLink = (props: {
|
||||||
<Link href={href}>
|
<Link href={href}>
|
||||||
<a
|
<a
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'hover:underline hover:decoration-indigo-400 hover:decoration-2',
|
'hover:underline hover:decoration-indigo-400 hover:decoration-2 z-10',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
|
45
web/components/tags-input.tsx
Normal file
45
web/components/tags-input.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { parseWordsAsTags } from '../../common/util/parse'
|
||||||
|
import { Contract, updateContract } from '../lib/firebase/contracts'
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
import { TagsList } from './tags-list'
|
||||||
|
|
||||||
|
export function TagsInput(props: { contract: Contract }) {
|
||||||
|
const { contract } = props
|
||||||
|
const { tags } = contract
|
||||||
|
|
||||||
|
const [tagText, setTagText] = useState('')
|
||||||
|
const newTags = parseWordsAsTags(`${tags.join(' ')} ${tagText}`)
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
const updateTags = () => {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
updateContract(contract.id, {
|
||||||
|
tags: newTags,
|
||||||
|
lowercaseTags: newTags.map((tag) => tag.toLowerCase()),
|
||||||
|
})
|
||||||
|
setIsSubmitting(false)
|
||||||
|
setTagText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row className="flex-wrap gap-4">
|
||||||
|
<TagsList tags={newTags.map((tag) => `#${tag}`)} />
|
||||||
|
|
||||||
|
<Row className="items-center gap-4">
|
||||||
|
<input
|
||||||
|
style={{ maxWidth: 150 }}
|
||||||
|
placeholder="Type a tag..."
|
||||||
|
className="input input-sm input-bordered resize-none"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
value={tagText}
|
||||||
|
onChange={(e) => setTagText(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-xs btn-outline" onClick={updateTags}>
|
||||||
|
Save tags
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
|
@ -37,7 +37,7 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
||||||
url={`/@${user.username}`}
|
url={`/@${user.username}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Title text={possesive + 'markets'} />
|
<Title className="mx-4 md:mx-0" text={possesive + 'markets'} />
|
||||||
|
|
||||||
<CreatorContractsList creator={user} />
|
<CreatorContractsList creator={user} />
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -5,27 +5,39 @@ import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
|
|
||||||
export function YesNoSelector(props: {
|
export function YesNoSelector(props: {
|
||||||
selected: 'YES' | 'NO'
|
selected?: 'YES' | 'NO'
|
||||||
onSelect: (selected: 'YES' | 'NO') => void
|
onSelect: (selected: 'YES' | 'NO') => void
|
||||||
className?: string
|
className?: string
|
||||||
|
btnClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const { selected, onSelect, className } = props
|
const { selected, onSelect, className, btnClassName } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={clsx('space-x-3', className)}>
|
<Row className={clsx('space-x-3', className)}>
|
||||||
<Button
|
<button
|
||||||
color={selected === 'YES' ? 'green' : 'gray'}
|
className={clsx(
|
||||||
|
'flex-1 inline-flex justify-center items-center p-2 hover:bg-primary-focus hover:text-white rounded-lg border-primary hover:border-primary-focus border-2',
|
||||||
|
selected == 'YES'
|
||||||
|
? 'bg-primary text-white'
|
||||||
|
: 'bg-transparent text-primary',
|
||||||
|
btnClassName
|
||||||
|
)}
|
||||||
onClick={() => onSelect('YES')}
|
onClick={() => onSelect('YES')}
|
||||||
>
|
>
|
||||||
YES
|
Buy YES
|
||||||
</Button>
|
</button>
|
||||||
|
<button
|
||||||
<Button
|
className={clsx(
|
||||||
color={selected === 'NO' ? 'red' : 'gray'}
|
'flex-1 inline-flex justify-center items-center p-2 hover:bg-red-500 hover:text-white rounded-lg border-red-400 hover:border-red-500 border-2',
|
||||||
|
selected == 'NO'
|
||||||
|
? 'bg-red-400 text-white'
|
||||||
|
: 'bg-transparent text-red-400',
|
||||||
|
btnClassName
|
||||||
|
)}
|
||||||
onClick={() => onSelect('NO')}
|
onClick={() => onSelect('NO')}
|
||||||
>
|
>
|
||||||
NO
|
Buy NO
|
||||||
</Button>
|
</button>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Fold } from '../../common/fold'
|
import { Fold } from '../../common/fold'
|
||||||
import { listenForFold, listenForFolds } from '../lib/firebase/folds'
|
import { User } from '../../common/user'
|
||||||
|
import {
|
||||||
|
listenForFold,
|
||||||
|
listenForFolds,
|
||||||
|
listenForFollow,
|
||||||
|
} from '../lib/firebase/folds'
|
||||||
|
|
||||||
export const useFold = (foldId: string) => {
|
export const useFold = (foldId: string | undefined) => {
|
||||||
const [fold, setFold] = useState<Fold | null | undefined>()
|
const [fold, setFold] = useState<Fold | null | undefined>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return listenForFold(foldId, setFold)
|
if (foldId) return listenForFold(foldId, setFold)
|
||||||
}, [foldId])
|
}, [foldId])
|
||||||
|
|
||||||
return fold
|
return fold
|
||||||
|
@ -21,3 +26,13 @@ export const useFolds = () => {
|
||||||
|
|
||||||
return folds
|
return folds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useFollowingFold = (fold: Fold, user: User | null | undefined) => {
|
||||||
|
const [following, setFollowing] = useState<boolean | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) return listenForFollow(fold, user, setFollowing)
|
||||||
|
}, [fold, user])
|
||||||
|
|
||||||
|
return following
|
||||||
|
}
|
||||||
|
|
|
@ -52,6 +52,13 @@ export function contractMetrics(contract: Contract) {
|
||||||
return { truePool, probPercent, startProb, createdDate, resolvedDate }
|
return { truePool, probPercent, startProb, createdDate, resolvedDate }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function tradingAllowed(contract: Contract) {
|
||||||
|
return (
|
||||||
|
!contract.isResolved &&
|
||||||
|
(!contract.closeTime || contract.closeTime > Date.now())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const db = getFirestore(app)
|
const db = getFirestore(app)
|
||||||
export const contractCollection = collection(db, 'contracts')
|
export const contractCollection = collection(db, 'contracts')
|
||||||
|
|
||||||
|
@ -153,7 +160,7 @@ export function listenForHotContracts(
|
||||||
export async function getHotContracts() {
|
export async function getHotContracts() {
|
||||||
const contracts = await getValues<Contract>(hotContractsQuery)
|
const contracts = await getValues<Contract>(hotContractsQuery)
|
||||||
return _.sortBy(
|
return _.sortBy(
|
||||||
chooseRandomSubset(contracts, 6),
|
chooseRandomSubset(contracts, 10),
|
||||||
(contract) => -1 * contract.volume24Hours
|
(contract) => -1 * contract.volume24Hours
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
import { collection, doc, query, updateDoc, where } from 'firebase/firestore'
|
import {
|
||||||
|
collection,
|
||||||
|
deleteDoc,
|
||||||
|
doc,
|
||||||
|
query,
|
||||||
|
setDoc,
|
||||||
|
updateDoc,
|
||||||
|
where,
|
||||||
|
} from 'firebase/firestore'
|
||||||
import { Fold } from '../../../common/fold'
|
import { Fold } from '../../../common/fold'
|
||||||
import { Contract, contractCollection } from './contracts'
|
import { Contract, contractCollection } from './contracts'
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
|
import { User } from './users'
|
||||||
import { getValues, listenForValue, listenForValues } from './utils'
|
import { getValues, listenForValue, listenForValues } from './utils'
|
||||||
|
|
||||||
const foldCollection = collection(db, 'folds')
|
const foldCollection = collection(db, 'folds')
|
||||||
|
@ -17,6 +26,10 @@ export function updateFold(fold: Fold, updates: Partial<Fold>) {
|
||||||
return updateDoc(doc(foldCollection, fold.id), updates)
|
return updateDoc(doc(foldCollection, fold.id), updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteFold(fold: Fold) {
|
||||||
|
return deleteDoc(doc(foldCollection, fold.id))
|
||||||
|
}
|
||||||
|
|
||||||
export async function listAllFolds() {
|
export async function listAllFolds() {
|
||||||
return getValues<Fold>(foldCollection)
|
return getValues<Fold>(foldCollection)
|
||||||
}
|
}
|
||||||
|
@ -90,3 +103,24 @@ export function listenForFold(
|
||||||
) {
|
) {
|
||||||
return listenForValue(doc(foldCollection, foldId), setFold)
|
return listenForValue(doc(foldCollection, foldId), setFold)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function followFold(fold: Fold, user: User) {
|
||||||
|
const followDoc = doc(foldCollection, fold.id, 'followers', user.id)
|
||||||
|
return setDoc(followDoc, { userId: user.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unfollowFold(fold: Fold, user: User) {
|
||||||
|
const followDoc = doc(foldCollection, fold.id, 'followers', user.id)
|
||||||
|
return deleteDoc(followDoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenForFollow(
|
||||||
|
fold: Fold,
|
||||||
|
user: User,
|
||||||
|
setFollow: (following: boolean) => void
|
||||||
|
) {
|
||||||
|
const followDoc = doc(foldCollection, fold.id, 'followers', user.id)
|
||||||
|
return listenForValue(followDoc, (value) => {
|
||||||
|
setFollow(!!value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ export function formatPercent(zeroToOne: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toCamelCase(words: string) {
|
export function toCamelCase(words: string) {
|
||||||
return words
|
const camelCase = words
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.map((word) => word.trim())
|
.map((word) => word.trim())
|
||||||
.filter((word) => word)
|
.filter((word) => word)
|
||||||
|
@ -26,4 +26,8 @@ export function toCamelCase(words: string) {
|
||||||
index === 0 ? word : word[0].toLocaleUpperCase() + word.substring(1)
|
index === 0 ? word : word[0].toLocaleUpperCase() + word.substring(1)
|
||||||
)
|
)
|
||||||
.join('')
|
.join('')
|
||||||
|
|
||||||
|
// Remove non-alpha-numeric-underscore chars.
|
||||||
|
const regex = /(?:^|\s)(?:[a-z0-9_]+)/gi
|
||||||
|
return (camelCase.match(regex) || [])[0] ?? ''
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
contractMetrics,
|
contractMetrics,
|
||||||
Contract,
|
Contract,
|
||||||
getContractFromSlug,
|
getContractFromSlug,
|
||||||
|
tradingAllowed,
|
||||||
} from '../../lib/firebase/contracts'
|
} from '../../lib/firebase/contracts'
|
||||||
import { SEO } from '../../components/SEO'
|
import { SEO } from '../../components/SEO'
|
||||||
import { Page } from '../../components/page'
|
import { Page } from '../../components/page'
|
||||||
|
@ -70,8 +71,7 @@ export default function ContractPage(props: {
|
||||||
|
|
||||||
const { creatorId, isResolved, resolution, question } = contract
|
const { creatorId, isResolved, resolution, question } = contract
|
||||||
const isCreator = user?.id === creatorId
|
const isCreator = user?.id === creatorId
|
||||||
const allowTrade =
|
const allowTrade = tradingAllowed(contract)
|
||||||
!isResolved && (!contract.closeTime || contract.closeTime > Date.now())
|
|
||||||
const allowResolve = !isResolved && isCreator && !!user
|
const allowResolve = !isResolved && isCreator && !!user
|
||||||
|
|
||||||
const { probPercent } = contractMetrics(contract)
|
const { probPercent } = contractMetrics(contract)
|
||||||
|
@ -97,8 +97,8 @@ export default function ContractPage(props: {
|
||||||
ogCardProps={ogCardProps}
|
ogCardProps={ogCardProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Col className="w-full md:flex-row justify-between mt-6">
|
<Col className="w-full md:flex-row justify-between">
|
||||||
<div className="flex-[3]">
|
<div className="flex-[3] bg-white px-2 py-6 md:px-6 md:py-8 rounded border-0 border-gray-100">
|
||||||
<ContractOverview
|
<ContractOverview
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={bets ?? []}
|
bets={bets ?? []}
|
||||||
|
@ -109,10 +109,12 @@ export default function ContractPage(props: {
|
||||||
|
|
||||||
{(allowTrade || allowResolve) && (
|
{(allowTrade || allowResolve) && (
|
||||||
<>
|
<>
|
||||||
<div className="md:ml-8" />
|
<div className="md:ml-6" />
|
||||||
|
|
||||||
<Col className="flex-1">
|
<Col className="flex-1">
|
||||||
{allowTrade && <BetPanel contract={contract} />}
|
{allowTrade && (
|
||||||
|
<BetPanel className="hidden lg:inline" contract={contract} />
|
||||||
|
)}
|
||||||
{allowResolve && (
|
{allowResolve && (
|
||||||
<ResolutionPanel creator={user} contract={contract} />
|
<ResolutionPanel creator={user} contract={contract} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import styles from './about.module.css'
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page margin>
|
||||||
<SEO title="About" description="About" url="/about" />
|
<SEO title="About" description="About" url="/about" />
|
||||||
<Contents />
|
<Contents />
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -5,8 +5,6 @@ import { Title } from '../components/title'
|
||||||
import { FundsSelector } from '../components/yes-no-selector'
|
import { FundsSelector } from '../components/yes-no-selector'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { checkoutURL } from '../lib/service/stripe'
|
import { checkoutURL } from '../lib/service/stripe'
|
||||||
import Image from 'next/image'
|
|
||||||
import { Spacer } from '../components/layout/spacer'
|
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
|
|
||||||
export default function AddFundsPage() {
|
export default function AddFundsPage() {
|
||||||
|
@ -23,7 +21,7 @@ export default function AddFundsPage() {
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col>
|
<Col>
|
||||||
<Title text="Get Manifold Dollars" />
|
<Title text="Get Manifold Dollars" />
|
||||||
<Image
|
<img
|
||||||
className="block mt-6"
|
className="block mt-6"
|
||||||
src="/praying-mantis-light.svg"
|
src="/praying-mantis-light.svg"
|
||||||
width={200}
|
width={200}
|
||||||
|
|
|
@ -47,8 +47,8 @@ export default function Create() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow user to create a new contract
|
// Allow user to create a new contract
|
||||||
export function NewContract(props: { question: string }) {
|
export function NewContract(props: { question: string; tag?: string }) {
|
||||||
const question = props.question
|
const { question, tag } = props
|
||||||
const creator = useUser()
|
const creator = useUser()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -72,12 +72,12 @@ export function NewContract(props: { question: string }) {
|
||||||
|
|
||||||
const [anteError, setAnteError] = useState<string | undefined>()
|
const [anteError, setAnteError] = useState<string | undefined>()
|
||||||
// By default, close the market a week from today
|
// By default, close the market a week from today
|
||||||
const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DD')
|
// const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DD')
|
||||||
const [closeDate, setCloseDate] = useState(weekFromToday)
|
const [closeDate, setCloseDate] = useState<undefined | string>(undefined)
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
const closeTime = dateToMillis(closeDate)
|
const closeTime = closeDate ? dateToMillis(closeDate) : undefined
|
||||||
|
|
||||||
const balance = creator?.balance || 0
|
const balance = creator?.balance || 0
|
||||||
|
|
||||||
|
@ -104,6 +104,7 @@ export function NewContract(props: { question: string }) {
|
||||||
initialProb,
|
initialProb,
|
||||||
ante,
|
ante,
|
||||||
closeTime,
|
closeTime,
|
||||||
|
tags: tag ? [tag] : [],
|
||||||
}).then((r) => r.data || {})
|
}).then((r) => r.data || {})
|
||||||
|
|
||||||
if (result.status !== 'success') {
|
if (result.status !== 'success') {
|
||||||
|
|
|
@ -28,9 +28,12 @@ import { useRouter } from 'next/router'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring'
|
import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring'
|
||||||
import { Leaderboard } from '../../../components/leaderboard'
|
import { Leaderboard } from '../../../components/leaderboard'
|
||||||
import { formatMoney } from '../../../lib/util/format'
|
import { formatMoney, toCamelCase } from '../../../lib/util/format'
|
||||||
import { EditFoldButton } from '../../../components/edit-fold-button'
|
import { EditFoldButton } from '../../../components/edit-fold-button'
|
||||||
import Custom404 from '../../404'
|
import Custom404 from '../../404'
|
||||||
|
import { FollowFoldButton } from '../../../components/follow-fold-button'
|
||||||
|
import FeedCreate from '../../../components/feed-create'
|
||||||
|
import { SEO } from '../../../components/SEO'
|
||||||
|
|
||||||
export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||||
const { slugs } = props.params
|
const { slugs } = props.params
|
||||||
|
@ -96,7 +99,8 @@ async function toUserScores(userScores: { [userId: string]: number }) {
|
||||||
const topUserPairs = _.take(
|
const topUserPairs = _.take(
|
||||||
_.sortBy(Object.entries(userScores), ([_, score]) => -1 * score),
|
_.sortBy(Object.entries(userScores), ([_, score]) => -1 * score),
|
||||||
10
|
10
|
||||||
)
|
).filter(([_, score]) => score > 0)
|
||||||
|
|
||||||
const topUsers = await Promise.all(
|
const topUsers = await Promise.all(
|
||||||
topUserPairs.map(([userId]) => getUser(userId))
|
topUserPairs.map(([userId]) => getUser(userId))
|
||||||
)
|
)
|
||||||
|
@ -138,7 +142,7 @@ export default function FoldPage(props: {
|
||||||
|
|
||||||
const page = (slugs[1] ?? 'activity') as typeof foldSubpages[number]
|
const page = (slugs[1] ?? 'activity') as typeof foldSubpages[number]
|
||||||
|
|
||||||
const fold = useFold(props.fold?.id ?? '') ?? props.fold
|
const fold = useFold(props.fold?.id) ?? props.fold
|
||||||
|
|
||||||
const { query, setQuery, sort, setSort } = useQueryAndSortParams({
|
const { query, setQuery, sort, setSort } = useQueryAndSortParams({
|
||||||
defaultSort: 'most-traded',
|
defaultSort: 'most-traded',
|
||||||
|
@ -153,71 +157,96 @@ export default function FoldPage(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page wide>
|
<Page wide>
|
||||||
<Col className="items-center">
|
<SEO
|
||||||
<Col className="max-w-5xl w-full">
|
title={fold.name}
|
||||||
<Col className="sm:flex-row sm:justify-between sm:items-end gap-4 mb-6">
|
description={`Curated by ${curator.name}. ${fold.about}`}
|
||||||
<Title className="!m-0" text={fold.name} />
|
url={foldPath(fold)}
|
||||||
{isCurator && <EditFoldButton fold={fold} />}
|
/>
|
||||||
</Col>
|
|
||||||
|
|
||||||
<div className="tabs mb-4">
|
<div className="px-3 lg:px-1">
|
||||||
<Link href={foldPath(fold)} shallow>
|
<Row className="justify-between mb-6">
|
||||||
<a
|
<Title className="!m-0" text={fold.name} />
|
||||||
className={clsx(
|
{isCurator ? (
|
||||||
'tab tab-bordered',
|
<EditFoldButton className="ml-1" fold={fold} />
|
||||||
page === 'activity' && 'tab-active'
|
) : (
|
||||||
)}
|
<FollowFoldButton className="ml-1" fold={fold} />
|
||||||
>
|
)}
|
||||||
Activity
|
</Row>
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link href={foldPath(fold, 'markets')} shallow>
|
<Col className="md:hidden text-gray-500 gap-2 mb-6">
|
||||||
<a
|
<Row>
|
||||||
className={clsx(
|
<div className="mr-1">Curated by</div>
|
||||||
'tab tab-bordered',
|
<UserLink
|
||||||
page === 'markets' && 'tab-active'
|
className="text-neutral"
|
||||||
)}
|
name={curator.name}
|
||||||
>
|
username={curator.username}
|
||||||
Markets
|
/>
|
||||||
</a>
|
</Row>
|
||||||
</Link>
|
<div>{fold.about}</div>
|
||||||
<Link href={foldPath(fold, 'leaderboards')} shallow>
|
</Col>
|
||||||
<a
|
</div>
|
||||||
className={clsx(
|
|
||||||
'tab tab-bordered',
|
|
||||||
page === 'leaderboards' && 'tab-active',
|
|
||||||
page !== 'leaderboards' && 'md:hidden'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Leaderboards
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{page === 'activity' && (
|
<div className="tabs mb-2">
|
||||||
<Row className="gap-8">
|
<Link href={foldPath(fold)} shallow>
|
||||||
<Col>
|
<a
|
||||||
|
className={clsx(
|
||||||
|
'tab tab-bordered',
|
||||||
|
page === 'activity' && 'tab-active'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Activity
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href={foldPath(fold, 'markets')} shallow>
|
||||||
|
<a
|
||||||
|
className={clsx(
|
||||||
|
'tab tab-bordered',
|
||||||
|
page === 'markets' && 'tab-active'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Markets
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Link href={foldPath(fold, 'leaderboards')} shallow>
|
||||||
|
<a
|
||||||
|
className={clsx(
|
||||||
|
'tab tab-bordered',
|
||||||
|
page === 'leaderboards' && 'tab-active',
|
||||||
|
page !== 'leaderboards' && 'md:hidden'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Leaderboards
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(page === 'activity' || page === 'markets') && (
|
||||||
|
<Row className={clsx(page === 'activity' ? 'gap-16' : 'gap-8')}>
|
||||||
|
<Col className="flex-1">
|
||||||
|
{user !== null && (
|
||||||
|
<FeedCreate
|
||||||
|
className={clsx(page !== 'activity' && 'hidden')}
|
||||||
|
user={user}
|
||||||
|
tag={toCamelCase(fold.name)}
|
||||||
|
placeholder={`Type your question about ${fold.name}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{page === 'activity' ? (
|
||||||
|
<>
|
||||||
<ActivityFeed
|
<ActivityFeed
|
||||||
contracts={activeContracts}
|
contracts={activeContracts}
|
||||||
contractBets={activeContractBets}
|
contractBets={activeContractBets}
|
||||||
contractComments={activeContractComments}
|
contractComments={activeContractComments}
|
||||||
/>
|
/>
|
||||||
</Col>
|
{activeContracts.length === 0 && (
|
||||||
<Col className="hidden md:flex max-w-xs gap-10">
|
<div className="text-gray-500 mt-4 mx-2 lg:mx-0">
|
||||||
<FoldOverview fold={fold} curator={curator} />
|
No activity from matching markets.{' '}
|
||||||
<FoldLeaderboards
|
{isCurator && 'Try editing to add more tags!'}
|
||||||
topTraders={topTraders}
|
</div>
|
||||||
topTraderScores={topTraderScores}
|
)}
|
||||||
topCreators={topCreators}
|
</>
|
||||||
topCreatorScores={topCreatorScores}
|
) : (
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{page === 'markets' && (
|
|
||||||
<div className="w-full">
|
|
||||||
<SearchableGrid
|
<SearchableGrid
|
||||||
contracts={contracts}
|
contracts={contracts}
|
||||||
query={query}
|
query={query}
|
||||||
|
@ -225,21 +254,30 @@ export default function FoldPage(props: {
|
||||||
sort={sort}
|
sort={sort}
|
||||||
setSort={setSort}
|
setSort={setSort}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
)}
|
</Col>
|
||||||
|
<Col className="hidden md:flex max-w-xs w-full gap-10">
|
||||||
|
<FoldOverview fold={fold} curator={curator} />
|
||||||
|
<FoldLeaderboards
|
||||||
|
topTraders={topTraders}
|
||||||
|
topTraderScores={topTraderScores}
|
||||||
|
topCreators={topCreators}
|
||||||
|
topCreatorScores={topCreatorScores}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
{page === 'leaderboards' && (
|
{page === 'leaderboards' && (
|
||||||
<Col className="gap-8 lg:flex-row">
|
<Col className="gap-8 lg:flex-row">
|
||||||
<FoldLeaderboards
|
<FoldLeaderboards
|
||||||
topTraders={topTraders}
|
topTraders={topTraders}
|
||||||
topTraderScores={topTraderScores}
|
topTraderScores={topTraderScores}
|
||||||
topCreators={topCreators}
|
topCreators={topCreators}
|
||||||
topCreatorScores={topCreatorScores}
|
topCreatorScores={topCreatorScores}
|
||||||
/>
|
/>
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -249,11 +287,11 @@ function FoldOverview(props: { fold: Fold; curator: User }) {
|
||||||
const { about, tags } = fold
|
const { about, tags } = fold
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="max-w-sm">
|
<Col>
|
||||||
<div className="px-4 py-3 bg-indigo-700 text-white text-sm rounded-t">
|
<div className="px-4 py-3 bg-indigo-500 text-white text-sm rounded-t">
|
||||||
About community
|
About community
|
||||||
</div>
|
</div>
|
||||||
<Col className="p-4 bg-white self-start gap-2 rounded-b">
|
<Col className="p-4 bg-white gap-2 rounded-b">
|
||||||
<Row>
|
<Row>
|
||||||
<div className="text-gray-500 mr-1">Curated by</div>
|
<div className="text-gray-500 mr-1">Curated by</div>
|
||||||
<UserLink
|
<UserLink
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import Link from 'next/link'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Fold } from '../../common/fold'
|
import { Fold } from '../../common/fold'
|
||||||
import { CreateFoldButton } from '../components/create-fold-button'
|
import { CreateFoldButton } from '../components/create-fold-button'
|
||||||
|
import { FollowFoldButton } from '../components/follow-fold-button'
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from '../components/layout/col'
|
||||||
import { Row } from '../components/layout/row'
|
import { Row } from '../components/layout/row'
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
|
@ -39,7 +41,8 @@ export default function Folds(props: {
|
||||||
}) {
|
}) {
|
||||||
const [curatorsDict, setCuratorsDict] = useState(props.curatorsDict)
|
const [curatorsDict, setCuratorsDict] = useState(props.curatorsDict)
|
||||||
|
|
||||||
const folds = useFolds() ?? props.folds
|
let folds = useFolds() ?? props.folds
|
||||||
|
folds = _.sortBy(folds, (fold) => -1 * fold.followCount)
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -60,36 +63,26 @@ export default function Folds(props: {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="max-w-2xl w-full px-2 sm:px-0">
|
<Col className="max-w-lg w-full">
|
||||||
<Row className="justify-between items-center">
|
<Col className="px-4 sm:px-0">
|
||||||
<Title text="Manifold communities: Folds" />
|
<Row className="justify-between items-center">
|
||||||
{user && <CreateFoldButton />}
|
<Title text="Explore folds" />
|
||||||
</Row>
|
{user && <CreateFoldButton />}
|
||||||
|
</Row>
|
||||||
|
|
||||||
<div className="text-gray-500 mb-6">
|
<div className="text-gray-500 mb-6">
|
||||||
Browse folds on topics that interest you.
|
Folds are communities on Manifold centered around a collection of
|
||||||
</div>
|
markets.
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Col className="gap-4">
|
<Col className="gap-2">
|
||||||
{folds.map((fold) => (
|
{folds.map((fold) => (
|
||||||
<Col key={fold.id} className="gap-2">
|
<FoldCard
|
||||||
<Row className="items-center flex-wrap gap-2">
|
key={fold.id}
|
||||||
<SiteLink href={foldPath(fold)}>{fold.name}</SiteLink>
|
fold={fold}
|
||||||
<div />
|
curator={curatorsDict[fold.curatorId]}
|
||||||
<div className="text-sm text-gray-500">12 followers</div>
|
/>
|
||||||
<div className="text-gray-500">•</div>
|
|
||||||
<Row>
|
|
||||||
<div className="text-sm text-gray-500 mr-1">Curated by</div>
|
|
||||||
<UserLink
|
|
||||||
className="text-sm text-neutral"
|
|
||||||
name={curatorsDict[fold.curatorId]?.name ?? ''}
|
|
||||||
username={curatorsDict[fold.curatorId]?.username ?? ''}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
</Row>
|
|
||||||
<div className="text-gray-500 text-sm">{fold.about}</div>
|
|
||||||
<div />
|
|
||||||
</Col>
|
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -97,3 +90,34 @@ export default function Folds(props: {
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FoldCard(props: { fold: Fold; curator: User | undefined }) {
|
||||||
|
const { fold, curator } = props
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
key={fold.id}
|
||||||
|
className="bg-white hover:bg-gray-100 p-4 rounded-xl gap-1 shadow-md relative"
|
||||||
|
>
|
||||||
|
<Link href={foldPath(fold)}>
|
||||||
|
<a className="absolute left-0 right-0 top-0 bottom-0" />
|
||||||
|
</Link>
|
||||||
|
<Row className="justify-between items-center gap-2">
|
||||||
|
<SiteLink href={foldPath(fold)}>{fold.name}</SiteLink>
|
||||||
|
<FollowFoldButton className="z-10 mb-1" fold={fold} />
|
||||||
|
</Row>
|
||||||
|
<Row className="items-center gap-2 text-gray-500 text-sm">
|
||||||
|
<div>{fold.followCount} followers</div>
|
||||||
|
<div>•</div>
|
||||||
|
<Row>
|
||||||
|
<div className="mr-1">Curated by</div>
|
||||||
|
<UserLink
|
||||||
|
className="text-neutral"
|
||||||
|
name={curator?.name ?? ''}
|
||||||
|
username={curator?.username ?? ''}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
<div className="text-gray-500 text-sm">{fold.about}</div>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -19,9 +19,6 @@ import FeedCreate from '../components/feed-create'
|
||||||
import { Spacer } from '../components/layout/spacer'
|
import { Spacer } from '../components/layout/spacer'
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from '../components/layout/col'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { ClosingSoonMarkets, HotMarkets } from './markets'
|
|
||||||
import { useContracts } from '../hooks/use-contracts'
|
|
||||||
import { useRecentComments } from '../hooks/use-comments'
|
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const [contracts, recentComments, hotContracts, closingSoonContracts] =
|
const [contracts, recentComments, hotContracts, closingSoonContracts] =
|
||||||
|
@ -66,8 +63,8 @@ const Home = (props: {
|
||||||
activeContracts,
|
activeContracts,
|
||||||
activeContractBets,
|
activeContractBets,
|
||||||
activeContractComments,
|
activeContractComments,
|
||||||
hotContracts,
|
// hotContracts,
|
||||||
closingSoonContracts,
|
// closingSoonContracts,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -87,25 +84,23 @@ const Home = (props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page assertUser="signed-in">
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="max-w-3xl">
|
<Col className="max-w-3xl">
|
||||||
<div className="-mx-2 sm:mx-0">
|
<FeedCreate user={user ?? undefined} />
|
||||||
<FeedCreate user={user ?? undefined} />
|
<Spacer h={4} />
|
||||||
<Spacer h={4} />
|
|
||||||
|
|
||||||
<HotMarkets contracts={hotContracts?.slice(0, 4) ?? []} />
|
{/* <HotMarkets contracts={hotContracts?.slice(0, 4) ?? []} />
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
<ClosingSoonMarkets contracts={closingSoonContracts ?? []} />
|
<ClosingSoonMarkets contracts={closingSoonContracts ?? []} />
|
||||||
<Spacer h={10} />
|
<Spacer h={10} /> */}
|
||||||
|
|
||||||
<ActivityFeed
|
<ActivityFeed
|
||||||
contracts={activeContracts ?? []}
|
contracts={activeContracts ?? []}
|
||||||
contractBets={activeContractBets ?? []}
|
contractBets={activeContractBets ?? []}
|
||||||
contractComments={activeContractComments ?? []}
|
contractComments={activeContractComments ?? []}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -27,12 +27,10 @@ const Home = (props: { hotContracts: Contract[] }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page assertUser="signed-out">
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="max-w-3xl">
|
<Col className="max-w-3xl">
|
||||||
<div className="-mx-2 sm:mx-0">
|
<FeedPromo hotContracts={hotContracts ?? []} />
|
||||||
<FeedPromo hotContracts={hotContracts ?? []} />
|
|
||||||
</div>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default function Leaderboards(props: {
|
||||||
const { topTraders, topCreators } = props
|
const { topTraders, topCreators } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page margin>
|
||||||
<Col className="items-center lg:flex-row gap-10">
|
<Col className="items-center lg:flex-row gap-10">
|
||||||
<Leaderboard
|
<Leaderboard
|
||||||
title="🏅 Top traders"
|
title="🏅 Top traders"
|
||||||
|
|
|
@ -1,30 +1,16 @@
|
||||||
import _ from 'lodash'
|
|
||||||
import { ContractsGrid, SearchableGrid } from '../components/contracts-list'
|
import { ContractsGrid, SearchableGrid } from '../components/contracts-list'
|
||||||
import { Spacer } from '../components/layout/spacer'
|
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
import { SEO } from '../components/SEO'
|
import { SEO } from '../components/SEO'
|
||||||
import { Title } from '../components/title'
|
import { Title } from '../components/title'
|
||||||
import { useContracts } from '../hooks/use-contracts'
|
import { useContracts } from '../hooks/use-contracts'
|
||||||
import { useQueryAndSortParams } from '../hooks/use-sort-and-query-params'
|
import { useQueryAndSortParams } from '../hooks/use-sort-and-query-params'
|
||||||
import {
|
import { Contract, listAllContracts } from '../lib/firebase/contracts'
|
||||||
Contract,
|
|
||||||
getClosingSoonContracts,
|
|
||||||
getHotContracts,
|
|
||||||
listAllContracts,
|
|
||||||
} from '../lib/firebase/contracts'
|
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const [contracts, hotContracts, closingSoonContracts] = await Promise.all([
|
const contracts = await listAllContracts().catch((_) => [])
|
||||||
listAllContracts().catch((_) => []),
|
|
||||||
getHotContracts().catch(() => []),
|
|
||||||
getClosingSoonContracts().catch(() => []),
|
|
||||||
])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
contracts,
|
contracts,
|
||||||
hotContracts,
|
|
||||||
closingSoonContracts,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
revalidate: 60, // regenerate after a minute
|
revalidate: 60, // regenerate after a minute
|
||||||
|
@ -32,15 +18,11 @@ export async function getStaticProps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rename endpoint to "Explore"
|
// TODO: Rename endpoint to "Explore"
|
||||||
export default function Markets(props: {
|
export default function Markets(props: { contracts: Contract[] }) {
|
||||||
contracts: Contract[]
|
const contracts = useContracts() ?? props.contracts ?? []
|
||||||
hotContracts: Contract[]
|
|
||||||
closingSoonContracts: Contract[]
|
|
||||||
}) {
|
|
||||||
const contracts = useContracts() ?? props.contracts
|
|
||||||
const { hotContracts, closingSoonContracts } = props
|
|
||||||
const { query, setQuery, sort, setSort } = useQueryAndSortParams({
|
const { query, setQuery, sort, setSort } = useQueryAndSortParams({
|
||||||
defaultSort: 'most-traded',
|
defaultSort: '24-hour-vol',
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -27,10 +27,8 @@ export default function TagPage(props: { contracts: Contract[] }) {
|
||||||
|
|
||||||
const contracts = useContracts()
|
const contracts = useContracts()
|
||||||
|
|
||||||
const taggedContracts = (contracts ?? props.contracts).filter(
|
const taggedContracts = (contracts ?? props.contracts).filter((contract) =>
|
||||||
(contract) =>
|
contract.lowercaseTags.includes(tag.toLowerCase())
|
||||||
contract.description.toLowerCase().includes(`#${tag.toLowerCase()}`) ||
|
|
||||||
contract.question.toLowerCase().includes(`#${tag.toLowerCase()}`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const { query, setQuery, sort, setSort } = useQueryAndSortParams({
|
const { query, setQuery, sort, setSort } = useQueryAndSortParams({
|
||||||
|
|
|
@ -10,7 +10,7 @@ export default function TradesPage() {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<SEO title="Your trades" description="Your trades" url="/trades" />
|
<SEO title="Your trades" description="Your trades" url="/trades" />
|
||||||
<Title text="Your trades" />
|
<Title className="mx-4 md:mx-0" text="Your trades" />
|
||||||
{user && <BetsList user={user} />}
|
{user && <BetsList user={user} />}
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user