Merge branch 'main' into sell-bets
This commit is contained in:
commit
4c353242b0
|
@ -2,9 +2,9 @@
|
|||
"name": "functions",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"serve": "npm run build && firebase emulators:start --only functions",
|
||||
"shell": "npm run build && firebase functions:shell",
|
||||
"start": "npm run shell",
|
||||
"serve": "yarn build && firebase emulators:start --only functions",
|
||||
"shell": "yarn build && firebase functions:shell",
|
||||
"start": "yarn shell",
|
||||
"deploy": "firebase deploy --only functions",
|
||||
"logs": "firebase functions:log"
|
||||
},
|
||||
|
|
1594
functions/yarn.lock
Normal file
1594
functions/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "mantic",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
1
web/.gitignore
vendored
1
web/.gitignore
vendored
|
@ -2,3 +2,4 @@
|
|||
.next
|
||||
node_modules
|
||||
out
|
||||
tsconfig.tsbuildinfo
|
|
@ -1,5 +1,7 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd web
|
||||
npx lint-staged
|
||||
npx pretty-quick --staged
|
||||
# Disable tsc lint for now, cuz it's been annoying
|
||||
# cd web
|
||||
# npx lint-staged
|
||||
|
|
|
@ -96,12 +96,9 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
|||
|
||||
return (
|
||||
<Col
|
||||
className={clsx(
|
||||
'bg-gray-100 shadow-xl px-8 py-6 rounded-md w-full md:w-auto',
|
||||
className
|
||||
)}
|
||||
className={clsx('bg-gray-100 shadow-xl px-8 py-6 rounded-md', 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>
|
||||
<YesNoSelector
|
||||
|
|
|
@ -108,14 +108,14 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
|
|||
<div
|
||||
tabIndex={0}
|
||||
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'
|
||||
)}
|
||||
onClick={() => setCollapsed((collapsed) => !collapsed)}
|
||||
>
|
||||
<Row className="flex-wrap gap-4">
|
||||
<Col className="flex-[2] gap-1">
|
||||
<div>
|
||||
<Row className="mr-6">
|
||||
<Link href={path(contract)}>
|
||||
<a
|
||||
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}
|
||||
</a>
|
||||
</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">
|
||||
<div>
|
||||
|
@ -141,22 +147,17 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
|
|||
</Row>
|
||||
</Col>
|
||||
|
||||
<Row className="flex-nowrap">
|
||||
<MyBetsSummary
|
||||
className="flex-1 justify-end"
|
||||
className="flex-1 justify-end mr-5 sm:mr-8"
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
{/* 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>
|
||||
|
||||
<div className="collapse-content" style={{ backgroundColor: 'white' }}>
|
||||
className="collapse-content !px-0"
|
||||
style={{ backgroundColor: 'white' }}
|
||||
>
|
||||
<Spacer h={8} />
|
||||
|
||||
<ContractBetsTable contract={contract} bets={bets} />
|
||||
|
@ -187,7 +188,7 @@ export function MyBetsSummary(props: {
|
|||
)
|
||||
|
||||
return (
|
||||
<Row className={clsx('gap-6', className)}>
|
||||
<Row className={clsx('gap-4 sm:gap-6', className)}>
|
||||
<Col>
|
||||
<div className="text-sm text-gray-500 whitespace-nowrap">
|
||||
Total bets
|
||||
|
@ -197,7 +198,7 @@ export function MyBetsSummary(props: {
|
|||
{resolution ? (
|
||||
<>
|
||||
<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>
|
||||
</Col>
|
||||
</>
|
||||
|
@ -309,5 +310,5 @@ function NoLabel() {
|
|||
}
|
||||
|
||||
function CancelLabel() {
|
||||
return <span className="text-yellow-400">CANCEL</span>
|
||||
return <span className="text-yellow-400">N/A</span>
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Fragment, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
compute,
|
||||
Contract,
|
||||
|
@ -13,7 +13,8 @@ import router from 'next/router'
|
|||
import { useUser } from '../hooks/use-user'
|
||||
import { Row } from './layout/row'
|
||||
import dayjs from 'dayjs'
|
||||
import Link from 'next/link'
|
||||
import { Linkify } from './linkify'
|
||||
import clsx from 'clsx'
|
||||
|
||||
function ContractDescription(props: {
|
||||
contract: Contract
|
||||
|
@ -33,46 +34,6 @@ function ContractDescription(props: {
|
|||
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 (
|
||||
<div className="whitespace-pre-line">
|
||||
<Linkify text={contract.description} />
|
||||
|
@ -142,23 +103,25 @@ export const ContractOverview = (props: {
|
|||
}[contract.resolution || '']
|
||||
|
||||
return (
|
||||
<Col className={className}>
|
||||
<Col className={clsx('mb-6', className)}>
|
||||
<Col className="justify-between md:flex-row">
|
||||
<Col>
|
||||
<div className="text-3xl text-indigo-700 mt-2 mb-4">
|
||||
{contract.question}
|
||||
<div className="text-3xl text-indigo-700 mb-4">
|
||||
<Linkify text={contract.question} />
|
||||
</div>
|
||||
|
||||
<ContractDetails contract={contract} />
|
||||
</Col>
|
||||
|
||||
{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={resolutionColor}>{resolution}</div>
|
||||
<div className={resolutionColor}>
|
||||
{resolution === 'CANCEL' ? 'N/A' : resolution}
|
||||
</div>
|
||||
</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}
|
||||
<div className="text-xl">chance</div>
|
||||
</Col>
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { formatMoney } from '../lib/util/format'
|
||||
import { User } from '../lib/firebase/users'
|
||||
import { UserLink } from './user-page'
|
||||
import { Linkify } from './linkify'
|
||||
|
||||
export function ContractDetails(props: { contract: Contract }) {
|
||||
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">
|
||||
<div className="card">
|
||||
<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">
|
||||
{contract.question}
|
||||
<Linkify text={contract.question} />
|
||||
</p>
|
||||
<div className={clsx('text-4xl', resolutionColor)}>
|
||||
{resolutionText || (
|
||||
|
@ -88,7 +89,7 @@ function ContractsGrid(props: { contracts: Contract[] }) {
|
|||
|
||||
if (contracts.length === 0) {
|
||||
return (
|
||||
<p>
|
||||
<p className="mx-4">
|
||||
No markets found. Would you like to{' '}
|
||||
<Link href="/create">
|
||||
<a className="text-green-500 hover:underline hover:decoration-2">
|
||||
|
@ -105,7 +106,6 @@ function ContractsGrid(props: { contracts: Contract[] }) {
|
|||
{contracts.map((contract) => (
|
||||
<ContractCard contract={contract} key={contract.id} />
|
||||
))}
|
||||
{/* TODO: Show placeholder if empty */}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ export function SearchableGrid(props: {
|
|||
|
||||
export function ContractsList(props: { creator: User }) {
|
||||
const { creator } = props
|
||||
const [contracts, setContracts] = useState<Contract[]>([])
|
||||
const [contracts, setContracts] = useState<Contract[] | 'loading'>('loading')
|
||||
|
||||
useEffect(() => {
|
||||
if (creator?.id) {
|
||||
|
@ -182,5 +182,7 @@ export function ContractsList(props: { creator: User }) {
|
|||
}
|
||||
}, [creator])
|
||||
|
||||
if (contracts === 'loading') return <></>
|
||||
|
||||
return <SearchableGrid contracts={contracts} defaultSort="all" />
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
42
web/components/linkify.tsx
Normal file
42
web/components/linkify.tsx
Normal 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>
|
||||
)
|
||||
}
|
27
web/components/mantic-logo.tsx
Normal file
27
web/components/mantic-logo.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -4,7 +4,7 @@ import clsx from 'clsx'
|
|||
|
||||
export function MenuButton(props: {
|
||||
buttonContent: any
|
||||
menuItems: { name: string; href: string }[]
|
||||
menuItems: { name: string; href: string; onClick?: () => void }[]
|
||||
className?: string
|
||||
}) {
|
||||
const { buttonContent, menuItems, className } = props
|
||||
|
@ -28,12 +28,13 @@ export function MenuButton(props: {
|
|||
leaveFrom="transform opacity-100 scale-100"
|
||||
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) => (
|
||||
<Menu.Item key={item.name}>
|
||||
{({ active }) => (
|
||||
<a
|
||||
href={item.href}
|
||||
onClick={item.onClick}
|
||||
className={clsx(
|
||||
active ? 'bg-gray-100' : '',
|
||||
'block py-2 px-4 text-sm text-gray-700'
|
||||
|
|
103
web/components/nav-bar.tsx
Normal file
103
web/components/nav-bar.tsx
Normal 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
20
web/components/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
73
web/components/profile-menu.tsx
Normal file
73
web/components/profile-menu.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -28,8 +28,10 @@ export function ResolutionPanel(props: {
|
|||
const resolve = async () => {
|
||||
setIsSubmitting(true)
|
||||
|
||||
const result = await resolveMarket({ outcome, contractId: contract.id })
|
||||
.then(r => r.data as any)
|
||||
const result = await resolveMarket({
|
||||
outcome,
|
||||
contractId: contract.id,
|
||||
}).then((r) => r.data as any)
|
||||
|
||||
console.log('resolved', outcome, 'result:', result)
|
||||
|
||||
|
@ -50,10 +52,7 @@ export function ResolutionPanel(props: {
|
|||
|
||||
return (
|
||||
<Col
|
||||
className={clsx(
|
||||
'bg-gray-100 shadow-xl px-8 py-6 rounded-md w-full md:w-auto',
|
||||
className
|
||||
)}
|
||||
className={clsx('bg-gray-100 shadow-xl px-8 py-6 rounded-md', className)}
|
||||
>
|
||||
<Title className="mt-0" text="Your market" />
|
||||
|
||||
|
@ -68,7 +67,6 @@ export function ResolutionPanel(props: {
|
|||
|
||||
<Spacer h={3} />
|
||||
|
||||
|
||||
<div>
|
||||
{outcome === 'YES' ? (
|
||||
<>
|
||||
|
@ -87,12 +85,9 @@ export function ResolutionPanel(props: {
|
|||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<Spacer h={3} />
|
||||
|
||||
{!!error &&
|
||||
<div className='text-red-500'>{error}</div>
|
||||
}
|
||||
{!!error && <div className="text-red-500">{error}</div>}
|
||||
|
||||
<ConfirmationButton
|
||||
id="resolution-modal"
|
||||
|
|
|
@ -5,7 +5,7 @@ export function Title(props: { text: string; className?: string }) {
|
|||
return (
|
||||
<h1
|
||||
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
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { firebaseLogout, User } from '../lib/firebase/users'
|
||||
import { Header } from './header'
|
||||
import { ContractsList } from './contracts-list'
|
||||
import { Title } from './title'
|
||||
import { Row } from './layout/row'
|
||||
|
@ -7,6 +6,7 @@ import { formatMoney } from '../lib/util/format'
|
|||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
import { SEO } from './SEO'
|
||||
import { Page } from './page'
|
||||
|
||||
export function UserLink(props: { username: string; className?: string }) {
|
||||
const { username, className } = props
|
||||
|
@ -29,7 +29,7 @@ export function UserLink(props: { username: string; className?: string }) {
|
|||
function UserCard(props: { user: User; showPrivateInfo?: boolean }) {
|
||||
const { user, showPrivateInfo } = props
|
||||
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">
|
||||
{user?.avatarUrl && (
|
||||
<img
|
||||
|
@ -70,24 +70,18 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
|||
const possesive = isCurrentUser ? 'Your ' : `${user.username}'s `
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Page>
|
||||
<SEO
|
||||
title={possesive + 'markets'}
|
||||
description={possesive + 'markets'}
|
||||
url={`/@${user.username}`}
|
||||
/>
|
||||
|
||||
<Header />
|
||||
|
||||
<div className="max-w-4xl pt-8 pb-0 sm:pb-8 mx-auto">
|
||||
<div>
|
||||
<UserCard user={user} showPrivateInfo={isCurrentUser} />
|
||||
{/* <UserCard user={user} showPrivateInfo={isCurrentUser} /> */}
|
||||
|
||||
<Title text={possesive + 'markets'} />
|
||||
|
||||
<ContractsList creator={user} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ export function YesNoCancelSelector(props: {
|
|||
onClick={() => onSelect('CANCEL')}
|
||||
className={btnClassName}
|
||||
>
|
||||
CANCEL
|
||||
N/A
|
||||
</Button>
|
||||
</Row>
|
||||
)
|
||||
|
|
15259
web/package-lock.json
generated
15259
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,8 @@
|
|||
"name": "mantic",
|
||||
"private": true,
|
||||
"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",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
|
@ -10,8 +11,8 @@
|
|||
"prepare": "cd .. && husky install web/.husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.4.2",
|
||||
"@heroicons/react": "^1.0.5",
|
||||
"@headlessui/react": "1.4.2",
|
||||
"@heroicons/react": "1.0.5",
|
||||
"@nivo/core": "0.74.0",
|
||||
"@nivo/line": "0.74.0",
|
||||
"clsx": "1.1.1",
|
||||
|
@ -25,19 +26,20 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "0.4.0",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/lodash": "4.14.178",
|
||||
"@types/node": "16.11.11",
|
||||
"@types/react": "17.0.37",
|
||||
"autoprefixer": "10.2.6",
|
||||
"concurrently": "6.5.1",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-next": "12.0.4",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.1.3",
|
||||
"husky": "7.0.4",
|
||||
"lint-staged": "12.1.3",
|
||||
"postcss": "8.3.5",
|
||||
"prettier": "2.5.0",
|
||||
"pretty-quick": "^3.1.2",
|
||||
"pretty-quick": "3.1.2",
|
||||
"tailwindcss": "3.0.1",
|
||||
"tsc-files": "^1.1.3",
|
||||
"tsc-files": "1.1.3",
|
||||
"typescript": "4.5.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { useContractWithPreload } from '../../hooks/use-contract'
|
||||
import { Header } from '../../components/header'
|
||||
import { ContractOverview } from '../../components/contract-overview'
|
||||
import { BetPanel } from '../../components/bet-panel'
|
||||
import { Col } from '../../components/layout/col'
|
||||
|
@ -15,15 +13,16 @@ import { Spacer } from '../../components/layout/spacer'
|
|||
import { User } from '../../lib/firebase/users'
|
||||
import { Contract, getContractFromSlug } from '../../lib/firebase/contracts'
|
||||
import { SEO } from '../../components/SEO'
|
||||
import { Page } from '../../components/page'
|
||||
|
||||
export async function getStaticProps(props: { params: any }) {
|
||||
const { username, slug } = props.params
|
||||
const contract = (await getContractFromSlug(slug)) || null
|
||||
const { username, contractSlug } = props.params
|
||||
const contract = (await getContractFromSlug(contractSlug)) || null
|
||||
|
||||
return {
|
||||
props: {
|
||||
username,
|
||||
slug,
|
||||
slug: contractSlug,
|
||||
contract,
|
||||
},
|
||||
|
||||
|
@ -52,31 +51,24 @@ export default function ContractPage(props: {
|
|||
const isCreator = user?.id === creatorId
|
||||
|
||||
return (
|
||||
<Col className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Page wide={!isResolved}>
|
||||
<SEO
|
||||
title={contract.question}
|
||||
description={contract.description}
|
||||
url={`/${props.username}/${props.slug}`}
|
||||
/>
|
||||
|
||||
<Header />
|
||||
|
||||
<Col
|
||||
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" />
|
||||
<Col className="w-full md:flex-row justify-between mt-6">
|
||||
<div className="flex-[3]">
|
||||
<ContractOverview contract={contract} />
|
||||
<BetsSection contract={contract} user={user ?? null} />
|
||||
</div>
|
||||
|
||||
{!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} />
|
||||
|
||||
{isCreator && user && (
|
||||
|
@ -86,7 +78,7 @@ export default function ContractPage(props: {
|
|||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Col>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -104,12 +96,12 @@ function BetsSection(props: { contract: Contract; user: User | null }) {
|
|||
if (!userBets || userBets.length === 0) return <></>
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<Title text="Your bets" />
|
||||
<MyBetsSummary contract={contract} bets={userBets} />
|
||||
<Spacer h={6} />
|
||||
<ContractBetsTable contract={contract} bets={userBets} />
|
||||
<Spacer h={6} />
|
||||
<Spacer h={12} />
|
||||
</div>
|
||||
)
|
||||
}
|
64
web/pages/about.module.css
Normal file
64
web/pages/about.module.css
Normal 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
311
web/pages/about.tsx
Normal 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'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: "Will Democrats win the 2024 US
|
||||
presidential election?"
|
||||
</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'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'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 "Will Taiwan remove its
|
||||
14-day COVID quarantine by Jun 01, 2022?" Then use the information
|
||||
to plan your trip.
|
||||
</p>
|
||||
<p>
|
||||
You can also ask subjective, personal questions like "Will I enjoy
|
||||
my 2022 Taiwan trip?". 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'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're still in Open Beta; we'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'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. "Will I enjoy participating in the
|
||||
Metaverse in 2023?")
|
||||
</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'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'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'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'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 don’t 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&rep=rep1&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>
|
||||
)
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react'
|
||||
import { Page } from '../components/page'
|
||||
import { UserPage } from '../components/user-page'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import { firebaseLogin } from '../lib/firebase/users'
|
||||
|
||||
function SignInCard() {
|
||||
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">
|
||||
<img
|
||||
src="/logo-icon-white-bg.png"
|
||||
|
@ -30,5 +31,11 @@ function SignInCard() {
|
|||
|
||||
export default function Account() {
|
||||
const user = useUser()
|
||||
return user ? <UserPage user={user} currentUser={user} /> : <SignInCard />
|
||||
return user ? (
|
||||
<UserPage user={user} currentUser={user} />
|
||||
) : (
|
||||
<Page>
|
||||
<SignInCard />
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BetsList } from '../components/bets-list'
|
||||
import { Header } from '../components/header'
|
||||
import { Page } from '../components/page'
|
||||
import { SEO } from '../components/SEO'
|
||||
import { Title } from '../components/title'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
|
@ -8,17 +8,10 @@ export default function BetsPage() {
|
|||
const user = useUser()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Page>
|
||||
<SEO title="Your bets" description="Your bets" url="/bets" />
|
||||
|
||||
<Header />
|
||||
|
||||
<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>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,12 +2,12 @@ import router from 'next/router'
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { ContractsList } from '../components/contracts-list'
|
||||
import { Header } from '../components/header'
|
||||
import { Spacer } from '../components/layout/spacer'
|
||||
import { Title } from '../components/title'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import { path } from '../lib/firebase/contracts'
|
||||
import { createContract } from '../lib/service/create-contract'
|
||||
import { Page } from '../components/page'
|
||||
|
||||
// Allow user to create a new contract
|
||||
export default function NewContract() {
|
||||
|
@ -42,10 +42,7 @@ export default function NewContract() {
|
|||
if (!creator) return <></>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
|
||||
<div className="max-w-4xl py-12 lg:mx-auto px-4">
|
||||
<Page>
|
||||
<Title text="Create a new prediction market" />
|
||||
|
||||
<div className="w-full bg-gray-100 rounded-lg shadow-xl px-6 py-4">
|
||||
|
@ -123,7 +120,6 @@ export default function NewContract() {
|
|||
<Title text="Your markets" />
|
||||
|
||||
{creator && <ContractsList creator={creator} />}
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,28 @@
|
|||
import React from 'react'
|
||||
import type { NextPage } from 'next'
|
||||
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import Markets from './markets'
|
||||
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()
|
||||
|
||||
if (user === undefined) return <></>
|
||||
return user ? <Markets /> : <LandingPage />
|
||||
|
||||
return user ? <Markets contracts={props.contracts} /> : <LandingPage />
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
|
|
@ -11,7 +11,8 @@ import { firebaseLogin } from '../lib/firebase/users'
|
|||
import { useContracts } from '../hooks/use-contracts'
|
||||
import { SearchableGrid } from '../components/contracts-list'
|
||||
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() {
|
||||
return (
|
||||
|
@ -23,25 +24,25 @@ export default function LandingPage() {
|
|||
)
|
||||
}
|
||||
|
||||
function Hero() {
|
||||
const scrollToAbout = () => {
|
||||
const aboutElem = document.getElementById('about')
|
||||
window.scrollTo({ top: aboutElem?.offsetTop, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
function Hero() {
|
||||
return (
|
||||
<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
|
||||
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}
|
||||
>
|
||||
About
|
||||
</div>
|
||||
</Header>
|
||||
</NavBar>
|
||||
<main>
|
||||
<div className="pt-40 sm:pt-16 lg:pt-8 lg:pb-14 lg:overflow-hidden">
|
||||
<div className="mx-auto max-w-7xl lg:px-8">
|
||||
<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 xl:px-0">
|
||||
<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="lg:py-24">
|
||||
|
@ -51,7 +52,7 @@ function Hero() {
|
|||
prediction markets
|
||||
</div>
|
||||
</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
|
||||
<br />
|
||||
for you and your community
|
||||
|
@ -82,9 +83,6 @@ function Hero() {
|
|||
)
|
||||
}
|
||||
|
||||
const notionAboutUrl =
|
||||
'https://mantic.notion.site/About-Mantic-Markets-7c44bc161356474cad54cba2d2973fe2'
|
||||
|
||||
function FeaturesSection() {
|
||||
const features = [
|
||||
{
|
||||
|
@ -115,7 +113,7 @@ function FeaturesSection() {
|
|||
return (
|
||||
<div id="about" className="w-full py-16 bg-green-50">
|
||||
<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">
|
||||
<h2 className="text-base text-teal-600 font-semibold tracking-wide uppercase">
|
||||
Mantic Markets
|
||||
|
@ -150,13 +148,9 @@ function FeaturesSection() {
|
|||
</div>
|
||||
|
||||
<Col className="mt-20">
|
||||
<a
|
||||
className="btn btn-primary mx-auto"
|
||||
href={notionAboutUrl}
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
<Link href="/about">
|
||||
<a className="btn btn-primary mx-auto">Learn more</a>
|
||||
</Link>
|
||||
</Col>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -167,8 +161,8 @@ function ExploreMarketsSection() {
|
|||
const contracts = useContracts()
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl py-8 mx-auto">
|
||||
<p className="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-indigo-700 sm:text-4xl">
|
||||
<div className="max-w-4xl px-4 py-8 mx-auto">
|
||||
<p className="my-12 text-3xl leading-8 font-extrabold tracking-tight text-indigo-700 sm:text-4xl">
|
||||
Explore our markets
|
||||
</p>
|
||||
<SearchableGrid contracts={contracts === 'loading' ? [] : contracts} />
|
||||
|
|
|
@ -1,16 +1,30 @@
|
|||
import { SearchableGrid } from '../components/contracts-list'
|
||||
import { Header } from '../components/header'
|
||||
import { Page } from '../components/page'
|
||||
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()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<div className="max-w-4xl py-8 mx-auto">
|
||||
<SearchableGrid contracts={contracts === 'loading' ? [] : contracts} />
|
||||
</div>
|
||||
</div>
|
||||
<Page>
|
||||
{(props.contracts || contracts !== 'loading') && (
|
||||
<SearchableGrid
|
||||
contracts={contracts === 'loading' ? props.contracts : contracts}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { DatumValue } from '@nivo/core'
|
|||
import { ResponsiveLine } from '@nivo/line'
|
||||
|
||||
import { Entry, makeEntries } from '../lib/simulator/entries'
|
||||
import { Header } from '../components/header'
|
||||
import { NavBar } from '../components/nav-bar'
|
||||
import { Col } from '../components/layout/col'
|
||||
|
||||
function TableBody(props: { entries: Entry[] }) {
|
||||
|
@ -149,9 +149,7 @@ function NewBidTable(props: {
|
|||
|
||||
function randomBid() {
|
||||
const bidType = Math.random() < 0.5 ? 'YES' : 'NO'
|
||||
const p = bidType === 'YES'
|
||||
? nextEntry.prob
|
||||
: 1 - nextEntry.prob
|
||||
const p = bidType === 'YES' ? nextEntry.prob : 1 - nextEntry.prob
|
||||
|
||||
const amount = Math.round(p * Math.random() * 300) + 1
|
||||
const bid = makeBid(bidType, amount)
|
||||
|
@ -256,7 +254,7 @@ export default function Simulator() {
|
|||
|
||||
return (
|
||||
<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">
|
||||
{/* Left column */}
|
||||
<div>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { SearchableGrid } from '../../components/contracts-list'
|
||||
import { Header } from '../../components/header'
|
||||
import { Page } from '../../components/page'
|
||||
import { Title } from '../../components/title'
|
||||
import { useContracts } from '../../hooks/use-contracts'
|
||||
import Markets from '../markets'
|
||||
|
||||
export default function TagPage() {
|
||||
const router = useRouter()
|
||||
|
@ -12,18 +11,17 @@ export default function TagPage() {
|
|||
let contracts = useContracts()
|
||||
|
||||
if (tag && contracts !== 'loading') {
|
||||
contracts = contracts.filter((contract) =>
|
||||
contract.description.toLowerCase().includes(`#${tag.toLowerCase()}`)
|
||||
contracts = contracts.filter(
|
||||
(contract) =>
|
||||
contract.description.toLowerCase().includes(`#${tag.toLowerCase()}`) ||
|
||||
contract.question.toLowerCase().includes(`#${tag.toLowerCase()}`)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<div className="max-w-4xl py-8 mx-auto">
|
||||
<Page>
|
||||
<Title text={`#${tag}`} />
|
||||
<SearchableGrid contracts={contracts === 'loading' ? [] : contracts} />
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ module.exports = {
|
|||
fontFamily: Object.assign(
|
||||
{ ...defaultTheme.fontFamily },
|
||||
{
|
||||
'major-mono': ['Major Mono Display', 'monospace'],
|
||||
'major-mono': ['Courier', 'monospace'],
|
||||
'readex-pro': ['Readex Pro', 'sans-serif'],
|
||||
}
|
||||
),
|
||||
|
|
4774
web/yarn.lock
Normal file
4774
web/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user