Merge branch 'main' into atlas
This commit is contained in:
commit
a1368abd66
27
common/quadratic-funding.ts
Normal file
27
common/quadratic-funding.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { groupBy, mapValues, sum, sumBy } from 'lodash'
|
||||
import { Txn } from './txn'
|
||||
|
||||
// Returns a map of charity ids to the amount of M$ matched
|
||||
export function quadraticMatches(
|
||||
allCharityTxns: Txn[],
|
||||
matchingPool: number
|
||||
): Record<string, number> {
|
||||
// For each charity, group the donations by each individual donor
|
||||
const donationsByCharity = groupBy(allCharityTxns, 'toId')
|
||||
const donationsByDonors = mapValues(donationsByCharity, (txns) =>
|
||||
groupBy(txns, 'fromId')
|
||||
)
|
||||
|
||||
// Weight for each charity = [sum of sqrt(individual donor)] ^ 2
|
||||
const weights = mapValues(donationsByDonors, (byDonor) => {
|
||||
const sumByDonor = Object.values(byDonor).map((txns) =>
|
||||
sumBy(txns, 'amount')
|
||||
)
|
||||
const sumOfRoots = sumBy(sumByDonor, Math.sqrt)
|
||||
return sumOfRoots ** 2
|
||||
})
|
||||
|
||||
// Then distribute the matching pool based on the individual weights
|
||||
const totalWeight = sum(Object.values(weights))
|
||||
return mapValues(weights, (weight) => matchingPool * (weight / totalWeight))
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import { sortBy } from 'lodash'
|
||||
|
||||
export const logInterpolation = (min: number, max: number, value: number) => {
|
||||
if (value <= min) return 0
|
||||
if (value >= max) return 1
|
||||
|
@ -16,4 +18,15 @@ export function normpdf(x: number, mean = 0, variance = 1) {
|
|||
)
|
||||
}
|
||||
|
||||
const TAU = Math.PI * 2
|
||||
export const TAU = Math.PI * 2
|
||||
|
||||
export function median(values: number[]) {
|
||||
if (values.length === 0) return NaN
|
||||
|
||||
const sorted = sortBy(values, (x) => x)
|
||||
const mid = Math.floor(sorted.length / 2)
|
||||
if (sorted.length % 2 === 0) {
|
||||
return (sorted[mid - 1] + sorted[mid]) / 2
|
||||
}
|
||||
return sorted[mid]
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as admin from 'firebase-admin'
|
|||
import { z } from 'zod'
|
||||
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
import { getCpmmSellBetInfo } from '../../common/sell-bet'
|
||||
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||
|
@ -57,8 +57,12 @@ export const sellshares = newEndpoint(['POST'], async (req, auth) => {
|
|||
prevLoanAmount
|
||||
)
|
||||
|
||||
if (!isFinite(newP)) {
|
||||
throw new APIError(500, 'Trade rejected due to overflow error.')
|
||||
if (
|
||||
!newP ||
|
||||
!isFinite(newP) ||
|
||||
Math.min(...Object.values(newPool ?? {})) < CPMM_MIN_POOL_QTY
|
||||
) {
|
||||
throw new APIError(400, 'Sale too large for current liquidity pool.')
|
||||
}
|
||||
|
||||
const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
|
||||
|
|
|
@ -8,8 +8,15 @@ module.exports = {
|
|||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'@next/next/no-img-element': 'off',
|
||||
'@next/next/no-typos': 'off',
|
||||
'lodash/import-scope': [2, 'member'],
|
||||
|
|
|
@ -24,7 +24,7 @@ export function AddFundsButton(props: { className?: string }) {
|
|||
className
|
||||
)}
|
||||
>
|
||||
Add funds
|
||||
Get M$
|
||||
</label>
|
||||
<input type="checkbox" id="add-funds" className="modal-toggle" />
|
||||
|
||||
|
|
24
web/components/alert-box.tsx
Normal file
24
web/components/alert-box.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { ExclamationIcon } from '@heroicons/react/solid'
|
||||
import { Linkify } from './linkify'
|
||||
|
||||
export function AlertBox(props: { title: string; text: string }) {
|
||||
const { title, text } = props
|
||||
return (
|
||||
<div className="rounded-md bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<ExclamationIcon
|
||||
className="h-5 w-5 text-yellow-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-yellow-800">{title}</h3>
|
||||
<div className="mt-2 text-sm text-yellow-700">
|
||||
<Linkify text={text} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { sortBy, partition, sum, uniq } from 'lodash'
|
||||
import { useLayoutEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { FreeResponseContract } from 'common/contract'
|
||||
import { Col } from '../layout/col'
|
||||
|
@ -85,7 +85,7 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) {
|
|||
})
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
setChosenAnswers({})
|
||||
}, [resolveOption])
|
||||
|
||||
|
@ -116,7 +116,7 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) {
|
|||
{!resolveOption && (
|
||||
<div className={clsx('flow-root pr-2 md:pr-0')}>
|
||||
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
|
||||
{answerItems.map((item, activityItemIdx) => (
|
||||
{answerItems.map((item) => (
|
||||
<div key={item.id} className={'relative pb-2'}>
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<OpenAnswer {...item} />
|
||||
|
|
|
@ -138,9 +138,8 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) {
|
|||
return !hasSoldAll
|
||||
})
|
||||
|
||||
const [settled, unsettled] = partition(
|
||||
contracts,
|
||||
(c) => c.isResolved || contractsMetrics[c.id].invested === 0
|
||||
const unsettled = contracts.filter(
|
||||
(c) => !c.isResolved && contractsMetrics[c.id].invested !== 0
|
||||
)
|
||||
|
||||
const currentInvested = sumBy(
|
||||
|
@ -261,7 +260,7 @@ function ContractBets(props: {
|
|||
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
const { payout, profit, profitPercent, invested } = getContractBetMetrics(
|
||||
const { payout, profit, profitPercent } = getContractBetMetrics(
|
||||
contract,
|
||||
bets
|
||||
)
|
||||
|
@ -657,7 +656,6 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
|||
|
||||
return (
|
||||
<ConfirmationButton
|
||||
id={`sell-${bet.id}`}
|
||||
openModalBtn={{
|
||||
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
||||
label: 'Sell',
|
||||
|
|
|
@ -6,9 +6,11 @@ import { Charity } from 'common/charity'
|
|||
import { useCharityTxns } from 'web/hooks/use-charity-txns'
|
||||
import { manaToUSD } from '../../../common/util/format'
|
||||
import { Row } from '../layout/row'
|
||||
import { Col } from '../layout/col'
|
||||
|
||||
export function CharityCard(props: { charity: Charity }) {
|
||||
const { name, slug, photo, preview, id, tags } = props.charity
|
||||
export function CharityCard(props: { charity: Charity; match?: number }) {
|
||||
const { charity, match } = props
|
||||
const { slug, photo, preview, id, tags } = charity
|
||||
|
||||
const txns = useCharityTxns(id)
|
||||
const raised = sumBy(txns, (txn) => txn.amount)
|
||||
|
@ -32,14 +34,22 @@ export function CharityCard(props: { charity: Charity }) {
|
|||
{/* <h3 className="card-title line-clamp-3">{name}</h3> */}
|
||||
<div className="line-clamp-4 text-sm">{preview}</div>
|
||||
{raised > 0 && (
|
||||
<Row className="text-primary mt-4 flex-1 items-end justify-center gap-2">
|
||||
<span className="text-3xl">
|
||||
{raised < 100
|
||||
? manaToUSD(raised)
|
||||
: '$' + Math.floor(raised / 100)}
|
||||
</span>
|
||||
<span>raised</span>
|
||||
</Row>
|
||||
<>
|
||||
<Row className="mt-4 flex-1 items-end justify-center gap-6 text-gray-900">
|
||||
<Col>
|
||||
<span className="text-3xl font-semibold">
|
||||
{formatUsd(raised)}
|
||||
</span>
|
||||
<span>raised</span>
|
||||
</Col>
|
||||
{match && (
|
||||
<Col className="text-gray-500">
|
||||
<span className="text-xl">+{formatUsd(match)}</span>
|
||||
<span className="">match</span>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -47,6 +57,10 @@ export function CharityCard(props: { charity: Charity }) {
|
|||
)
|
||||
}
|
||||
|
||||
function formatUsd(mana: number) {
|
||||
return mana < 100 ? manaToUSD(mana) : '$' + Math.floor(mana / 100)
|
||||
}
|
||||
|
||||
function FeaturedBadge() {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 bg-yellow-100 px-3 py-0.5 text-sm font-medium text-yellow-800">
|
||||
|
|
|
@ -24,13 +24,12 @@ export function ChoicesToggleGroup(props: {
|
|||
<RadioGroup
|
||||
className={clsx(className, 'flex flex-row flex-wrap items-center gap-3')}
|
||||
value={currentChoice.toString()}
|
||||
onChange={(str) => null}
|
||||
onChange={setChoice}
|
||||
>
|
||||
{Object.keys(choicesMap).map((choiceKey) => (
|
||||
<RadioGroup.Option
|
||||
key={choiceKey}
|
||||
value={choicesMap[choiceKey]}
|
||||
onClick={() => setChoice(choicesMap[choiceKey])}
|
||||
className={({ active }) =>
|
||||
clsx(
|
||||
active ? 'ring-2 ring-indigo-500 ring-offset-2' : '',
|
||||
|
|
|
@ -5,7 +5,6 @@ import { Modal } from './layout/modal'
|
|||
import { Row } from './layout/row'
|
||||
|
||||
export function ConfirmationButton(props: {
|
||||
id: string
|
||||
openModalBtn: {
|
||||
label: string
|
||||
icon?: JSX.Element
|
||||
|
@ -22,7 +21,7 @@ export function ConfirmationButton(props: {
|
|||
onSubmit: () => void
|
||||
children: ReactNode
|
||||
}) {
|
||||
const { id, openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
|
||||
const { openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
|
@ -67,7 +66,6 @@ export function ResolveConfirmationButton(props: {
|
|||
props
|
||||
return (
|
||||
<ConfirmationButton
|
||||
id="resolution-modal"
|
||||
openModalBtn={{
|
||||
className: clsx(
|
||||
'border-none self-start',
|
||||
|
|
|
@ -140,7 +140,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
}
|
||||
}
|
||||
|
||||
const textColor = `text-${getColor(contract, previewProb)}`
|
||||
const textColor = `text-${getColor(contract)}`
|
||||
|
||||
return (
|
||||
<Col
|
||||
|
@ -223,7 +223,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
|
||||
export function ProbBar(props: { contract: Contract; previewProb?: number }) {
|
||||
const { contract, previewProb } = props
|
||||
const color = getColor(contract, previewProb)
|
||||
const color = getColor(contract)
|
||||
const prob = previewProb ?? getProb(contract)
|
||||
return (
|
||||
<>
|
||||
|
@ -257,7 +257,7 @@ function QuickOutcomeView(props: {
|
|||
// If there's a preview prob, display that instead of the current prob
|
||||
const override =
|
||||
previewProb === undefined ? undefined : formatPercent(previewProb)
|
||||
const textColor = `text-${getColor(contract, previewProb)}`
|
||||
const textColor = `text-${getColor(contract)}`
|
||||
|
||||
let display: string | undefined
|
||||
switch (outcomeType) {
|
||||
|
@ -306,7 +306,7 @@ function getNumericScale(contract: NumericContract) {
|
|||
return (ev - min) / (max - min)
|
||||
}
|
||||
|
||||
export function getColor(contract: Contract, previewProb?: number) {
|
||||
export function getColor(contract: Contract) {
|
||||
// TODO: Try injecting a gradient here
|
||||
// return 'primary'
|
||||
const { resolution } = contract
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
import { sample } from 'lodash'
|
||||
import { SparklesIcon, XIcon } from '@heroicons/react/solid'
|
||||
import { Avatar } from './avatar'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { NewContract } from '../pages/create'
|
||||
import { firebaseLogin, User } from 'web/lib/firebase/users'
|
||||
import { ContractsGrid } from './contract/contracts-list'
|
||||
import { Contract, MAX_QUESTION_LENGTH } from 'common/contract'
|
||||
import { Col } from './layout/col'
|
||||
import clsx from 'clsx'
|
||||
import { Row } from './layout/row'
|
||||
import { ENV_CONFIG } from '../../common/envs/constants'
|
||||
import { SiteLink } from './site-link'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
|
||||
export function FeedPromo(props: { hotContracts: Contract[] }) {
|
||||
const { hotContracts } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col className="mb-6 rounded-xl text-center sm:m-12 sm:mt-0">
|
||||
<img
|
||||
height={250}
|
||||
width={250}
|
||||
className="self-center"
|
||||
src="/flappy-logo.gif"
|
||||
/>
|
||||
<h1 className="text-3xl sm:text-6xl xl:text-6xl">
|
||||
<div className="font-semibold sm:mb-2">
|
||||
Bet on{' '}
|
||||
<span className="bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text font-bold text-transparent">
|
||||
anything!
|
||||
</span>
|
||||
</div>
|
||||
</h1>
|
||||
<Spacer h={6} />
|
||||
<div className="mb-4 px-2 text-gray-500">
|
||||
Bet on any topic imaginable with play-money markets. Or create your
|
||||
own!
|
||||
<br />
|
||||
<br />
|
||||
Sign up and get {formatMoney(1000)} - worth $10 to your{' '}
|
||||
<SiteLink className="font-semibold" href="/charity">
|
||||
favorite charity.
|
||||
</SiteLink>
|
||||
<br />
|
||||
</div>
|
||||
<Spacer h={6} />
|
||||
<button
|
||||
className="self-center rounded-md border-none bg-gradient-to-r from-teal-500 to-green-500 py-4 px-6 text-lg font-semibold normal-case text-white hover:from-teal-600 hover:to-green-600"
|
||||
onClick={firebaseLogin}
|
||||
>
|
||||
Start betting now
|
||||
</button>{' '}
|
||||
</Col>
|
||||
|
||||
<Row className="m-4 mb-6 items-center gap-1 text-xl font-semibold text-gray-800">
|
||||
<SparklesIcon className="inline h-5 w-5" aria-hidden="true" />
|
||||
Trending markets
|
||||
</Row>
|
||||
<ContractsGrid
|
||||
contracts={hotContracts?.slice(0, 10) || []}
|
||||
loadMore={() => {}}
|
||||
hasMore={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FeedCreate(props: {
|
||||
user?: User
|
||||
tag?: string
|
||||
placeholder?: string
|
||||
className?: string
|
||||
}) {
|
||||
const { user, tag, className } = props
|
||||
const [question, setQuestion] = useState('')
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>()
|
||||
|
||||
// Rotate through a new placeholder each day
|
||||
// Easter egg idea: click your own name to shuffle the placeholder
|
||||
// const daysSinceEpoch = Math.floor(Date.now() / 1000 / 60 / 60 / 24)
|
||||
|
||||
// Take care not to produce a different placeholder on the server and client
|
||||
const [defaultPlaceholder, setDefaultPlaceholder] = useState('')
|
||||
useEffect(() => {
|
||||
setDefaultPlaceholder(`e.g. ${sample(ENV_CONFIG.newQuestionPlaceholders)}`)
|
||||
}, [])
|
||||
|
||||
const placeholder = props.placeholder ?? defaultPlaceholder
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'w-full cursor-text rounded bg-white p-4 shadow-md',
|
||||
isExpanded ? 'ring-2 ring-indigo-300' : '',
|
||||
className
|
||||
)}
|
||||
onClick={() => {
|
||||
!isExpanded && inputRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<Avatar username={user?.username} avatarUrl={user?.avatarUrl} noLink />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<Row className="justify-between">
|
||||
<p className="my-0.5 text-sm">Ask a question... </p>
|
||||
{isExpanded && (
|
||||
<button
|
||||
className="btn btn-xs btn-circle btn-ghost rounded"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
>
|
||||
<XIcon
|
||||
className="mx-auto h-6 w-6 text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Row>
|
||||
<textarea
|
||||
ref={inputRef as any}
|
||||
className={clsx(
|
||||
'w-full resize-none appearance-none border-transparent bg-transparent p-0 text-indigo-700 placeholder:text-gray-400 focus:border-transparent focus:ring-transparent',
|
||||
question && 'text-lg sm:text-xl',
|
||||
!question && 'text-base sm:text-lg'
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
value={question}
|
||||
rows={question.length > 68 ? 4 : 2}
|
||||
maxLength={MAX_QUESTION_LENGTH}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setQuestion(e.target.value.replace('\n', ''))}
|
||||
onFocus={() => setIsExpanded(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hide component instead of deleting, so edits to NewContract don't get lost */}
|
||||
<div className={isExpanded ? '' : 'hidden'}>
|
||||
<NewContract question={question} tag={tag} />
|
||||
</div>
|
||||
|
||||
{/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
|
||||
{!isExpanded && (
|
||||
<div className="flex justify-end sm:-mt-4">
|
||||
<button className="btn btn-sm capitalize" disabled>
|
||||
Create Market
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -2,7 +2,7 @@ import { Answer } from 'common/answer'
|
|||
import { Bet } from 'common/bet'
|
||||
import { Comment } from 'common/comment'
|
||||
import { formatPercent } from 'common/util/format'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { Modal } from 'web/components/layout/modal'
|
||||
import { AnswerBetPanel } from 'web/components/answers/answer-bet-panel'
|
||||
|
|
|
@ -3,7 +3,6 @@ import React, { useState } from 'react'
|
|||
import {
|
||||
BanIcon,
|
||||
CheckIcon,
|
||||
DotsVerticalIcon,
|
||||
LockClosedIcon,
|
||||
XIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
|
@ -274,30 +273,3 @@ function FeedClose(props: { contract: Contract }) {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Should highlight the entire Feed segment
|
||||
function FeedExpand(props: { setExpanded: (expanded: boolean) => void }) {
|
||||
const { setExpanded } = props
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setExpanded(true)}>
|
||||
<div className="relative px-1">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300">
|
||||
<DotsVerticalIcon
|
||||
className="h-5 w-5 text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button onClick={() => setExpanded(true)}>
|
||||
<div className="min-w-0 flex-1 py-1.5">
|
||||
<div className="text-sm text-gray-500 hover:text-gray-700">
|
||||
<span>Show all activity</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ export function CreateFoldButton() {
|
|||
|
||||
return (
|
||||
<ConfirmationButton
|
||||
id="create-fold"
|
||||
openModalBtn={{
|
||||
label: 'New',
|
||||
icon: <PlusCircleIcon className="mr-2 h-5 w-5" />,
|
||||
|
|
65
web/components/landing-page-panel.tsx
Normal file
65
web/components/landing-page-panel.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { SparklesIcon } from '@heroicons/react/solid'
|
||||
|
||||
import { Contract } from 'common/contract'
|
||||
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||
import { ContractsGrid } from './contract/contracts-list'
|
||||
import { Col } from './layout/col'
|
||||
import { Row } from './layout/row'
|
||||
|
||||
export function LandingPagePanel(props: { hotContracts: Contract[] }) {
|
||||
const { hotContracts } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col className="mb-6 rounded-xl sm:m-12 sm:mt-0">
|
||||
<img
|
||||
height={250}
|
||||
width={250}
|
||||
className="self-center"
|
||||
src="/flappy-logo.gif"
|
||||
/>
|
||||
<div className="m-4 max-w-[550px] self-center">
|
||||
<h1 className="text-3xl sm:text-6xl xl:text-6xl">
|
||||
<div className="font-semibold sm:mb-2">
|
||||
Predict{' '}
|
||||
<span className="bg-gradient-to-r from-indigo-500 to-blue-500 bg-clip-text font-bold text-transparent">
|
||||
anything!
|
||||
</span>
|
||||
</div>
|
||||
</h1>
|
||||
<Spacer h={6} />
|
||||
<div className="mb-4 px-2 ">
|
||||
Create a play-money prediction market on any topic you care about
|
||||
and bet with your friends on what will happen!
|
||||
<br />
|
||||
{/* <br />
|
||||
Sign up and get {formatMoney(1000)} - worth $10 to your{' '}
|
||||
<SiteLink className="font-semibold" href="/charity">
|
||||
favorite charity.
|
||||
</SiteLink>
|
||||
<br /> */}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer h={6} />
|
||||
<button
|
||||
className="self-center rounded-md border-none bg-gradient-to-r from-indigo-500 to-blue-500 py-4 px-6 text-lg font-semibold normal-case text-white hover:from-indigo-600 hover:to-blue-600"
|
||||
onClick={firebaseLogin}
|
||||
>
|
||||
Get started
|
||||
</button>{' '}
|
||||
</Col>
|
||||
|
||||
<Row className="m-4 mb-6 items-center gap-1 text-xl font-semibold text-gray-800">
|
||||
<SparklesIcon className="inline h-5 w-5" aria-hidden="true" />
|
||||
Trending markets
|
||||
</Row>
|
||||
<ContractsGrid
|
||||
contracts={hotContracts?.slice(0, 10) || []}
|
||||
loadMore={() => {}}
|
||||
hasMore={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -98,7 +98,7 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) {
|
|||
setError('Server error')
|
||||
}
|
||||
})
|
||||
.catch((e) => setError('Server error'))
|
||||
.catch((_) => setError('Server error'))
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -136,7 +136,7 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) {
|
|||
|
||||
function ViewLiquidityPanel(props: { contract: CPMMContract }) {
|
||||
const { contract } = props
|
||||
const { id: contractId, pool } = contract
|
||||
const { pool } = contract
|
||||
const { YES: yesShares, NO: noShares } = pool
|
||||
|
||||
return (
|
||||
|
@ -162,7 +162,7 @@ function WithdrawLiquidityPanel(props: {
|
|||
const { contract, lpShares } = props
|
||||
const { YES: yesShares, NO: noShares } = lpShares
|
||||
|
||||
const [error, setError] = useState<string | undefined>(undefined)
|
||||
const [_error, setError] = useState<string | undefined>(undefined)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
|
@ -171,12 +171,12 @@ function WithdrawLiquidityPanel(props: {
|
|||
setIsSuccess(false)
|
||||
|
||||
withdrawLiquidity({ contractId: contract.id })
|
||||
.then((r) => {
|
||||
.then((_) => {
|
||||
setIsSuccess(true)
|
||||
setError(undefined)
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch((e) => setError('Server error'))
|
||||
.catch((_) => setError('Server error'))
|
||||
}
|
||||
|
||||
if (isSuccess)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import Link from 'next/link'
|
||||
import { firebaseLogout, User } from 'web/lib/firebase/users'
|
||||
import { User } from 'web/lib/firebase/users'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { Avatar } from '../avatar'
|
||||
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
||||
|
||||
export function ProfileSummary(props: { user: User }) {
|
||||
const { user } = props
|
||||
|
|
|
@ -7,15 +7,12 @@ import {
|
|||
CashIcon,
|
||||
HeartIcon,
|
||||
PresentationChartLineIcon,
|
||||
ChatAltIcon,
|
||||
SparklesIcon,
|
||||
NewspaperIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import clsx from 'clsx'
|
||||
import { sortBy } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useFollowedFolds } from 'web/hooks/use-fold'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { firebaseLogin, firebaseLogout, User } from 'web/lib/firebase/users'
|
||||
import { ManifoldLogo } from './manifold-logo'
|
||||
|
@ -51,7 +48,7 @@ function getNavigation(username: string) {
|
|||
icon: NotificationsIcon,
|
||||
},
|
||||
|
||||
{ name: 'Charity', href: '/charity', icon: HeartIcon },
|
||||
{ name: 'Get M$', href: '/add-funds', icon: CashIcon },
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -63,14 +60,15 @@ function getMoreNavigation(user?: User | null) {
|
|||
if (!user) {
|
||||
return [
|
||||
{ name: 'Leaderboards', href: '/leaderboards' },
|
||||
{ name: 'Charity', href: '/charity' },
|
||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{ name: 'Add funds', href: '/add-funds' },
|
||||
{ name: 'Leaderboards', href: '/leaderboards' },
|
||||
{ name: 'Charity', href: '/charity' },
|
||||
{ name: 'Blog', href: 'https://news.manifold.markets' },
|
||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
|
||||
|
@ -104,7 +102,7 @@ const signedOutMobileNavigation = [
|
|||
]
|
||||
|
||||
const mobileNavigation = [
|
||||
{ name: 'Add funds', href: '/add-funds', icon: CashIcon },
|
||||
{ name: 'Get M$', href: '/add-funds', icon: CashIcon },
|
||||
...signedOutMobileNavigation,
|
||||
]
|
||||
|
||||
|
@ -179,8 +177,6 @@ export default function Sidebar(props: { className?: string }) {
|
|||
}, [])
|
||||
|
||||
const user = useUser()
|
||||
let folds = useFollowedFolds(user) || []
|
||||
folds = sortBy(folds, 'followCount').reverse()
|
||||
const mustWaitForFreeMarketStatus = useHasCreatedContractToday(user)
|
||||
const navigationOptions =
|
||||
user === null
|
||||
|
|
|
@ -53,7 +53,6 @@ function NumericBuyPanel(props: {
|
|||
|
||||
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
||||
|
||||
const [valueError, setValueError] = useState<string | undefined>()
|
||||
const [error, setError] = useState<string | undefined>()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [wasSubmitted, setWasSubmitted] = useState(false)
|
||||
|
|
|
@ -6,12 +6,11 @@ import { Toaster } from 'react-hot-toast'
|
|||
|
||||
export function Page(props: {
|
||||
margin?: boolean
|
||||
assertUser?: 'signed-in' | 'signed-out'
|
||||
rightSidebar?: ReactNode
|
||||
suspend?: boolean
|
||||
children?: ReactNode
|
||||
}) {
|
||||
const { margin, assertUser, children, rightSidebar, suspend } = props
|
||||
const { margin, children, rightSidebar, suspend } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import clsx from 'clsx'
|
||||
import { uniq } from 'lodash'
|
||||
import { LinkIcon } from '@heroicons/react/solid'
|
||||
import { PencilIcon } from '@heroicons/react/outline'
|
||||
|
||||
import { follow, unfollow, User } from 'web/lib/firebase/users'
|
||||
import { CreatorContractsList } from './contract/contracts-list'
|
||||
import { SEO } from './SEO'
|
||||
|
@ -9,9 +13,7 @@ import { Col } from './layout/col'
|
|||
import { Linkify } from './linkify'
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { Row } from './layout/row'
|
||||
import { LinkIcon } from '@heroicons/react/solid'
|
||||
import { genHash } from 'common/util/random'
|
||||
import { PencilIcon } from '@heroicons/react/outline'
|
||||
import { Tabs } from './layout/tabs'
|
||||
import { UserCommentsList } from './comments-list'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
@ -22,8 +24,10 @@ import { LoadingIndicator } from './loading-indicator'
|
|||
import { BetsList } from './bets-list'
|
||||
import { Bet } from 'common/bet'
|
||||
import { getUserBets } from 'web/lib/firebase/bets'
|
||||
import { uniq } from 'lodash'
|
||||
import { FollowersButton, FollowingButton } from './following-button'
|
||||
import { AlertBox } from './alert-box'
|
||||
import { useFollows } from 'web/hooks/use-follows'
|
||||
import { FollowButton } from './follow-button'
|
||||
|
||||
export function UserLink(props: {
|
||||
name: string
|
||||
|
@ -305,29 +309,3 @@ export function defaultBannerUrl(userId: string) {
|
|||
]
|
||||
return defaultBanner[genHash(userId)() % defaultBanner.length]
|
||||
}
|
||||
|
||||
import { ExclamationIcon } from '@heroicons/react/solid'
|
||||
import { FollowButton } from './follow-button'
|
||||
import { useFollows } from 'web/hooks/use-follows'
|
||||
|
||||
function AlertBox(props: { title: string; text: string }) {
|
||||
const { title, text } = props
|
||||
return (
|
||||
<div className="rounded-md bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<ExclamationIcon
|
||||
className="h-5 w-5 text-yellow-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-yellow-800">{title}</h3>
|
||||
<div className="mt-2 text-sm text-yellow-700">
|
||||
<Linkify text={text} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -168,7 +168,7 @@ export function FundsSelector(props: {
|
|||
{fundAmounts.map((amount) => (
|
||||
<Button
|
||||
key={amount}
|
||||
color={selected === amount ? 'green' : 'gray'}
|
||||
color={selected === amount ? 'indigo' : 'gray'}
|
||||
onClick={() => onSelect(amount as any)}
|
||||
className={btnClassName}
|
||||
>
|
||||
|
@ -230,7 +230,7 @@ export function NumberCancelSelector(props: {
|
|||
function Button(props: {
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
color: 'green' | 'red' | 'blue' | 'yellow' | 'gray'
|
||||
color: 'green' | 'red' | 'blue' | 'indigo' | 'yellow' | 'gray'
|
||||
children?: ReactNode
|
||||
}) {
|
||||
const { className, onClick, children, color } = props
|
||||
|
@ -244,6 +244,7 @@ function Button(props: {
|
|||
color === 'red' && 'bg-red-400 text-white hover:bg-red-500',
|
||||
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
|
||||
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
|
||||
color === 'indigo' && 'bg-indigo-500 text-white hover:bg-indigo-600',
|
||||
color === 'gray' && 'bg-gray-200 text-gray-700 hover:bg-gray-300',
|
||||
className
|
||||
)}
|
||||
|
|
|
@ -18,10 +18,12 @@ export const useContract = (contractId: string) => {
|
|||
return result.isLoading ? undefined : result.data
|
||||
}
|
||||
|
||||
export const useContractWithPreload = (initial: Contract | null) => {
|
||||
const [contract, setContract] = useStateCheckEquality<Contract | null>(
|
||||
initial
|
||||
)
|
||||
export const useContractWithPreload = (
|
||||
initial: Contract | null | undefined
|
||||
) => {
|
||||
const [contract, setContract] = useStateCheckEquality<
|
||||
Contract | null | undefined
|
||||
>(initial)
|
||||
const contractId = initial?.id
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { isEqual, sortBy } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Fold } from 'common/fold'
|
||||
import { User } from 'common/user'
|
||||
import {
|
||||
listAllFolds,
|
||||
listenForFold,
|
||||
listenForFolds,
|
||||
listenForFoldsWithTags,
|
||||
|
@ -80,38 +78,3 @@ export const useFollowedFoldIds = (user: User | null | undefined) => {
|
|||
|
||||
return followedFoldIds
|
||||
}
|
||||
|
||||
// We also cache followedFolds directly in JSON.
|
||||
// TODO: Extract out localStorage caches to a utility
|
||||
export const useFollowedFolds = (user: User | null | undefined) => {
|
||||
const [followedFolds, setFollowedFolds] = useState<Fold[] | undefined>()
|
||||
const ids = useFollowedFoldIds(user)
|
||||
|
||||
useEffect(() => {
|
||||
if (user && ids) {
|
||||
const key = `followed-full-folds-${user.id}`
|
||||
const followedFoldJson = localStorage.getItem(key)
|
||||
if (followedFoldJson) {
|
||||
setFollowedFolds(JSON.parse(followedFoldJson))
|
||||
// Exit early if ids and followedFoldIds have all the same elements.
|
||||
if (
|
||||
isEqual(
|
||||
sortBy(ids),
|
||||
sortBy(JSON.parse(followedFoldJson).map((f: Fold) => f.id))
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, fetch the full contents of all folds
|
||||
listAllFolds().then((folds) => {
|
||||
const followedFolds = folds.filter((fold) => ids.includes(fold.id))
|
||||
setFollowedFolds(followedFolds)
|
||||
localStorage.setItem(key, JSON.stringify(followedFolds))
|
||||
})
|
||||
}
|
||||
}, [user, ids])
|
||||
|
||||
return followedFolds
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { collection, query, where } from 'firebase/firestore'
|
||||
import { Notification } from 'common/notification'
|
||||
import { db } from 'web/lib/firebase/init'
|
||||
import { getValues, listenForValues } from 'web/lib/firebase/utils'
|
||||
import { listenForValues } from 'web/lib/firebase/utils'
|
||||
|
||||
function getNotificationsQuery(userId: string, unseenOnly?: boolean) {
|
||||
const notifsCollection = collection(db, `/users/${userId}/notifications`)
|
||||
|
|
|
@ -39,6 +39,7 @@ import { FeedBet } from 'web/components/feed/feed-bets'
|
|||
import { useIsIframe } from 'web/hooks/use-is-iframe'
|
||||
import ContractEmbedPage from '../embed/[username]/[contractSlug]'
|
||||
import { useBets } from 'web/hooks/use-bets'
|
||||
import { AlertBox } from 'web/components/alert-box'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz(props: {
|
||||
|
@ -86,13 +87,13 @@ export default function ContractPage(props: {
|
|||
slug: '',
|
||||
}
|
||||
|
||||
const contract = useContractWithPreload(props.contract)
|
||||
|
||||
const inIframe = useIsIframe()
|
||||
if (inIframe) {
|
||||
return <ContractEmbedPage {...props} />
|
||||
}
|
||||
|
||||
const { contract } = props
|
||||
|
||||
if (!contract) {
|
||||
return <Custom404 />
|
||||
}
|
||||
|
@ -103,7 +104,9 @@ export default function ContractPage(props: {
|
|||
export function ContractPageContent(
|
||||
props: Parameters<typeof ContractPage>[0] & { contract: Contract }
|
||||
) {
|
||||
const { contract, backToHome, comments } = props
|
||||
const { backToHome, comments } = props
|
||||
|
||||
const contract = useContractWithPreload(props.contract) ?? props.contract
|
||||
|
||||
const bets = useBets(contract.id) ?? props.bets
|
||||
// Sort for now to see if bug is fixed.
|
||||
|
@ -187,6 +190,12 @@ export function ContractPageContent(
|
|||
bets={bets}
|
||||
comments={comments ?? []}
|
||||
/>
|
||||
{isNumeric && (
|
||||
<AlertBox
|
||||
title="Warning"
|
||||
text="Numeric markets were introduced as an experimental feature and are now deprecated."
|
||||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'FREE_RESPONSE' && (
|
||||
<>
|
||||
|
|
|
@ -37,7 +37,7 @@ export default function Activity() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Page assertUser="signed-in" suspend={!!contract}>
|
||||
<Page suspend={!!contract}>
|
||||
<Col className="mx-auto w-full max-w-[700px]">
|
||||
<CategorySelector category={category} setCategory={setCategory} />
|
||||
<Spacer h={1} />
|
||||
|
|
|
@ -16,7 +16,11 @@ export default function AddFundsPage() {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
<SEO title="Add funds" description="Add funds" url="/add-funds" />
|
||||
<SEO
|
||||
title="Get Manifold Dollars"
|
||||
description="Get Manifold Dollars"
|
||||
url="/add-funds"
|
||||
/>
|
||||
|
||||
<Col className="items-center">
|
||||
<Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md">
|
||||
|
@ -29,8 +33,8 @@ export default function AddFundsPage() {
|
|||
/>
|
||||
|
||||
<div className="mb-6 text-gray-500">
|
||||
Use Manifold Dollars to trade in your favorite markets. <br /> (Not
|
||||
redeemable for cash.)
|
||||
Purchase Manifold Dollars to trade in your favorite markets. <br />{' '}
|
||||
(Not redeemable for cash.)
|
||||
</div>
|
||||
|
||||
<div className="mb-2 text-sm text-gray-500">Amount</div>
|
||||
|
@ -54,7 +58,7 @@ export default function AddFundsPage() {
|
|||
>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full bg-gradient-to-r from-teal-500 to-green-500 font-medium hover:from-teal-600 hover:to-green-600"
|
||||
className="btn btn-primary w-full bg-gradient-to-r from-indigo-500 to-blue-500 font-medium hover:from-indigo-600 hover:to-blue-600"
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
|
|
|
@ -19,6 +19,7 @@ export default async function handler(
|
|||
listAllComments(contractId),
|
||||
])
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const bets = allBets.map(({ userId, ...bet }) => bet) as Exclude<
|
||||
Bet,
|
||||
'userId'
|
||||
|
|
|
@ -24,6 +24,7 @@ export default async function handler(
|
|||
listAllComments(contract.id),
|
||||
])
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const bets = allBets.map(({ userId, ...bet }) => bet) as Exclude<
|
||||
Bet,
|
||||
'userId'
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { mapValues, groupBy, sumBy, sum, sortBy, debounce } from 'lodash'
|
||||
import {
|
||||
mapValues,
|
||||
groupBy,
|
||||
sumBy,
|
||||
sum,
|
||||
sortBy,
|
||||
debounce,
|
||||
uniqBy,
|
||||
} from 'lodash'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { charities, Charity as CharityType } from 'common/charity'
|
||||
import { CharityCard } from 'web/components/charity/charity-card'
|
||||
|
@ -8,7 +16,9 @@ import { Page } from 'web/components/page'
|
|||
import { SiteLink } from 'web/components/site-link'
|
||||
import { Title } from 'web/components/title'
|
||||
import { getAllCharityTxns } from 'web/lib/firebase/txns'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { manaToUSD } from 'common/util/format'
|
||||
import { quadraticMatches } from 'common/quadratic-funding'
|
||||
import { Txn } from 'common/txn'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const txns = await getAllCharityTxns()
|
||||
|
@ -20,21 +30,55 @@ export async function getStaticProps() {
|
|||
(charity) => (charity.tags?.includes('Featured') ? 0 : 1),
|
||||
(charity) => -totals[charity.id],
|
||||
])
|
||||
const matches = quadraticMatches(txns, totalRaised)
|
||||
const numDonors = uniqBy(txns, (txn) => txn.fromId).length
|
||||
|
||||
return {
|
||||
props: {
|
||||
totalRaised,
|
||||
charities: sortedCharities,
|
||||
matches,
|
||||
txns,
|
||||
numDonors,
|
||||
},
|
||||
revalidate: 60,
|
||||
}
|
||||
}
|
||||
|
||||
type Stat = {
|
||||
name: string
|
||||
stat: string
|
||||
}
|
||||
|
||||
function DonatedStats(props: { stats: Stat[] }) {
|
||||
const { stats } = props
|
||||
return (
|
||||
<dl className="mt-3 grid grid-cols-1 gap-5 rounded-lg bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400 p-4 sm:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6"
|
||||
>
|
||||
<dt className="truncate text-sm font-medium text-gray-500">
|
||||
{item.name}
|
||||
</dt>
|
||||
<dd className="mt-1 text-3xl font-semibold text-gray-900">
|
||||
{item.stat}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Charity(props: {
|
||||
totalRaised: number
|
||||
charities: CharityType[]
|
||||
matches: { [charityId: string]: number }
|
||||
txns: Txn[]
|
||||
numDonors: number
|
||||
}) {
|
||||
const { totalRaised, charities } = props
|
||||
const { totalRaised, charities, matches, numDonors } = props
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
const debouncedQuery = debounce(setQuery, 50)
|
||||
|
@ -54,26 +98,51 @@ export default function Charity(props: {
|
|||
return (
|
||||
<Page>
|
||||
<Col className="w-full rounded px-4 py-6 sm:px-8 xl:w-[125%]">
|
||||
<Col className="max-w-xl gap-2">
|
||||
<Col className="">
|
||||
<Title className="!mt-0" text="Manifold for Charity" />
|
||||
<div className="mb-6 text-gray-500">
|
||||
Donate your winnings to charity! Every {formatMoney(100)} you give
|
||||
turns into $1 USD we send to your chosen charity.
|
||||
<Spacer h={5} />
|
||||
Together we've donated over ${Math.floor(totalRaised / 100)} USD so
|
||||
far!
|
||||
</div>
|
||||
<span className="text-gray-600">
|
||||
Through July 15, up to $25k of donations will be matched via{' '}
|
||||
<SiteLink href="https://wtfisqf.com/" className="font-bold">
|
||||
quadratic funding
|
||||
</SiteLink>
|
||||
, courtesy of{' '}
|
||||
<SiteLink href="https://ftxfuturefund.org/" className="font-bold">
|
||||
the FTX Future Fund
|
||||
</SiteLink>
|
||||
!
|
||||
</span>
|
||||
<DonatedStats
|
||||
stats={[
|
||||
{
|
||||
name: 'Raised by Manifold users',
|
||||
stat: manaToUSD(totalRaised),
|
||||
},
|
||||
{
|
||||
name: 'Number of donors',
|
||||
stat: `${numDonors}`,
|
||||
},
|
||||
{
|
||||
name: 'Matched via quadratic funding',
|
||||
stat: manaToUSD(sum(Object.values(matches))),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Spacer h={10} />
|
||||
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => debouncedQuery(e.target.value)}
|
||||
placeholder="Search charities"
|
||||
placeholder="Find a charity"
|
||||
className="input input-bordered mb-6 w-full"
|
||||
/>
|
||||
</Col>
|
||||
<div className="grid max-w-xl grid-flow-row grid-cols-1 gap-4 lg:max-w-full lg:grid-cols-2 xl:grid-cols-3">
|
||||
{filterCharities.map((charity) => (
|
||||
<CharityCard charity={charity} key={charity.name} />
|
||||
<CharityCard
|
||||
charity={charity}
|
||||
key={charity.name}
|
||||
match={matches[charity.id]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{filterCharities.length === 0 && (
|
||||
|
@ -82,32 +151,28 @@ export default function Charity(props: {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<iframe
|
||||
height="405"
|
||||
src="https://manifold.markets/embed/ManifoldMarkets/total-donations-for-manifold-for-go"
|
||||
title="Total donations for Manifold for Charity this May (in USD)"
|
||||
frameBorder="0"
|
||||
className="m-10 w-full rounded-xl bg-white p-10"
|
||||
></iframe>
|
||||
<div className="mt-10 w-full rounded-xl bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400 p-5">
|
||||
<iframe
|
||||
height="405"
|
||||
src="https://manifold.markets/ManifoldMarkets/how-much-will-be-donated-through-ma"
|
||||
title="Total donations for Manifold for Charity this May (in USD)"
|
||||
frameBorder="0"
|
||||
className="w-full rounded-xl bg-white p-10"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 text-gray-500">
|
||||
Don't see your favorite charity? Recommend it{' '}
|
||||
<SiteLink
|
||||
href="https://manifold.markets/Sinclair/which-charities-should-manifold-add"
|
||||
className="text-indigo-700"
|
||||
>
|
||||
here
|
||||
</SiteLink>
|
||||
!
|
||||
<span className="font-semibold">Notes</span>
|
||||
<br />
|
||||
- Don't see your favorite charity? Recommend it by emailing
|
||||
charity@manifold.markets!
|
||||
<br />
|
||||
<span className="italic">
|
||||
Note: Manifold is not affiliated with non-Featured charities; we're
|
||||
just fans of their work!
|
||||
<br />
|
||||
As Manifold is a for-profit entity, your contributions will not be
|
||||
tax deductible.
|
||||
</span>
|
||||
- Manifold is not affiliated with non-Featured charities; we're just
|
||||
fans of their work.
|
||||
<br />
|
||||
- As Manifold itself is a for-profit entity, your contributions will
|
||||
not be tax deductible.
|
||||
<br />- Donations + matches are wired once each quarter.
|
||||
</div>
|
||||
</Col>
|
||||
</Page>
|
||||
|
|
|
@ -56,8 +56,8 @@ export default function Create() {
|
|||
}
|
||||
|
||||
// Allow user to create a new contract
|
||||
export function NewContract(props: { question: string; tag?: string }) {
|
||||
const { question, tag } = props
|
||||
export function NewContract(props: { question: string }) {
|
||||
const { question } = props
|
||||
const creator = useUser()
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -74,7 +74,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
||||
// const tags = parseWordsAsTags(tagText)
|
||||
|
||||
const [ante, setAnte] = useState(FIXED_ANTE)
|
||||
const [ante, _setAnte] = useState(FIXED_ANTE)
|
||||
|
||||
const mustWaitForDailyFreeMarketStatus = useHasCreatedContractToday(creator)
|
||||
|
||||
|
@ -348,7 +348,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
className="btn btn-xs btn-primary"
|
||||
onClick={() => (window.location.href = '/add-funds')}
|
||||
>
|
||||
Add funds
|
||||
Get M$
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -27,10 +27,8 @@ import { EditFoldButton } from 'web/components/folds/edit-fold-button'
|
|||
import Custom404 from '../../404'
|
||||
import { FollowFoldButton } from 'web/components/folds/follow-fold-button'
|
||||
import { SEO } from 'web/components/SEO'
|
||||
import { useTaggedContracts } from 'web/hooks/use-contracts'
|
||||
import { Linkify } from 'web/components/linkify'
|
||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||
import { filterDefined } from 'common/util/array'
|
||||
import { findActiveContracts } from 'web/components/feed/find-active-contracts'
|
||||
import { Tabs } from 'web/components/layout/tabs'
|
||||
|
||||
|
@ -133,15 +131,6 @@ export default function FoldPage(props: {
|
|||
const user = useUser()
|
||||
const isCurator = user && fold && user.id === fold.curatorId
|
||||
|
||||
const taggedContracts = useTaggedContracts(fold?.tags) ?? props.contracts
|
||||
const contractsMap = Object.fromEntries(
|
||||
taggedContracts.map((contract) => [contract.id, contract])
|
||||
)
|
||||
|
||||
const contracts = filterDefined(
|
||||
props.contracts.map((contract) => contractsMap[contract.id])
|
||||
)
|
||||
|
||||
if (fold === null || !foldSubpages.includes(page) || slugs[2]) {
|
||||
return <Custom404 />
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ const Home = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Page assertUser="signed-in" suspend={!!contract}>
|
||||
<Page suspend={!!contract}>
|
||||
<Col className="mx-auto w-full p-2">
|
||||
<ContractSearch
|
||||
querySortOptions={{
|
||||
|
|
|
@ -3,7 +3,7 @@ import Router from 'next/router'
|
|||
|
||||
import { Contract, getContractsBySlugs } from 'web/lib/firebase/contracts'
|
||||
import { Page } from 'web/components/page'
|
||||
import { FeedPromo } from 'web/components/feed-create'
|
||||
import { LandingPagePanel } from 'web/components/landing-page-panel'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { ManifoldLogo } from 'web/components/nav/manifold-logo'
|
||||
|
@ -40,13 +40,13 @@ const Home = (props: { hotContracts: Contract[] }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Page assertUser="signed-out">
|
||||
<Page>
|
||||
<div className="px-4 pt-2 md:mt-0 lg:hidden">
|
||||
<ManifoldLogo />
|
||||
</div>
|
||||
<Col className="items-center">
|
||||
<Col className="max-w-3xl">
|
||||
<FeedPromo hotContracts={hotContracts ?? []} />
|
||||
<LandingPagePanel hotContracts={hotContracts ?? []} />
|
||||
{/* <p className="mt-6 text-gray-500">
|
||||
View{' '}
|
||||
<SiteLink href="/markets" className="font-bold text-gray-700">
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
LightningBoltIcon,
|
||||
ScaleIcon,
|
||||
UserCircleIcon,
|
||||
BeakerIcon,
|
||||
ArrowDownIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
|
||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||
import { ContractsGrid } from 'web/components/contract/contracts-list'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import Link from 'next/link'
|
||||
import { Contract } from 'web/lib/firebase/contracts'
|
||||
|
||||
export default function LandingPage(props: { hotContracts: Contract[] }) {
|
||||
const { hotContracts } = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Hero />
|
||||
<FeaturesSection />
|
||||
{/* <ExploreMarketsSection hotContracts={hotContracts} /> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const scrollToAbout = () => {
|
||||
const aboutElem = document.getElementById('about')
|
||||
window.scrollTo({ top: aboutElem?.offsetTop, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
function Hero() {
|
||||
return (
|
||||
<div className="bg-world-trading h-screen overflow-hidden bg-gray-900 bg-cover bg-center lg:bg-left">
|
||||
<main>
|
||||
<div className="pt-32 sm:pt-8 lg:overflow-hidden lg:pt-0 lg:pb-14">
|
||||
<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:flex lg:items-center lg:px-0 lg:text-left">
|
||||
<div className="lg:py-24">
|
||||
<h1 className="mt-4 text-4xl text-white sm:mt-5 sm:text-6xl lg:mt-6 xl:text-6xl">
|
||||
<div className="mb-2">Create your own</div>
|
||||
<div className="bg-gradient-to-r from-teal-300 to-green-400 bg-clip-text font-bold text-transparent">
|
||||
prediction markets
|
||||
</div>
|
||||
</h1>
|
||||
<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
|
||||
</p>
|
||||
<div className="mt-10 sm:mt-12">
|
||||
<button
|
||||
className="btn bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
||||
onClick={firebaseLogin}
|
||||
>
|
||||
Sign in to get started!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-12 w-full">
|
||||
<ArrowDownIcon
|
||||
className="mx-auto animate-bounce cursor-pointer text-white"
|
||||
width={32}
|
||||
height={32}
|
||||
onClick={scrollToAbout}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FeaturesSection() {
|
||||
const features = [
|
||||
{
|
||||
name: 'Easy to participate',
|
||||
description: 'Sign up for free and make your own predictions in seconds!',
|
||||
icon: UserCircleIcon,
|
||||
},
|
||||
{
|
||||
name: 'Play money, real results',
|
||||
description:
|
||||
'Get accurate predictions by betting with Manifold Dollars, our virtual currency.',
|
||||
icon: LightningBoltIcon,
|
||||
},
|
||||
{
|
||||
name: 'Creator-driven markets',
|
||||
description:
|
||||
'Resolve markets you create with your own judgment—enabling new markets with subjective or personal questions.',
|
||||
icon: ScaleIcon,
|
||||
},
|
||||
{
|
||||
name: 'Become smarter',
|
||||
description:
|
||||
'Bet on questions that matter and share the forecasts. With better information, we can all make better decisions.',
|
||||
icon: BeakerIcon,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div id="about" className="w-full bg-green-50 py-16">
|
||||
<div className="mx-auto max-w-4xl py-12">
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div className="lg:text-center">
|
||||
<h2 className="text-base font-semibold uppercase tracking-wide text-teal-600">
|
||||
Manifold Markets
|
||||
</h2>
|
||||
<p className="mt-2 text-3xl font-extrabold leading-8 tracking-tight text-gray-900 sm:text-4xl">
|
||||
Better forecasting for everyone
|
||||
</p>
|
||||
<p className="mt-4 max-w-2xl text-xl text-gray-500 lg:mx-auto">
|
||||
The easiest way to get an accurate forecast on anything
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-10">
|
||||
<dl className="space-y-10 md:grid md:grid-cols-2 md:gap-x-8 md:gap-y-10 md:space-y-0">
|
||||
{features.map((feature) => (
|
||||
<div key={feature.name} className="relative">
|
||||
<dt>
|
||||
<div className="absolute flex h-12 w-12 items-center justify-center rounded-md bg-teal-500 text-white">
|
||||
<feature.icon className="h-6 w-6" aria-hidden="true" />
|
||||
</div>
|
||||
<p className="ml-16 text-lg font-medium leading-6 text-gray-900">
|
||||
{feature.name}
|
||||
</p>
|
||||
</dt>
|
||||
<dd className="mt-2 ml-16 text-base text-gray-500">
|
||||
{feature.description}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Col className="mt-20">
|
||||
<Link href="/about">
|
||||
<a className="btn btn-primary mx-auto">Learn more</a>
|
||||
</Link>
|
||||
</Col>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ExploreMarketsSection(props: { hotContracts: Contract[] }) {
|
||||
const { hotContracts } = props
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl px-4 py-8">
|
||||
<p className="my-12 text-3xl font-extrabold leading-8 tracking-tight text-indigo-700 sm:text-4xl">
|
||||
Today's top markets
|
||||
</p>
|
||||
|
||||
<ContractsGrid
|
||||
contracts={hotContracts}
|
||||
loadMore={() => {}}
|
||||
hasMore={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -8,8 +8,8 @@ import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
|||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz() {
|
||||
const [topTraders, topCreators] = await Promise.all([
|
||||
getTopTraders().catch((_) => {}),
|
||||
getTopCreators().catch((_) => {}),
|
||||
getTopTraders().catch(() => {}),
|
||||
getTopCreators().catch(() => {}),
|
||||
])
|
||||
|
||||
return {
|
||||
|
|
|
@ -290,13 +290,3 @@ ${TEST_VALUE}
|
|||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
// Given a date string like '2022-04-02',
|
||||
// return the time just before midnight on that date (in the user's local time), as millis since epoch
|
||||
function dateToMillis(date: string) {
|
||||
return dayjs(date)
|
||||
.set('hour', 23)
|
||||
.set('minute', 59)
|
||||
.set('second', 59)
|
||||
.valueOf()
|
||||
}
|
||||
|
|
|
@ -31,9 +31,7 @@ import { Linkify } from 'web/components/linkify'
|
|||
import {
|
||||
BinaryOutcomeLabel,
|
||||
CancelLabel,
|
||||
FreeResponseOutcomeLabel,
|
||||
MultiLabel,
|
||||
OutcomeLabel,
|
||||
ProbPercentLabel,
|
||||
} from 'web/components/outcome-label'
|
||||
import {
|
||||
|
@ -44,7 +42,7 @@ import {
|
|||
import { getContractFromId } from 'web/lib/firebase/contracts'
|
||||
import { CheckIcon, XIcon } from '@heroicons/react/outline'
|
||||
import toast from 'react-hot-toast'
|
||||
import { formatMoney, formatPercent } from 'common/util/format'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
|
||||
export default function Notifications() {
|
||||
const user = useUser()
|
||||
|
@ -184,7 +182,7 @@ function NotificationGroupItem(props: {
|
|||
className?: string
|
||||
}) {
|
||||
const { notificationGroup, className } = props
|
||||
const { sourceContractId, notifications, timePeriod } = notificationGroup
|
||||
const { sourceContractId, notifications } = notificationGroup
|
||||
const {
|
||||
sourceContractTitle,
|
||||
sourceContractSlug,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import dayjs from 'dayjs'
|
||||
import { zip, uniq, sumBy, concat, countBy, sortBy, sum } from 'lodash'
|
||||
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
||||
import {
|
||||
DailyCountChart,
|
||||
DailyPercentChart,
|
||||
|
@ -158,13 +157,11 @@ export async function getStaticPropz() {
|
|||
bets?.map((b) => b.userId) ?? [],
|
||||
comments?.map((c) => c.userId) ?? []
|
||||
)
|
||||
const counts = Object.entries(countBy(userIds))
|
||||
const topTenth = sortBy(counts, ([, count]) => count)
|
||||
.reverse()
|
||||
// Take the top 10% of users, except for the top 2, to avoid outliers.
|
||||
.slice(2, counts.length * 0.1)
|
||||
const topTenthTotal = sumBy(topTenth, ([_, count]) => count)
|
||||
return topTenthTotal
|
||||
const counts = Object.values(countBy(userIds))
|
||||
const sortedCounts = sortBy(counts, (count) => count).reverse()
|
||||
if (sortedCounts.length === 0) return 0
|
||||
const tenthPercentile = sortedCounts[Math.floor(sortedCounts.length * 0.1)]
|
||||
return tenthPercentile
|
||||
})
|
||||
const weeklyTopTenthActions = dailyTopTenthActions.map((_, i) => {
|
||||
const start = Math.max(0, i - 6)
|
||||
|
@ -213,9 +210,11 @@ export async function getStaticPropz() {
|
|||
weekOnWeekRetention,
|
||||
weeklyActivationRate,
|
||||
monthlyRetention,
|
||||
dailyTopTenthActions,
|
||||
weeklyTopTenthActions,
|
||||
monthlyTopTenthActions,
|
||||
topTenthActions: {
|
||||
daily: dailyTopTenthActions,
|
||||
weekly: weeklyTopTenthActions,
|
||||
monthly: monthlyTopTenthActions,
|
||||
},
|
||||
manaBet: {
|
||||
daily: dailyManaBet,
|
||||
weekly: weeklyManaBet,
|
||||
|
@ -238,9 +237,11 @@ export default function Analytics(props: {
|
|||
weekOnWeekRetention: number[]
|
||||
monthlyRetention: number[]
|
||||
weeklyActivationRate: number[]
|
||||
dailyTopTenthActions: number[]
|
||||
weeklyTopTenthActions: number[]
|
||||
monthlyTopTenthActions: number[]
|
||||
topTenthActions: {
|
||||
daily: number[]
|
||||
weekly: number[]
|
||||
monthly: number[]
|
||||
}
|
||||
manaBet: {
|
||||
daily: number[]
|
||||
weekly: number[]
|
||||
|
@ -259,9 +260,11 @@ export default function Analytics(props: {
|
|||
weekOnWeekRetention: [],
|
||||
monthlyRetention: [],
|
||||
weeklyActivationRate: [],
|
||||
dailyTopTenthActions: [],
|
||||
weeklyTopTenthActions: [],
|
||||
monthlyTopTenthActions: [],
|
||||
topTenthActions: {
|
||||
daily: [],
|
||||
weekly: [],
|
||||
monthly: [],
|
||||
},
|
||||
manaBet: {
|
||||
daily: [],
|
||||
weekly: [],
|
||||
|
@ -302,9 +305,11 @@ export function CustomAnalytics(props: {
|
|||
weekOnWeekRetention: number[]
|
||||
monthlyRetention: number[]
|
||||
weeklyActivationRate: number[]
|
||||
dailyTopTenthActions: number[]
|
||||
weeklyTopTenthActions: number[]
|
||||
monthlyTopTenthActions: number[]
|
||||
topTenthActions: {
|
||||
daily: number[]
|
||||
weekly: number[]
|
||||
monthly: number[]
|
||||
}
|
||||
manaBet: {
|
||||
daily: number[]
|
||||
weekly: number[]
|
||||
|
@ -322,9 +327,7 @@ export function CustomAnalytics(props: {
|
|||
weekOnWeekRetention,
|
||||
monthlyRetention,
|
||||
weeklyActivationRate,
|
||||
dailyTopTenthActions,
|
||||
weeklyTopTenthActions,
|
||||
monthlyTopTenthActions,
|
||||
topTenthActions,
|
||||
manaBet,
|
||||
} = props
|
||||
|
||||
|
@ -526,10 +529,10 @@ export function CustomAnalytics(props: {
|
|||
/>
|
||||
<Spacer h={8} />
|
||||
|
||||
<Title text="Total actions by top tenth" />
|
||||
<Title text="Action count of top tenth" />
|
||||
<p className="text-gray-500">
|
||||
From the top 10% of users, how many bets, comments, and markets did they
|
||||
create? (Excluding top 2 users each day.)
|
||||
Number of actions (bets, comments, markets created) taken by the tenth
|
||||
percentile of top users.
|
||||
</p>
|
||||
<Tabs
|
||||
defaultIndex={1}
|
||||
|
@ -538,7 +541,7 @@ export function CustomAnalytics(props: {
|
|||
title: 'Daily',
|
||||
content: (
|
||||
<DailyCountChart
|
||||
dailyCounts={dailyTopTenthActions}
|
||||
dailyCounts={topTenthActions.daily}
|
||||
startDate={startDate}
|
||||
small
|
||||
/>
|
||||
|
@ -548,7 +551,7 @@ export function CustomAnalytics(props: {
|
|||
title: 'Weekly',
|
||||
content: (
|
||||
<DailyCountChart
|
||||
dailyCounts={weeklyTopTenthActions}
|
||||
dailyCounts={topTenthActions.weekly}
|
||||
startDate={startDate}
|
||||
small
|
||||
/>
|
||||
|
@ -558,7 +561,7 @@ export function CustomAnalytics(props: {
|
|||
title: 'Monthly',
|
||||
content: (
|
||||
<DailyCountChart
|
||||
dailyCounts={monthlyTopTenthActions}
|
||||
dailyCounts={topTenthActions.monthly}
|
||||
startDate={startDate}
|
||||
small
|
||||
/>
|
||||
|
|
|
@ -6,8 +6,8 @@ import { Title } from '../../components/title'
|
|||
export default function TagPage() {
|
||||
const router = useRouter()
|
||||
const { tag } = router.query as { tag: string }
|
||||
if (!router.isReady) return <div />
|
||||
|
||||
// TODO: Fix error: The provided `href` (/tag/[tag]?s=newest) value is missing query values (tag)
|
||||
return (
|
||||
<Page>
|
||||
<Title text={`#${tag}`} />
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 212 KiB |
Loading…
Reference in New Issue
Block a user