Merge branch 'main' into sell-bets
This commit is contained in:
commit
4c353242b0
|
@ -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
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
|
.next
|
||||||
node_modules
|
node_modules
|
||||||
out
|
out
|
||||||
|
tsconfig.tsbuildinfo
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
className="flex-1 justify-end mr-5 sm:mr-8"
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={bets}
|
bets={bets}
|
||||||
/>
|
/>
|
||||||
|
</Row>
|
||||||
|
|
||||||
{/* Show carrot for collapsing. Hack the positioning. */}
|
|
||||||
<div
|
<div
|
||||||
className="collapse-title p-0 pr-8 relative w-0 h-0 min-h-0"
|
className="collapse-content !px-0"
|
||||||
style={{ top: -10, right: -20 }}
|
style={{ backgroundColor: 'white' }}
|
||||||
/>
|
>
|
||||||
</Row>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<div className="collapse-content" 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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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" />
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
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
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 () => {
|
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)
|
||||||
|
|
||||||
|
@ -50,10 +52,7 @@ export function ResolutionPanel(props: {
|
||||||
|
|
||||||
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"
|
||||||
|
|
|
@ -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
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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">
|
|
||||||
<div>
|
|
||||||
<UserCard user={user} showPrivateInfo={isCurrentUser} />
|
|
||||||
|
|
||||||
<Title text={possesive + 'markets'} />
|
<Title text={possesive + 'markets'} />
|
||||||
|
|
||||||
<ContractsList creator={user} />
|
<ContractsList creator={user} />
|
||||||
</div>
|
</Page>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
15259
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -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": {
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
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 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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
<div className="max-w-4xl pt-8 pb-0 sm:pb-8 mx-auto">
|
|
||||||
<div>
|
|
||||||
<Title text="Your bets" />
|
<Title text="Your bets" />
|
||||||
{user && <BetsList user={user} />}
|
{user && <BetsList user={user} />}
|
||||||
</div>
|
</Page>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,10 +42,7 @@ export default function NewContract() {
|
||||||
if (!creator) return <></>
|
if (!creator) return <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Page>
|
||||||
<Header />
|
|
||||||
|
|
||||||
<div className="max-w-4xl py-12 lg:mx-auto px-4">
|
|
||||||
<Title text="Create a new prediction market" />
|
<Title text="Create a new prediction market" />
|
||||||
|
|
||||||
<div className="w-full bg-gray-100 rounded-lg shadow-xl px-6 py-4">
|
<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" />
|
<Title text="Your markets" />
|
||||||
|
|
||||||
{creator && <ContractsList creator={creator} />}
|
{creator && <ContractsList creator={creator} />}
|
||||||
</div>
|
</Page>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 />
|
|
||||||
<div className="max-w-4xl py-8 mx-auto">
|
|
||||||
<Title text={`#${tag}`} />
|
<Title text={`#${tag}`} />
|
||||||
<SearchableGrid contracts={contracts === 'loading' ? [] : contracts} />
|
<SearchableGrid contracts={contracts === 'loading' ? [] : contracts} />
|
||||||
</div>
|
</Page>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
4774
web/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user