Merge branch 'main' into sell-bets

This commit is contained in:
mantikoros 2021-12-20 11:10:56 -06:00
commit 4c353242b0
35 changed files with 7256 additions and 15697 deletions

View File

@ -2,9 +2,9 @@
"name": "functions", "name": "functions",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"serve": "npm run build && firebase emulators:start --only functions", "serve": "yarn build && firebase emulators:start --only functions",
"shell": "npm run build && firebase functions:shell", "shell": "yarn build && firebase functions:shell",
"start": "npm run shell", "start": "yarn shell",
"deploy": "firebase deploy --only functions", "deploy": "firebase deploy --only functions",
"logs": "firebase functions:log" "logs": "firebase functions:log"
}, },

1594
functions/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

6
package-lock.json generated
View File

@ -1,6 +0,0 @@
{
"name": "mantic",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}

3
web/.gitignore vendored
View File

@ -1,4 +1,5 @@
.DS_Store .DS_Store
.next .next
node_modules node_modules
out out
tsconfig.tsbuildinfo

View File

@ -1,5 +1,7 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
cd web npx pretty-quick --staged
npx lint-staged # Disable tsc lint for now, cuz it's been annoying
# cd web
# npx lint-staged

View File

@ -96,12 +96,9 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
return ( return (
<Col <Col
className={clsx( className={clsx('bg-gray-100 shadow-xl px-8 py-6 rounded-md', className)}
'bg-gray-100 shadow-xl px-8 py-6 rounded-md w-full md:w-auto',
className
)}
> >
<Title className="mt-0 whitespace-nowrap" text="Place a bet" /> <Title className="!mt-0 whitespace-nowrap" text="Place a bet" />
<div className="mt-2 mb-1 text-sm text-gray-400">Outcome</div> <div className="mt-2 mb-1 text-sm text-gray-400">Outcome</div>
<YesNoSelector <YesNoSelector

View File

@ -108,14 +108,14 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
<div <div
tabIndex={0} tabIndex={0}
className={clsx( className={clsx(
'p-6 bg-white card card-body shadow-xl collapse collapse-arrow cursor-pointer', 'p-6 bg-white card card-body shadow-xl collapse collapse-arrow cursor-pointer relative',
collapsed ? 'collapse-close' : 'collapse-open pb-2' collapsed ? 'collapse-close' : 'collapse-open pb-2'
)} )}
onClick={() => setCollapsed((collapsed) => !collapsed)} onClick={() => setCollapsed((collapsed) => !collapsed)}
> >
<Row className="flex-wrap gap-4"> <Row className="flex-wrap gap-4">
<Col className="flex-[2] gap-1"> <Col className="flex-[2] gap-1">
<div> <Row className="mr-6">
<Link href={path(contract)}> <Link href={path(contract)}>
<a <a
className="font-medium text-indigo-700 hover:underline hover:decoration-indigo-400 hover:decoration-2" className="font-medium text-indigo-700 hover:underline hover:decoration-indigo-400 hover:decoration-2"
@ -124,7 +124,13 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
{contract.question} {contract.question}
</a> </a>
</Link> </Link>
</div>
{/* Show carrot for collapsing. Hack the positioning. */}
<div
className="collapse-title p-0 absolute w-0 h-0 min-h-0"
style={{ top: -10, right: 4 }}
/>
</Row>
<Row className="gap-2 text-gray-500 text-sm"> <Row className="gap-2 text-gray-500 text-sm">
<div> <div>
@ -141,22 +147,17 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
</Row> </Row>
</Col> </Col>
<Row className="flex-nowrap"> <MyBetsSummary
<MyBetsSummary className="flex-1 justify-end mr-5 sm:mr-8"
className="flex-1 justify-end" contract={contract}
contract={contract} bets={bets}
bets={bets} />
/>
{/* Show carrot for collapsing. Hack the positioning. */}
<div
className="collapse-title p-0 pr-8 relative w-0 h-0 min-h-0"
style={{ top: -10, right: -20 }}
/>
</Row>
</Row> </Row>
<div className="collapse-content" style={{ backgroundColor: 'white' }}> <div
className="collapse-content !px-0"
style={{ backgroundColor: 'white' }}
>
<Spacer h={8} /> <Spacer h={8} />
<ContractBetsTable contract={contract} bets={bets} /> <ContractBetsTable contract={contract} bets={bets} />
@ -187,7 +188,7 @@ export function MyBetsSummary(props: {
) )
return ( return (
<Row className={clsx('gap-6', className)}> <Row className={clsx('gap-4 sm:gap-6', className)}>
<Col> <Col>
<div className="text-sm text-gray-500 whitespace-nowrap"> <div className="text-sm text-gray-500 whitespace-nowrap">
Total bets Total bets
@ -197,7 +198,7 @@ export function MyBetsSummary(props: {
{resolution ? ( {resolution ? (
<> <>
<Col> <Col>
<div className="text-sm text-gray-500">Winnings</div> <div className="text-sm text-gray-500">Payout</div>
<div className="whitespace-nowrap">{formatMoney(betsPayout)}</div> <div className="whitespace-nowrap">{formatMoney(betsPayout)}</div>
</Col> </Col>
</> </>
@ -309,5 +310,5 @@ function NoLabel() {
} }
function CancelLabel() { function CancelLabel() {
return <span className="text-yellow-400">CANCEL</span> return <span className="text-yellow-400">N/A</span>
} }

View File

@ -1,4 +1,4 @@
import { Fragment, useState } from 'react' import { useState } from 'react'
import { import {
compute, compute,
Contract, Contract,
@ -13,7 +13,8 @@ import router from 'next/router'
import { useUser } from '../hooks/use-user' import { useUser } from '../hooks/use-user'
import { Row } from './layout/row' import { Row } from './layout/row'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import Link from 'next/link' import { Linkify } from './linkify'
import clsx from 'clsx'
function ContractDescription(props: { function ContractDescription(props: {
contract: Contract contract: Contract
@ -33,46 +34,6 @@ function ContractDescription(props: {
setDescription(editStatement()) setDescription(editStatement())
} }
// Return a JSX span, linkifying @username, #hashtags, and https://...
function Linkify(props: { text: string }) {
const { text } = props
const regex = /(?:^|\s)(?:[@#][a-z0-9_]+|https?:\/\/\S+)/gi
const matches = text.match(regex) || []
const links = matches.map((match) => {
// Matches are in the form: " @username" or "https://example.com"
const whitespace = match.match(/^\s/)
const symbol = match.trim().substring(0, 1)
const tag = match.trim().substring(1)
const href =
{
'@': `/${tag}`,
'#': `/tag/${tag}`,
}[symbol] ?? match
return (
<>
{whitespace}
<Link href={href}>
<a className="text-indigo-700 hover:underline hover:decoration-2">
{symbol}
{tag}
</a>
</Link>
</>
)
})
return (
<span>
{text.split(regex).map((part, i) => (
<Fragment key={i}>
{part}
{links[i]}
</Fragment>
))}
</span>
)
}
return ( return (
<div className="whitespace-pre-line"> <div className="whitespace-pre-line">
<Linkify text={contract.description} /> <Linkify text={contract.description} />
@ -142,23 +103,25 @@ export const ContractOverview = (props: {
}[contract.resolution || ''] }[contract.resolution || '']
return ( return (
<Col className={className}> <Col className={clsx('mb-6', className)}>
<Col className="justify-between md:flex-row"> <Col className="justify-between md:flex-row">
<Col> <Col>
<div className="text-3xl text-indigo-700 mt-2 mb-4"> <div className="text-3xl text-indigo-700 mb-4">
{contract.question} <Linkify text={contract.question} />
</div> </div>
<ContractDetails contract={contract} /> <ContractDetails contract={contract} />
</Col> </Col>
{resolution ? ( {resolution ? (
<Col className="text-4xl mt-4 md:mt-2 md:ml-4 md:mr-6 items-end self-center md:self-start"> <Col className="text-4xl mt-8 md:mt-0 md:ml-4 md:mr-6 items-end self-center md:self-start">
<div className="text-xl text-gray-500">Resolved</div> <div className="text-xl text-gray-500">Resolved</div>
<div className={resolutionColor}>{resolution}</div> <div className={resolutionColor}>
{resolution === 'CANCEL' ? 'N/A' : resolution}
</div>
</Col> </Col>
) : ( ) : (
<Col className="text-4xl mt-4 md:mt-2 md:ml-4 md:mr-6 text-primary items-end self-center md:self-start"> <Col className="text-4xl mt-8 md:mt-0 md:ml-4 md:mr-6 text-primary items-end self-center md:self-start">
{probPercent} {probPercent}
<div className="text-xl">chance</div> <div className="text-xl">chance</div>
</Col> </Col>

View File

@ -13,6 +13,7 @@ import {
import { formatMoney } from '../lib/util/format' import { formatMoney } from '../lib/util/format'
import { User } from '../lib/firebase/users' import { User } from '../lib/firebase/users'
import { UserLink } from './user-page' import { UserLink } from './user-page'
import { Linkify } from './linkify'
export function ContractDetails(props: { contract: Contract }) { export function ContractDetails(props: { contract: Contract }) {
const { contract } = props const { contract } = props
@ -57,9 +58,9 @@ function ContractCard(props: { contract: Contract }) {
<li className="col-span-1 bg-white hover:bg-gray-100 shadow-xl rounded-lg divide-y divide-gray-200"> <li className="col-span-1 bg-white hover:bg-gray-100 shadow-xl rounded-lg divide-y divide-gray-200">
<div className="card"> <div className="card">
<div className="card-body p-6"> <div className="card-body p-6">
<Row className="justify-between gap-2 mb-2"> <Row className="justify-between gap-4 mb-2">
<p className="font-medium text-indigo-700"> <p className="font-medium text-indigo-700">
{contract.question} <Linkify text={contract.question} />
</p> </p>
<div className={clsx('text-4xl', resolutionColor)}> <div className={clsx('text-4xl', resolutionColor)}>
{resolutionText || ( {resolutionText || (
@ -88,7 +89,7 @@ function ContractsGrid(props: { contracts: Contract[] }) {
if (contracts.length === 0) { if (contracts.length === 0) {
return ( return (
<p> <p className="mx-4">
No markets found. Would you like to{' '} No markets found. Would you like to{' '}
<Link href="/create"> <Link href="/create">
<a className="text-green-500 hover:underline hover:decoration-2"> <a className="text-green-500 hover:underline hover:decoration-2">
@ -105,7 +106,6 @@ function ContractsGrid(props: { contracts: Contract[] }) {
{contracts.map((contract) => ( {contracts.map((contract) => (
<ContractCard contract={contract} key={contract.id} /> <ContractCard contract={contract} key={contract.id} />
))} ))}
{/* TODO: Show placeholder if empty */}
</ul> </ul>
) )
} }
@ -173,7 +173,7 @@ export function SearchableGrid(props: {
export function ContractsList(props: { creator: User }) { export function ContractsList(props: { creator: User }) {
const { creator } = props const { creator } = props
const [contracts, setContracts] = useState<Contract[]>([]) const [contracts, setContracts] = useState<Contract[] | 'loading'>('loading')
useEffect(() => { useEffect(() => {
if (creator?.id) { if (creator?.id) {
@ -182,5 +182,7 @@ export function ContractsList(props: { creator: User }) {
} }
}, [creator]) }, [creator])
if (contracts === 'loading') return <></>
return <SearchableGrid contracts={contracts} defaultSort="all" /> return <SearchableGrid contracts={contracts} defaultSort="all" />
} }

View File

@ -1,147 +0,0 @@
import clsx from 'clsx'
import Link from 'next/link'
import Image from 'next/image'
import { useUser } from '../hooks/use-user'
import { formatMoney } from '../lib/util/format'
import { Row } from './layout/row'
import { firebaseLogin, User } from '../lib/firebase/users'
import { MenuButton } from './menu'
const hoverClasses =
'hover:underline hover:decoration-indigo-400 hover:decoration-2'
const mobileNavigation = [
{
name: 'Home',
href: '/',
},
{
name: 'Account',
href: '/account',
},
{
name: 'Your bets',
href: '/bets',
},
{
name: 'Create a market',
href: '/create',
},
]
function ProfileSummary(props: { user: User }) {
const { user } = props
return (
<Row className="avatar items-center">
<div className="rounded-full w-10 h-10 mr-4">
<Image src={user.avatarUrl} width={40} height={40} />
</div>
<div className="truncate" style={{ maxWidth: 175 }}>
{user.name}
<div className="text-gray-700 text-sm">{formatMoney(user.balance)}</div>
</div>
</Row>
)
}
function SignedInHeaders(props: { user: User; themeClasses?: string }) {
const { user, themeClasses } = props
return (
<>
<Link href="/create">
<a
className={clsx(
'text-base font-medium hidden md:block',
themeClasses
)}
>
Create a market
</a>
</Link>
<Link href="/bets">
<a
className={clsx(
'text-base font-medium hidden md:block',
themeClasses
)}
>
Your bets
</a>
</Link>
<Link href="/account">
<a className={clsx('text-base font-medium hidden md:block')}>
<ProfileSummary user={user} />
</a>
</Link>
<MenuButton
className="md:hidden"
menuItems={mobileNavigation}
buttonContent={<ProfileSummary user={user} />}
/>
</>
)
}
function SignedOutHeaders(props: { themeClasses?: string }) {
const { themeClasses } = props
return (
<>
<div
className={clsx('text-base font-medium cursor-pointer', themeClasses)}
onClick={firebaseLogin}
>
Sign in
</div>
</>
)
}
export function Header(props: { darkBackground?: boolean; children?: any }) {
const { darkBackground, children } = props
const user = useUser()
const themeClasses = clsx(darkBackground && 'text-white', hoverClasses)
return (
<nav
className="max-w-7xl w-full flex flex-row justify-between md:justify-start mx-auto pt-5 px-4 sm:px-6"
aria-label="Global"
>
<Link href="/">
<a className="flex flex-row gap-3">
<img
className="sm:h-10 sm:w-10 hover:rotate-12 transition-all"
src="/logo-icon.svg"
width={40}
height={40}
/>
<div
className={clsx(
'font-major-mono lowercase mt-1 sm:text-2xl',
darkBackground && 'text-white'
)}
>
Mantic Markets
</div>
</a>
</Link>
<Row className="gap-8 mt-1 md:ml-16">
{children}
{user ? (
<SignedInHeaders user={user} themeClasses={themeClasses} />
) : (
<SignedOutHeaders themeClasses={themeClasses} />
)}
</Row>
</nav>
)
}

View File

@ -0,0 +1,42 @@
import Link from 'next/link'
import { Fragment } from 'react'
// Return a JSX span, linkifying @username, #hashtags, and https://...
export function Linkify(props: { text: string }) {
const { text } = props
const regex = /(?:^|\s)(?:[@#][a-z0-9_]+|https?:\/\/\S+)/gi
const matches = text.match(regex) || []
const links = matches.map((match) => {
// Matches are in the form: " @username" or "https://example.com"
const whitespace = match.match(/^\s/)
const symbol = match.trim().substring(0, 1)
const tag = match.trim().substring(1)
const href =
{
'@': `/${tag}`,
'#': `/tag/${tag}`,
}[symbol] ?? match
return (
<>
{whitespace}
<Link href={href}>
<a className="text-indigo-700 hover:underline hover:decoration-2">
{symbol}
{tag}
</a>
</Link>
</>
)
})
return (
<span>
{text.split(regex).map((part, i) => (
<Fragment key={i}>
{part}
{links[i]}
</Fragment>
))}
</span>
)
}

View File

@ -0,0 +1,27 @@
import Link from 'next/link'
import clsx from 'clsx'
export function ManticLogo(props: { darkBackground?: boolean }) {
const { darkBackground } = props
return (
<Link href="/">
<a className="flex flex-row gap-3">
<img
className="sm:h-10 sm:w-10 hover:rotate-12 transition-all"
src="/logo-icon.svg"
width={40}
height={40}
/>
<div
className={clsx(
'font-major-mono lowercase mt-1 sm:text-2xl md:whitespace-nowrap',
darkBackground && 'text-white'
)}
style={{ fontFamily: 'Major Mono Display,monospace' }}
>
Mantic Markets
</div>
</a>
</Link>
)
}

View File

@ -4,7 +4,7 @@ import clsx from 'clsx'
export function MenuButton(props: { export function MenuButton(props: {
buttonContent: any buttonContent: any
menuItems: { name: string; href: string }[] menuItems: { name: string; href: string; onClick?: () => void }[]
className?: string className?: string
}) { }) {
const { buttonContent, menuItems, className } = props const { buttonContent, menuItems, className } = props
@ -28,12 +28,13 @@ export function MenuButton(props: {
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 py-1 focus:outline-none"> <Menu.Items className="origin-top-right absolute right-0 mt-2 w-40 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 py-1 focus:outline-none">
{menuItems.map((item) => ( {menuItems.map((item) => (
<Menu.Item key={item.name}> <Menu.Item key={item.name}>
{({ active }) => ( {({ active }) => (
<a <a
href={item.href} href={item.href}
onClick={item.onClick}
className={clsx( className={clsx(
active ? 'bg-gray-100' : '', active ? 'bg-gray-100' : '',
'block py-2 px-4 text-sm text-gray-700' 'block py-2 px-4 text-sm text-gray-700'

103
web/components/nav-bar.tsx Normal file
View File

@ -0,0 +1,103 @@
import clsx from 'clsx'
import Link from 'next/link'
import { useUser } from '../hooks/use-user'
import { Row } from './layout/row'
import { firebaseLogin, User } from '../lib/firebase/users'
import { ManticLogo } from './mantic-logo'
import { ProfileMenu } from './profile-menu'
export function NavBar(props: {
darkBackground?: boolean
wide?: boolean
className?: string
children?: any
}) {
const { darkBackground, wide, className, children } = props
const user = useUser()
const hoverClasses =
'hover:underline hover:decoration-indigo-400 hover:decoration-2'
const themeClasses = clsx(darkBackground && 'text-white', hoverClasses)
return (
<nav
className={clsx(
'w-full p-4 mb-4 shadow-sm',
!darkBackground && 'bg-white',
className
)}
aria-label="Global"
>
<Row
className={clsx(
'justify-between items-center mx-auto sm:px-4',
wide ? 'max-w-7xl' : 'max-w-4xl'
)}
>
<ManticLogo darkBackground={darkBackground} />
<Row className="items-center gap-6 sm:gap-8 md:ml-16 lg:ml-40">
{children}
{user ? (
<SignedInHeaders user={user} themeClasses={themeClasses} />
) : (
<SignedOutHeaders themeClasses={themeClasses} />
)}
</Row>
</Row>
</nav>
)
}
function SignedInHeaders(props: { user: User; themeClasses?: string }) {
const { user, themeClasses } = props
return (
<>
<Link href="/about">
<a
className={clsx(
'text-base hidden md:block whitespace-nowrap',
themeClasses
)}
>
About
</a>
</Link>
<Link href="/create">
<a
className={clsx(
'text-base hidden md:block whitespace-nowrap',
themeClasses
)}
>
Create a market
</a>
</Link>
<ProfileMenu user={user} />
</>
)
}
function SignedOutHeaders(props: { themeClasses?: string }) {
const { themeClasses } = props
return (
<>
<div
className={clsx(
'text-base font-medium cursor-pointer whitespace-nowrap',
themeClasses
)}
onClick={firebaseLogin}
>
Sign in
</div>
</>
)
}

20
web/components/page.tsx Normal file
View File

@ -0,0 +1,20 @@
import clsx from 'clsx'
import { NavBar } from './nav-bar'
export function Page(props: { wide?: boolean; children?: any }) {
const { wide, children } = props
return (
<div>
<NavBar wide={wide} />
<div
className={clsx(
'w-full px-4 pb-8 mx-auto',
wide ? 'max-w-7xl' : 'max-w-4xl'
)}
>
{children}
</div>
</div>
)
}

View File

@ -0,0 +1,73 @@
import Image from 'next/image'
import { firebaseLogout, User } from '../lib/firebase/users'
import { formatMoney } from '../lib/util/format'
import { Row } from './layout/row'
import { MenuButton } from './menu'
export function ProfileMenu(props: { user: User }) {
const { user } = props
return (
<>
<MenuButton
className="hidden md:block"
menuItems={getNavigationOptions(user, { mobile: false })}
buttonContent={<ProfileSummary user={user} />}
/>
<MenuButton
className="md:hidden"
menuItems={getNavigationOptions(user, { mobile: true })}
buttonContent={<ProfileSummary user={user} />}
/>
</>
)
}
function getNavigationOptions(user: User, options: { mobile: boolean }) {
const { mobile } = options
return [
{
name: 'Home',
href: '/',
},
...(mobile
? [
{ name: 'About', href: '/about' },
{
name: 'Create a market',
href: '/create',
},
]
: []),
{
name: 'Your bets',
href: '/bets',
},
{
name: 'Your markets',
href: `/${user.username}`,
},
{
name: 'Sign out',
href: '#',
onClick: () => firebaseLogout(),
},
]
}
function ProfileSummary(props: { user: User }) {
const { user } = props
return (
<Row className="avatar items-center">
<div className="rounded-full w-10 h-10 mr-4">
<Image src={user.avatarUrl} width={40} height={40} />
</div>
<div className="truncate" style={{ maxWidth: 175 }}>
{user.name}
<div className="text-gray-700 text-sm">{formatMoney(user.balance)}</div>
</div>
</Row>
)
}

View File

@ -28,8 +28,10 @@ export function ResolutionPanel(props: {
const resolve = async () => { const resolve = async () => {
setIsSubmitting(true) setIsSubmitting(true)
const result = await resolveMarket({ outcome, contractId: contract.id }) const result = await resolveMarket({
.then(r => r.data as any) outcome,
contractId: contract.id,
}).then((r) => r.data as any)
console.log('resolved', outcome, 'result:', result) console.log('resolved', outcome, 'result:', result)
@ -43,17 +45,14 @@ export function ResolutionPanel(props: {
outcome === 'YES' outcome === 'YES'
? 'btn-primary' ? 'btn-primary'
: outcome === 'NO' : outcome === 'NO'
? 'bg-red-400 hover:bg-red-500' ? 'bg-red-400 hover:bg-red-500'
: outcome === 'CANCEL' : outcome === 'CANCEL'
? 'bg-yellow-400 hover:bg-yellow-500' ? 'bg-yellow-400 hover:bg-yellow-500'
: 'btn-disabled' : 'btn-disabled'
return ( return (
<Col <Col
className={clsx( className={clsx('bg-gray-100 shadow-xl px-8 py-6 rounded-md', className)}
'bg-gray-100 shadow-xl px-8 py-6 rounded-md w-full md:w-auto',
className
)}
> >
<Title className="mt-0" text="Your market" /> <Title className="mt-0" text="Your market" />
@ -68,7 +67,6 @@ export function ResolutionPanel(props: {
<Spacer h={3} /> <Spacer h={3} />
<div> <div>
{outcome === 'YES' ? ( {outcome === 'YES' ? (
<> <>
@ -87,12 +85,9 @@ export function ResolutionPanel(props: {
)} )}
</div> </div>
<Spacer h={3} /> <Spacer h={3} />
{!!error && {!!error && <div className="text-red-500">{error}</div>}
<div className='text-red-500'>{error}</div>
}
<ConfirmationButton <ConfirmationButton
id="resolution-modal" id="resolution-modal"

View File

@ -5,7 +5,7 @@ export function Title(props: { text: string; className?: string }) {
return ( return (
<h1 <h1
className={clsx( className={clsx(
'text-3xl font-major-mono text-indigo-700 inline-block mt-6 mb-4', 'text-3xl font-major-mono text-indigo-700 inline-block my-6',
className className
)} )}
> >

View File

@ -1,5 +1,4 @@
import { firebaseLogout, User } from '../lib/firebase/users' import { firebaseLogout, User } from '../lib/firebase/users'
import { Header } from './header'
import { ContractsList } from './contracts-list' import { ContractsList } from './contracts-list'
import { Title } from './title' import { Title } from './title'
import { Row } from './layout/row' import { Row } from './layout/row'
@ -7,6 +6,7 @@ import { formatMoney } from '../lib/util/format'
import Link from 'next/link' import Link from 'next/link'
import clsx from 'clsx' import clsx from 'clsx'
import { SEO } from './SEO' import { SEO } from './SEO'
import { Page } from './page'
export function UserLink(props: { username: string; className?: string }) { export function UserLink(props: { username: string; className?: string }) {
const { username, className } = props const { username, className } = props
@ -29,7 +29,7 @@ export function UserLink(props: { username: string; className?: string }) {
function UserCard(props: { user: User; showPrivateInfo?: boolean }) { function UserCard(props: { user: User; showPrivateInfo?: boolean }) {
const { user, showPrivateInfo } = props const { user, showPrivateInfo } = props
return ( return (
<Row className="card glass lg:card-side shadow-xl hover:shadow-xl text-neutral-content bg-green-600 hover:bg-green-600 transition-all max-w-sm mx-auto my-12"> <Row className="card glass lg:card-side shadow-xl hover:shadow-xl text-neutral-content bg-green-600 hover:bg-green-600 transition-all max-w-sm my-12 mx-auto">
<div className="p-4"> <div className="p-4">
{user?.avatarUrl && ( {user?.avatarUrl && (
<img <img
@ -70,24 +70,18 @@ export function UserPage(props: { user: User; currentUser?: User }) {
const possesive = isCurrentUser ? 'Your ' : `${user.username}'s ` const possesive = isCurrentUser ? 'Your ' : `${user.username}'s `
return ( return (
<div> <Page>
<SEO <SEO
title={possesive + 'markets'} title={possesive + 'markets'}
description={possesive + 'markets'} description={possesive + 'markets'}
url={`/@${user.username}`} url={`/@${user.username}`}
/> />
<Header /> {/* <UserCard user={user} showPrivateInfo={isCurrentUser} /> */}
<div className="max-w-4xl pt-8 pb-0 sm:pb-8 mx-auto"> <Title text={possesive + 'markets'} />
<div>
<UserCard user={user} showPrivateInfo={isCurrentUser} />
<Title text={possesive + 'markets'} /> <ContractsList creator={user} />
</Page>
<ContractsList creator={user} />
</div>
</div>
</div>
) )
} }

View File

@ -61,7 +61,7 @@ export function YesNoCancelSelector(props: {
onClick={() => onSelect('CANCEL')} onClick={() => onSelect('CANCEL')}
className={btnClassName} className={btnClassName}
> >
CANCEL N/A
</Button> </Button>
</Row> </Row>
) )

15259
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,8 @@
"name": "mantic", "name": "mantic",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "concurrently -n NEXT,TS -c magenta,cyan \"next dev -p 3000\" \"yarn ts --watch\"",
"ts": "tsc --noEmit --incremental --preserveWatchOutput --pretty",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
@ -10,8 +11,8 @@
"prepare": "cd .. && husky install web/.husky" "prepare": "cd .. && husky install web/.husky"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.4.2", "@headlessui/react": "1.4.2",
"@heroicons/react": "^1.0.5", "@heroicons/react": "1.0.5",
"@nivo/core": "0.74.0", "@nivo/core": "0.74.0",
"@nivo/line": "0.74.0", "@nivo/line": "0.74.0",
"clsx": "1.1.1", "clsx": "1.1.1",
@ -25,19 +26,20 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "0.4.0", "@tailwindcss/forms": "0.4.0",
"@types/lodash": "^4.14.178", "@types/lodash": "4.14.178",
"@types/node": "16.11.11", "@types/node": "16.11.11",
"@types/react": "17.0.37", "@types/react": "17.0.37",
"autoprefixer": "10.2.6", "autoprefixer": "10.2.6",
"concurrently": "6.5.1",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-next": "12.0.4", "eslint-config-next": "12.0.4",
"husky": "^7.0.4", "husky": "7.0.4",
"lint-staged": "^12.1.3", "lint-staged": "12.1.3",
"postcss": "8.3.5", "postcss": "8.3.5",
"prettier": "2.5.0", "prettier": "2.5.0",
"pretty-quick": "^3.1.2", "pretty-quick": "3.1.2",
"tailwindcss": "3.0.1", "tailwindcss": "3.0.1",
"tsc-files": "^1.1.3", "tsc-files": "1.1.3",
"typescript": "4.5.2" "typescript": "4.5.2"
}, },
"lint-staged": { "lint-staged": {

View File

@ -1,8 +1,6 @@
import React from 'react' import React from 'react'
import clsx from 'clsx'
import { useContractWithPreload } from '../../hooks/use-contract' import { useContractWithPreload } from '../../hooks/use-contract'
import { Header } from '../../components/header'
import { ContractOverview } from '../../components/contract-overview' import { ContractOverview } from '../../components/contract-overview'
import { BetPanel } from '../../components/bet-panel' import { BetPanel } from '../../components/bet-panel'
import { Col } from '../../components/layout/col' import { Col } from '../../components/layout/col'
@ -15,15 +13,16 @@ import { Spacer } from '../../components/layout/spacer'
import { User } from '../../lib/firebase/users' import { User } from '../../lib/firebase/users'
import { Contract, getContractFromSlug } from '../../lib/firebase/contracts' import { Contract, getContractFromSlug } from '../../lib/firebase/contracts'
import { SEO } from '../../components/SEO' import { SEO } from '../../components/SEO'
import { Page } from '../../components/page'
export async function getStaticProps(props: { params: any }) { export async function getStaticProps(props: { params: any }) {
const { username, slug } = props.params const { username, contractSlug } = props.params
const contract = (await getContractFromSlug(slug)) || null const contract = (await getContractFromSlug(contractSlug)) || null
return { return {
props: { props: {
username, username,
slug, slug: contractSlug,
contract, contract,
}, },
@ -52,31 +51,24 @@ export default function ContractPage(props: {
const isCreator = user?.id === creatorId const isCreator = user?.id === creatorId
return ( return (
<Col className="max-w-7xl mx-auto sm:px-6 lg:px-8"> <Page wide={!isResolved}>
<SEO <SEO
title={contract.question} title={contract.question}
description={contract.description} description={contract.description}
url={`/${props.username}/${props.slug}`} url={`/${props.username}/${props.slug}`}
/> />
<Header /> <Col className="w-full md:flex-row justify-between mt-6">
<div className="flex-[3]">
<Col <ContractOverview contract={contract} />
className={clsx(
'w-full items-start md:flex-row mt-4',
isResolved ? 'md:justify-center' : 'md:justify-between'
)}
>
<div className="max-w-4xl w-full ">
<ContractOverview contract={contract} className="p-4" />
<BetsSection contract={contract} user={user ?? null} /> <BetsSection contract={contract} user={user ?? null} />
</div> </div>
{!isResolved && ( {!isResolved && (
<> <>
<div className="mt-12 md:mt-0 md:ml-8" /> <div className="md:ml-8" />
<Col className="w-full sm:w-auto"> <Col className="flex-1">
<BetPanel contract={contract} /> <BetPanel contract={contract} />
{isCreator && user && ( {isCreator && user && (
@ -86,7 +78,7 @@ export default function ContractPage(props: {
</> </>
)} )}
</Col> </Col>
</Col> </Page>
) )
} }
@ -104,12 +96,12 @@ function BetsSection(props: { contract: Contract; user: User | null }) {
if (!userBets || userBets.length === 0) return <></> if (!userBets || userBets.length === 0) return <></>
return ( return (
<div className="p-4"> <div>
<Title text="Your bets" /> <Title text="Your bets" />
<MyBetsSummary contract={contract} bets={userBets} /> <MyBetsSummary contract={contract} bets={userBets} />
<Spacer h={6} /> <Spacer h={6} />
<ContractBetsTable contract={contract} bets={userBets} /> <ContractBetsTable contract={contract} bets={userBets} />
<Spacer h={6} /> <Spacer h={12} />
</div> </div>
) )
} }

View File

@ -0,0 +1,64 @@
.div {
/* Tailwind Gray 700 */
color: #334155;
font-size: 1.1em;
}
.h1,
.h2,
.h3 {
color: #4338ca;
margin-top: 2rem;
margin-bottom: 1rem;
font-family: 'Readex Pro', sans-serif;
}
.h1 {
font-size: 2rem;
}
.h2 {
font-size: 2rem;
}
.h3 {
font-size: 1.5rem;
}
.p {
margin-bottom: 0.75rem;
}
.a {
/* Tailwind Indigo 700 */
color: #4338ca;
}
.a:hover {
text-decoration: solid underline #4338ca 2px;
}
.aside {
border-radius: 0.5rem;
/* Tailwind Slate 200 */
background-color: #e2e8f0;
padding: 1.5rem;
}
.hr {
margin-top: -0.5rem;
margin-bottom: 1rem;
}
.ul {
list-style-type: disc;
}
.ol {
list-style-type: decimal;
}
.li {
margin-left: 2rem;
margin-bottom: 0.5rem;
}

311
web/pages/about.tsx Normal file
View File

@ -0,0 +1,311 @@
import { cloneElement } from 'react'
import { Page } from '../components/page'
import { SEO } from '../components/SEO'
import styles from './about.module.css'
export default function About() {
return (
<Page>
<SEO title="About" description="About" url="/about" />
<Contents />
</Page>
)
}
// Return a copy of the JSX node tree, with the style applied
const cloneWithStyle = (node: JSX.Element) => {
// Base case: Node is a string
if (!node.type) return node
// Find the appropriate style from the module.css
const className = styles[node.type]
// Recursively call this function on each child
let children = node.props.children
if (children?.map) {
// Multiple child elements
children = children.map(cloneWithStyle)
} else if (children) {
// Single child element
children = cloneWithStyle(children)
}
// Note: This probably strips out any existing classNames
return cloneElement(node, { className, children })
}
// Copied from https://www.notion.so/mantic/About-Mantic-Markets-7c44bc161356474cad54cba2d2973fe2
// And then run through https://markdowntohtml.com/
function Contents() {
return cloneWithStyle(
<div>
<h1 id="about">About</h1>
<hr />
<p>
Mantic Markets is creating better forecasting through user-created
prediction markets.
</p>
<p>
Our mission is to expand humanity&#39;s collective knowledge by making
prediction markets accessible to all.
</p>
<h1 id="faq">FAQ</h1>
<hr />
<h3 id="what-are-prediction-markets-">What are prediction markets?</h3>
<p>
<strong>
Prediction markets are a place where you can bet on the outcome of
future events.
</strong>
</p>
<p>
Consider a question like: &quot;Will Democrats win the 2024 US
presidential election?&quot;
</p>
<p>
If I think the Democrats are very likely to win, and you disagree, I
might offer $70 to your $30 (with the winner taking home $100 total).
This set of bets imply a 70% probability of the Democrats winning.
</p>
<p>
Now, you or I could be mistaken and overshooting the true probability
one way or another. If so, there&#39;s an incentive for someone else to
bet and correct it! Over time, the implied probability will converge to
the{' '}
<a href="https://en.wikipedia.org/wiki/Efficient-market_hypothesis">
market&#39;s best estimate
</a>
. This is the power of prediction markets!
</p>
<h3 id="how-does-mantic-markets-work-">How does Mantic Markets work?</h3>
<ol>
<li>
<strong>
Anyone can create a market for any yes-or-no question.
</strong>
</li>
</ol>
<p>
You can ask questions about the future like &quot;Will Taiwan remove its
14-day COVID quarantine by Jun 01, 2022?&quot; Then use the information
to plan your trip.
</p>
<p>
You can also ask subjective, personal questions like &quot;Will I enjoy
my 2022 Taiwan trip?&quot;. Then share the market with your family and
friends.
</p>
<ol>
<li>
<strong>
Anyone can bet on a market using Mantic Dollars (M$), our platform
currency.
</strong>
</li>
</ol>
<p>
You get M$ 100 just for signing up, so you can start betting
immediately! When a market creator decides an outcome in your favor,
you&#39;ll win money from people who bet against you.
</p>
<p>
If you run out of money, you can purchase more at a rate of $1 USD to M$
100. (Note that Mantic Dollars are not convertible to cash and can only
be used within our platform.)
</p>
<aside>
💡 We&#39;re still in Open Beta; we&#39;ll tweak this model and
periodically reset balances before our official launch. If you purchase
any M$ during the beta, we promise to honor that when we launch!
</aside>
<h3 id="why-do-i-want-to-bet-with-play-money-">
Why do I want to bet with play-money?
</h3>
<p>
Prediction markets work best when bettors have skin in the game. By
restricting the supply of our currency, you know that the other bettors
have thought carefully about where to spend their M$, and that the
market prices line up with reality.
</p>
<p>By buying M$, you support:</p>
<ul>
<li>The continued development of Mantic Markets</li>
<li>Cash payouts to market creators (TBD)</li>
<li>Forecasting tournaments for bettors (TBD)</li>
</ul>
<p>
We also have some thoughts on how to reward bettors: physical swag,
exclusive conversations with market creators, NFTs...? If you have
ideas, let us know!
</p>
<h3 id="can-prediction-markets-work-without-real-money-">
Can prediction markets work without real money?
</h3>
<p>
Yes! There is substantial evidence that play-money prediction markets
provide real predictive power. Examples include{' '}
<a href="http://www.electronicmarkets.org/fileadmin/user_upload/doc/Issues/Volume_16/Issue_01/V16I1_Statistical_Tests_of_Real-Money_versus_Play-Money_Prediction_Markets.pdf">
sports betting
</a>{' '}
and internal prediction markets at firms like{' '}
<a href="https://www.networkworld.com/article/2284098/google-bets-on-value-of-prediction-markets.html">
Google
</a>
.
</p>
<p>
Our overall design also ensures that good forecasting will come out on
top in the long term. In the competitive environment of the marketplace,
bettors that are correct more often will gain influence, leading to
better-calibrated forecasts over time.
</p>
<h3 id="how-are-markets-resolved-">How are markets resolved?</h3>
<p>
The creator of the prediction market decides the outcome and earns 0.5%
of the trade volume for their effort.
</p>
<p>
This simple resolution mechanism has surprising benefits in allowing a
diversity of views to flourish. Competition between market creators will
lead to traders flocking to the creators with good judgment on market
resolution.
</p>
<p>
What&#39;s more, when the creator is free to use their judgment, many
new kinds of prediction markets can be created that are less objective
or even personal. (E.g. &quot;Will I enjoy participating in the
Metaverse in 2023?&quot;)
</p>
<h3 id="why-is-this-important-">Why is this important?</h3>
<p>
Prediction markets aggregate and reveal crucial information that would
not otherwise be known. They are a bottom-up mechanism that can
influence everything from politics, economics, and business, to
scientific research and education.
</p>
<p>
Prediction markets can predict{' '}
<a href="https://www.pnas.org/content/112/50/15343">
which research papers will replicate
</a>
; which drug is the most effective; which policy would generate the most
tax revenue; which charity will be underfunded; or, which startup idea
is the most promising.
</p>
<p>
By surfacing and quantifying our collective knowledge, we as a society
become wiser.
</p>
<h3 id="how-is-this-different-from-metaculus-or-hypermind-">
How is this different from Metaculus or Hypermind?
</h3>
<p>
We believe that in order to get the best results, you have to have skin
in the game. We require that people use real money to buy the currency
they use on our platform.
</p>
<p>
With Mantic Dollars being a scarce resource, people will bet more
carefully and can&#39;t rig the outcome by creating multiple accounts.
The result is more accurate predictions.
</p>
<p>
Mantic Markets is also focused on accessibility and allowing anyone to
quickly create and judge a prediction market. When we all have the power
to create and share prediction markets in seconds and apply our own
judgment on the outcome, it leads to a qualitative shift in the number,
variety, and usefulness of prediction markets.
</p>
<h3 id="how-does-betting-in-a-market-work-on-a-technical-level-">
How does betting in a market work on a technical level?
</h3>
<p>
Mantic Markets uses a special type of automated market marker based on a
dynamic pari-mutuel (DPM) betting system.
</p>
<p>
Like traditional pari-mutuel systems, your payoff is not known at the
time you place your bet (it&#39;s dependent on the size of the pot when
the event ends).
</p>
<p>
Unlike traditional pari-mutuel systems, the price or probability that
you buy in at changes continuously to ensure that you&#39;re always
getting fair odds.
</p>
<p>
The result is a market that can function well when trading volume is low
without any risk to the market creator.
</p>
<h3 id="who-are-we-">Who are we?</h3>
<p>Mantic Markets is currently a team of three:</p>
<ul>
<li>James Grugett</li>
<li>Stephen Grugett</li>
<li>Austin Chen</li>
</ul>
<p>
We&#39;ve previously launched consumer-facing startups (
<a href="https://throne.live/">Throne</a>,{' '}
<a href="http://oneword.games/platform">One Word</a>), and worked at top
tech and finance companies (Google, Susquehanna).
</p>
<h1 id="talk-to-us-">Talk to us!</h1>
<hr />
<p>
Questions? Comments? Want to create a market? Talk to us unlike
praying mantises, we dont bite!
</p>
<ul>
<li>
Email: <code>info@mantic.markets</code>
</li>
<li>
Office hours:{' '}
<a href="https://calendly.com/austinchen/mantic">Calendly</a>
</li>
<li>Discord:</li>
</ul>
<p>
<a href="https://discord.gg/eHQBNBqXuh">
Join the Mantic Markets Discord Server!
</a>
</p>
<h1 id="further-reading">Further Reading</h1>
<hr />
<ul>
<li>
<a href="https://en.wikipedia.org/wiki/Prediction_market">
Wikipedia: Prediction markets
</a>
</li>
<li>
<a href="https://www.gwern.net/Prediction-markets">
Gwern: Prediction markets
</a>
</li>
<li>
<a href="https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.91.7441&amp;rep=rep1&amp;type=pdf">
David Pennock: Dynamic parimutuel markets
</a>
</li>
<li>
<a href="https://sideways-view.com/2019/10/27/prediction-markets-for-internet-points/">
Paul Christiano: Prediction markets for internet points
</a>
</li>
<li>
<a href="https://mantic.markets/simulator">
Dynamic parimutuel market simulator
</a>
</li>
<li>
<a href="https://thezvi.wordpress.com/2021/12/02/covid-prediction-markets-at-polymarket/">
Zvi Mowshowitz on resolving prediction markets
</a>
</li>
</ul>
</div>
)
}

View File

@ -1,11 +1,12 @@
import React from 'react' import React from 'react'
import { Page } from '../components/page'
import { UserPage } from '../components/user-page' import { UserPage } from '../components/user-page'
import { useUser } from '../hooks/use-user' import { useUser } from '../hooks/use-user'
import { firebaseLogin } from '../lib/firebase/users' import { firebaseLogin } from '../lib/firebase/users'
function SignInCard() { function SignInCard() {
return ( return (
<div className="card glass lg:card-side shadow-xl hover:shadow-xl text-neutral-content bg-green-600 hover:bg-green-600 transition-all max-w-sm mx-auto my-12"> <div className="card glass sm:card-side shadow-xl hover:shadow-xl text-neutral-content bg-green-600 hover:bg-green-600 transition-all max-w-sm mx-4 sm:mx-auto my-12">
<div className="p-4"> <div className="p-4">
<img <img
src="/logo-icon-white-bg.png" src="/logo-icon-white-bg.png"
@ -30,5 +31,11 @@ function SignInCard() {
export default function Account() { export default function Account() {
const user = useUser() const user = useUser()
return user ? <UserPage user={user} currentUser={user} /> : <SignInCard /> return user ? (
<UserPage user={user} currentUser={user} />
) : (
<Page>
<SignInCard />
</Page>
)
} }

View File

@ -1,5 +1,5 @@
import { BetsList } from '../components/bets-list' import { BetsList } from '../components/bets-list'
import { Header } from '../components/header' 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 { useUser } from '../hooks/use-user' import { useUser } from '../hooks/use-user'
@ -8,17 +8,10 @@ export default function BetsPage() {
const user = useUser() const user = useUser()
return ( return (
<div> <Page>
<SEO title="Your bets" description="Your bets" url="/bets" /> <SEO title="Your bets" description="Your bets" url="/bets" />
<Title text="Your bets" />
<Header /> {user && <BetsList user={user} />}
</Page>
<div className="max-w-4xl pt-8 pb-0 sm:pb-8 mx-auto">
<div>
<Title text="Your bets" />
{user && <BetsList user={user} />}
</div>
</div>
</div>
) )
} }

View File

@ -2,12 +2,12 @@ import router from 'next/router'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { ContractsList } from '../components/contracts-list' import { ContractsList } from '../components/contracts-list'
import { Header } from '../components/header'
import { Spacer } from '../components/layout/spacer' import { Spacer } from '../components/layout/spacer'
import { Title } from '../components/title' import { Title } from '../components/title'
import { useUser } from '../hooks/use-user' import { useUser } from '../hooks/use-user'
import { path } from '../lib/firebase/contracts' import { path } from '../lib/firebase/contracts'
import { createContract } from '../lib/service/create-contract' import { createContract } from '../lib/service/create-contract'
import { Page } from '../components/page'
// Allow user to create a new contract // Allow user to create a new contract
export default function NewContract() { export default function NewContract() {
@ -42,88 +42,84 @@ export default function NewContract() {
if (!creator) return <></> if (!creator) return <></>
return ( return (
<div> <Page>
<Header /> <Title text="Create a new prediction market" />
<div className="max-w-4xl py-12 lg:mx-auto px-4"> <div className="w-full bg-gray-100 rounded-lg shadow-xl px-6 py-4">
<Title text="Create a new prediction market" /> {/* Create a Tailwind form that takes in all the fields needed for a new contract */}
{/* When the form is submitted, create a new contract in the database */}
<form>
<div className="form-control">
<label className="label">
<span className="label-text">Question</span>
</label>
<div className="w-full bg-gray-100 rounded-lg shadow-xl px-6 py-4"> <input
{/* Create a Tailwind form that takes in all the fields needed for a new contract */} type="text"
{/* When the form is submitted, create a new contract in the database */} placeholder="e.g. Will the FDA approve Paxlovid before Jun 2nd, 2022?"
<form> className="input"
<div className="form-control"> value={question}
<label className="label"> onChange={(e) => setQuestion(e.target.value || '')}
<span className="label-text">Question</span> />
</label> </div>
<input <Spacer h={4} />
type="text"
placeholder="e.g. Will the FDA approve Paxlovid before Jun 2nd, 2022?"
className="input"
value={question}
onChange={(e) => setQuestion(e.target.value || '')}
/>
</div>
<Spacer h={4} /> <div className="form-control">
<label className="label">
<span className="label-text">Description (optional)</span>
</label>
<div className="form-control"> <textarea
<label className="label"> className="textarea h-24 textarea-bordered"
<span className="label-text">Description (optional)</span> placeholder={descriptionPlaceholder}
</label> value={description}
onChange={(e) => setDescription(e.target.value || '')}
></textarea>
</div>
<textarea <Spacer h={4} />
className="textarea h-24 textarea-bordered"
placeholder={descriptionPlaceholder}
value={description}
onChange={(e) => setDescription(e.target.value || '')}
></textarea>
</div>
<Spacer h={4} /> <div className="form-control">
<label className="label">
<span className="label-text">
Initial probability: {initialProb}%
</span>
</label>
<div className="form-control"> <input
<label className="label"> type="range"
<span className="label-text"> className="range range-lg range-primary"
Initial probability: {initialProb}% min="1"
</span> max={99}
</label> value={initialProb}
onChange={(e) => setInitialProb(parseInt(e.target.value))}
/>
</div>
<input <Spacer h={4} />
type="range"
className="range range-lg range-primary"
min="1"
max={99}
value={initialProb}
onChange={(e) => setInitialProb(parseInt(e.target.value))}
/>
</div>
<Spacer h={4} /> <div className="flex justify-end my-4">
<button
<div className="flex justify-end my-4"> type="submit"
<button className="btn btn-primary"
type="submit" disabled={isSubmitting || !question}
className="btn btn-primary" onClick={(e) => {
disabled={isSubmitting || !question} e.preventDefault()
onClick={(e) => { submit()
e.preventDefault() }}
submit() >
}} Create market
> </button>
Create market </div>
</button> </form>
</div>
</form>
</div>
<Spacer h={10} />
<Title text="Your markets" />
{creator && <ContractsList creator={creator} />}
</div> </div>
</div>
<Spacer h={10} />
<Title text="Your markets" />
{creator && <ContractsList creator={creator} />}
</Page>
) )
} }

View File

@ -1,15 +1,28 @@
import React from 'react' import React from 'react'
import type { NextPage } from 'next'
import { useUser } from '../hooks/use-user' import { useUser } from '../hooks/use-user'
import Markets from './markets' import Markets from './markets'
import LandingPage from './landing-page' import LandingPage from './landing-page'
import { Contract, listAllContracts } from '../lib/firebase/contracts'
const Home: NextPage = () => { export async function getStaticProps() {
const contracts = await listAllContracts().catch((_) => [])
return {
props: {
contracts,
},
revalidate: 60, // regenerate after a minute
}
}
const Home = (props: { contracts: Contract[] }) => {
const user = useUser() const user = useUser()
if (user === undefined) return <></> if (user === undefined) return <></>
return user ? <Markets /> : <LandingPage />
return user ? <Markets contracts={props.contracts} /> : <LandingPage />
} }
export default Home export default Home

View File

@ -11,7 +11,8 @@ import { firebaseLogin } from '../lib/firebase/users'
import { useContracts } from '../hooks/use-contracts' import { useContracts } from '../hooks/use-contracts'
import { SearchableGrid } from '../components/contracts-list' import { SearchableGrid } from '../components/contracts-list'
import { Col } from '../components/layout/col' import { Col } from '../components/layout/col'
import { Header } from '../components/header' import { NavBar } from '../components/nav-bar'
import Link from 'next/link'
export default function LandingPage() { export default function LandingPage() {
return ( return (
@ -23,25 +24,25 @@ export default function LandingPage() {
) )
} }
function Hero() { const scrollToAbout = () => {
const scrollToAbout = () => { const aboutElem = document.getElementById('about')
const aboutElem = document.getElementById('about') window.scrollTo({ top: aboutElem?.offsetTop, behavior: 'smooth' })
window.scrollTo({ top: aboutElem?.offsetTop, behavior: 'smooth' }) }
}
function Hero() {
return ( return (
<div className="overflow-hidden h-screen bg-world-trading bg-cover bg-gray-900 bg-center lg:bg-left"> <div className="overflow-hidden h-screen bg-world-trading bg-cover bg-gray-900 bg-center lg:bg-left">
<Header darkBackground> <NavBar wide darkBackground>
<div <div
className="text-base font-medium text-white cursor-pointer hover:underline hover:decoration-teal-500 hover:decoration-2" className="text-base font-medium text-white ml-8 cursor-pointer hover:underline hover:decoration-teal-500 hover:decoration-2"
onClick={scrollToAbout} onClick={scrollToAbout}
> >
About About
</div> </div>
</Header> </NavBar>
<main> <main>
<div className="pt-40 sm:pt-16 lg:pt-8 lg:pb-14 lg:overflow-hidden"> <div className="pt-32 sm:pt-8 lg:pt-0 lg:pb-14 lg:overflow-hidden">
<div className="mx-auto max-w-7xl lg:px-8"> <div className="mx-auto max-w-7xl lg:px-8 xl:px-0">
<div className="lg:grid lg:grid-cols-2 lg:gap-8"> <div className="lg:grid lg:grid-cols-2 lg:gap-8">
<div className="mx-auto max-w-md px-8 sm:max-w-2xl sm:text-center lg:px-0 lg:text-left lg:flex lg:items-center"> <div className="mx-auto max-w-md px-8 sm:max-w-2xl sm:text-center lg:px-0 lg:text-left lg:flex lg:items-center">
<div className="lg:py-24"> <div className="lg:py-24">
@ -51,7 +52,7 @@ function Hero() {
prediction markets prediction markets
</div> </div>
</h1> </h1>
<p className="mt-3 text-base text-gray-300 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl"> <p className="mt-3 text-base text-white sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
Better forecasting through accessible prediction markets Better forecasting through accessible prediction markets
<br /> <br />
for you and your community for you and your community
@ -82,9 +83,6 @@ function Hero() {
) )
} }
const notionAboutUrl =
'https://mantic.notion.site/About-Mantic-Markets-7c44bc161356474cad54cba2d2973fe2'
function FeaturesSection() { function FeaturesSection() {
const features = [ const features = [
{ {
@ -115,7 +113,7 @@ function FeaturesSection() {
return ( return (
<div id="about" className="w-full py-16 bg-green-50"> <div id="about" className="w-full py-16 bg-green-50">
<div className="max-w-4xl py-12 mx-auto"> <div className="max-w-4xl py-12 mx-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-6 lg:px-8">
<div className="lg:text-center"> <div className="lg:text-center">
<h2 className="text-base text-teal-600 font-semibold tracking-wide uppercase"> <h2 className="text-base text-teal-600 font-semibold tracking-wide uppercase">
Mantic Markets Mantic Markets
@ -150,13 +148,9 @@ function FeaturesSection() {
</div> </div>
<Col className="mt-20"> <Col className="mt-20">
<a <Link href="/about">
className="btn btn-primary mx-auto" <a className="btn btn-primary mx-auto">Learn more</a>
href={notionAboutUrl} </Link>
target="_blank"
>
Learn more
</a>
</Col> </Col>
</div> </div>
</div> </div>
@ -167,8 +161,8 @@ function ExploreMarketsSection() {
const contracts = useContracts() const contracts = useContracts()
return ( return (
<div className="max-w-4xl py-8 mx-auto"> <div className="max-w-4xl px-4 py-8 mx-auto">
<p className="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-indigo-700 sm:text-4xl"> <p className="my-12 text-3xl leading-8 font-extrabold tracking-tight text-indigo-700 sm:text-4xl">
Explore our markets Explore our markets
</p> </p>
<SearchableGrid contracts={contracts === 'loading' ? [] : contracts} /> <SearchableGrid contracts={contracts === 'loading' ? [] : contracts} />

View File

@ -1,16 +1,30 @@
import { SearchableGrid } from '../components/contracts-list' import { SearchableGrid } from '../components/contracts-list'
import { Header } from '../components/header' import { Page } from '../components/page'
import { useContracts } from '../hooks/use-contracts' import { useContracts } from '../hooks/use-contracts'
import { Contract, listAllContracts } from '../lib/firebase/contracts'
export default function Markets() { export async function getStaticProps() {
const contracts = await listAllContracts().catch((_) => [])
return {
props: {
contracts,
},
revalidate: 60, // regenerate after a minute
}
}
export default function Markets(props: { contracts: Contract[] }) {
const contracts = useContracts() const contracts = useContracts()
return ( return (
<div> <Page>
<Header /> {(props.contracts || contracts !== 'loading') && (
<div className="max-w-4xl py-8 mx-auto"> <SearchableGrid
<SearchableGrid contracts={contracts === 'loading' ? [] : contracts} /> contracts={contracts === 'loading' ? props.contracts : contracts}
</div> />
</div> )}
</Page>
) )
} }

View File

@ -3,7 +3,7 @@ import { DatumValue } from '@nivo/core'
import { ResponsiveLine } from '@nivo/line' import { ResponsiveLine } from '@nivo/line'
import { Entry, makeEntries } from '../lib/simulator/entries' import { Entry, makeEntries } from '../lib/simulator/entries'
import { Header } from '../components/header' import { NavBar } from '../components/nav-bar'
import { Col } from '../components/layout/col' import { Col } from '../components/layout/col'
function TableBody(props: { entries: Entry[] }) { function TableBody(props: { entries: Entry[] }) {
@ -149,9 +149,7 @@ function NewBidTable(props: {
function randomBid() { function randomBid() {
const bidType = Math.random() < 0.5 ? 'YES' : 'NO' const bidType = Math.random() < 0.5 ? 'YES' : 'NO'
const p = bidType === 'YES' const p = bidType === 'YES' ? nextEntry.prob : 1 - nextEntry.prob
? nextEntry.prob
: 1 - nextEntry.prob
const amount = Math.round(p * Math.random() * 300) + 1 const amount = Math.round(p * Math.random() * 300) + 1
const bid = makeBid(bidType, amount) const bid = makeBid(bidType, amount)
@ -256,7 +254,7 @@ export default function Simulator() {
return ( return (
<Col> <Col>
<Header /> <NavBar />
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 w-full mt-8 p-2 mx-auto text-center"> <div className="grid grid-cols-1 xl:grid-cols-2 gap-4 w-full mt-8 p-2 mx-auto text-center">
{/* Left column */} {/* Left column */}
<div> <div>

View File

@ -1,9 +1,8 @@
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { SearchableGrid } from '../../components/contracts-list' import { SearchableGrid } from '../../components/contracts-list'
import { Header } from '../../components/header' import { Page } from '../../components/page'
import { Title } from '../../components/title' import { Title } from '../../components/title'
import { useContracts } from '../../hooks/use-contracts' import { useContracts } from '../../hooks/use-contracts'
import Markets from '../markets'
export default function TagPage() { export default function TagPage() {
const router = useRouter() const router = useRouter()
@ -12,18 +11,17 @@ export default function TagPage() {
let contracts = useContracts() let contracts = useContracts()
if (tag && contracts !== 'loading') { if (tag && contracts !== 'loading') {
contracts = contracts.filter((contract) => contracts = contracts.filter(
contract.description.toLowerCase().includes(`#${tag.toLowerCase()}`) (contract) =>
contract.description.toLowerCase().includes(`#${tag.toLowerCase()}`) ||
contract.question.toLowerCase().includes(`#${tag.toLowerCase()}`)
) )
} }
return ( return (
<div> <Page>
<Header /> <Title text={`#${tag}`} />
<div className="max-w-4xl py-8 mx-auto"> <SearchableGrid contracts={contracts === 'loading' ? [] : contracts} />
<Title text={`#${tag}`} /> </Page>
<SearchableGrid contracts={contracts === 'loading' ? [] : contracts} />
</div>
</div>
) )
} }

View File

@ -8,7 +8,7 @@ module.exports = {
fontFamily: Object.assign( fontFamily: Object.assign(
{ ...defaultTheme.fontFamily }, { ...defaultTheme.fontFamily },
{ {
'major-mono': ['Major Mono Display', 'monospace'], 'major-mono': ['Courier', 'monospace'],
'readex-pro': ['Readex Pro', 'sans-serif'], 'readex-pro': ['Readex Pro', 'sans-serif'],
} }
), ),

4774
web/yarn.lock Normal file

File diff suppressed because it is too large Load Diff