Merge branch 'main' into user-profile

This commit is contained in:
mantikoros 2022-01-28 18:22:52 -06:00
commit 7dcdfdaec6
49 changed files with 878 additions and 420 deletions

View File

@ -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
} }

View File

@ -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 = {

View File

@ -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;
} }
} }
} }

View File

@ -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)

View File

@ -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 }
} }
) )

View File

@ -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'

View 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()))
})

View 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 })
})

View File

@ -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
View 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>
)
}

View File

@ -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)} />
&nbsp; <span>(+{currentReturnPercent})</span> </Row>
</div> <div>
{formatMoney(currentPayout)}
&nbsp; <span>(+{currentReturnPercent})</span>
</div>
</>
)}
<Spacer h={6} /> <Spacer h={6} />

100
web/components/bet-row.tsx Normal file
View 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"
>
&#8203;
</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>
)
}

View File

@ -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>

View File

@ -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>
) )
} }

View File

@ -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>
) )
} }

View File

@ -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>
) )

View File

@ -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])
} }

View File

@ -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>
) )
} }

View File

@ -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}

View File

@ -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>

View File

@ -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>

View 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>
)
}

View File

@ -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>
) )
} }

View File

@ -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}

View File

@ -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>

View File

@ -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} />}
</> </>
) )
} }

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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()}

View 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>
)
}

View File

@ -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>

View File

@ -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>
) )
} }

View File

@ -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
}

View File

@ -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
) )
} }

View File

@ -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)
})
}

View File

@ -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] ?? ''
} }

View File

@ -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} />
)} )}

View File

@ -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>

View File

@ -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}

View File

@ -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') {

View File

@ -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

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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 (

View File

@ -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({

View File

@ -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>
) )