Merge branch 'main' into show-comments-position
This commit is contained in:
commit
3e600f45b5
|
@ -1,7 +1,6 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { chargeUser, getUser } from './utils'
|
import { chargeUser, getUser } from './utils'
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
|
@ -73,11 +72,19 @@ export const createContract = functions
|
||||||
return { status: 'error', message: 'Invalid initial probability' }
|
return { status: 'error', message: 'Invalid initial probability' }
|
||||||
|
|
||||||
const ante = FIXED_ANTE // data.ante
|
const ante = FIXED_ANTE // data.ante
|
||||||
|
// uses utc time on server:
|
||||||
|
const today = new Date().setHours(0, 0, 0, 0)
|
||||||
|
const userContractsCreatedTodaySnapshot = await firestore
|
||||||
|
.collection(`contracts`)
|
||||||
|
.where('creatorId', '==', userId)
|
||||||
|
.where('createdTime', '>=', today)
|
||||||
|
.get()
|
||||||
|
const isFree = userContractsCreatedTodaySnapshot.size === 0
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ante === undefined ||
|
ante === undefined ||
|
||||||
ante < MINIMUM_ANTE ||
|
ante < MINIMUM_ANTE ||
|
||||||
ante > creator.balance ||
|
(ante > creator.balance && !isFree) ||
|
||||||
isNaN(ante) ||
|
isNaN(ante) ||
|
||||||
!isFinite(ante)
|
!isFinite(ante)
|
||||||
)
|
)
|
||||||
|
@ -109,7 +116,7 @@ export const createContract = functions
|
||||||
tags ?? []
|
tags ?? []
|
||||||
)
|
)
|
||||||
|
|
||||||
if (ante) await chargeUser(creator.id, ante)
|
if (!isFree && ante) await chargeUser(creator.id, ante)
|
||||||
|
|
||||||
await contractRef.create(contract)
|
await contractRef.create(contract)
|
||||||
|
|
||||||
|
|
|
@ -25,15 +25,16 @@ export function ContractsGrid(props: {
|
||||||
showCloseTime?: boolean
|
showCloseTime?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { showCloseTime } = props
|
const { showCloseTime } = props
|
||||||
|
const PAGE_SIZE = 100
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
const [resolvedContracts, activeContracts] = _.partition(
|
const [resolvedContracts, activeContracts] = _.partition(
|
||||||
props.contracts,
|
props.contracts,
|
||||||
(c) => c.isResolved
|
(c) => c.isResolved
|
||||||
)
|
)
|
||||||
const contracts = [...activeContracts, ...resolvedContracts].slice(
|
const allContracts = [...activeContracts, ...resolvedContracts]
|
||||||
0,
|
const showMore = allContracts.length > PAGE_SIZE * page
|
||||||
MAX_CONTRACTS_DISPLAYED
|
const contracts = allContracts.slice(0, PAGE_SIZE * page)
|
||||||
)
|
|
||||||
|
|
||||||
if (contracts.length === 0) {
|
if (contracts.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
@ -47,16 +48,27 @@ export function ContractsGrid(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="grid w-full grid-cols-1 gap-6 md:grid-cols-2">
|
<>
|
||||||
{contracts.map((contract) => (
|
<ul className="grid w-full grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
<ContractCard
|
{contracts.map((contract) => (
|
||||||
contract={contract}
|
<ContractCard
|
||||||
key={contract.id}
|
contract={contract}
|
||||||
// showHotVolume={showHotVolume}
|
key={contract.id}
|
||||||
showCloseTime={showCloseTime}
|
// showHotVolume={showHotVolume}
|
||||||
/>
|
showCloseTime={showCloseTime}
|
||||||
))}
|
/>
|
||||||
</ul>
|
))}
|
||||||
|
</ul>
|
||||||
|
{/* Show a link that increases the page num when clicked */}
|
||||||
|
{showMore && (
|
||||||
|
<button
|
||||||
|
className="btn btn-link float-right normal-case"
|
||||||
|
onClick={() => setPage(page + 1)}
|
||||||
|
>
|
||||||
|
Show more...
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,7 @@ export default function FeedCreate(props: {
|
||||||
{/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
|
{/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
|
||||||
{!isExpanded && (
|
{!isExpanded && (
|
||||||
<div className="flex justify-end sm:-mt-4">
|
<div className="flex justify-end sm:-mt-4">
|
||||||
<button className="btn btn-sm" disabled>
|
<button className="btn btn-sm capitalize" disabled>
|
||||||
Create Market
|
Create Market
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { firebaseLogin, firebaseLogout } from '../../lib/firebase/users'
|
||||||
import { ManifoldLogo } from './manifold-logo'
|
import { ManifoldLogo } from './manifold-logo'
|
||||||
import { MenuButton } from './menu'
|
import { MenuButton } from './menu'
|
||||||
import { getNavigationOptions, ProfileSummary } from './profile-menu'
|
import { getNavigationOptions, ProfileSummary } from './profile-menu'
|
||||||
|
import { useHasCreatedContractToday } from '../../hooks/use-has-created-contract-today'
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: 'Home', href: '/home', icon: HomeIcon },
|
{ name: 'Home', href: '/home', icon: HomeIcon },
|
||||||
|
@ -96,6 +97,7 @@ export default function Sidebar() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
let folds = useFollowedFolds(user) || []
|
let folds = useFollowedFolds(user) || []
|
||||||
folds = _.sortBy(folds, 'followCount').reverse()
|
folds = _.sortBy(folds, 'followCount').reverse()
|
||||||
|
const deservesDailyFreeMarket = !useHasCreatedContractToday(user)
|
||||||
|
|
||||||
const navigationOptions = user === null ? signedOutNavigation : navigation
|
const navigationOptions = user === null ? signedOutNavigation : navigation
|
||||||
const mobileNavigationOptions =
|
const mobileNavigationOptions =
|
||||||
|
@ -159,10 +161,22 @@ export default function Sidebar() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{deservesDailyFreeMarket ? (
|
||||||
|
<div className=" text-primary mt-4 text-center">
|
||||||
|
Use your daily free market! 🎉
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<Link href={'/create'}>
|
<div className={'aligncenter flex justify-center'}>
|
||||||
<button className="btn btn-primary btn-md mt-4">Create Market</button>
|
<Link href={'/create'}>
|
||||||
</Link>
|
<button className="btn btn-primary btn-md mt-4 capitalize">
|
||||||
|
Create Market
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
|
27
web/hooks/use-has-created-contract-today.ts
Normal file
27
web/hooks/use-has-created-contract-today.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { listContracts } from '../lib/firebase/contracts'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
|
||||||
|
export const useHasCreatedContractToday = (user: User | null | undefined) => {
|
||||||
|
const [hasCreatedContractToday, setHasCreatedContractToday] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Uses utc time like the server.
|
||||||
|
const utcTimeString = new Date().toISOString()
|
||||||
|
const todayAtMidnight = new Date(utcTimeString).setUTCHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
async function listUserContractsForToday() {
|
||||||
|
if (!user) return
|
||||||
|
|
||||||
|
const contracts = await listContracts(user.id)
|
||||||
|
const todayContracts = contracts.filter(
|
||||||
|
(contract) => contract.createdTime > todayAtMidnight
|
||||||
|
)
|
||||||
|
setHasCreatedContractToday(todayContracts.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
listUserContractsForToday()
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
return hasCreatedContractToday
|
||||||
|
}
|
|
@ -115,6 +115,18 @@ export async function listContracts(creatorId: string): Promise<Contract[]> {
|
||||||
return snapshot.docs.map((doc) => doc.data() as Contract)
|
return snapshot.docs.map((doc) => doc.data() as Contract)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listTaggedContractsCaseInsensitive(
|
||||||
|
tag: string
|
||||||
|
): Promise<Contract[]> {
|
||||||
|
const q = query(
|
||||||
|
contractCollection,
|
||||||
|
where('lowercaseTags', 'array-contains', tag.toLowerCase()),
|
||||||
|
orderBy('createdTime', 'desc')
|
||||||
|
)
|
||||||
|
const snapshot = await getDocs(q)
|
||||||
|
return snapshot.docs.map((doc) => doc.data() as Contract)
|
||||||
|
}
|
||||||
|
|
||||||
export async function listAllContracts(): Promise<Contract[]> {
|
export async function listAllContracts(): Promise<Contract[]> {
|
||||||
const q = query(contractCollection, orderBy('createdTime', 'desc'))
|
const q = query(contractCollection, orderBy('createdTime', 'desc'))
|
||||||
const snapshot = await getDocs(q)
|
const snapshot = await getDocs(q)
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"next": "12.1.2",
|
"next": "12.1.2",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
"react-confetti": "^6.0.1",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-expanding-textarea": "2.3.5"
|
"react-expanding-textarea": "2.3.5"
|
||||||
},
|
},
|
||||||
|
|
|
@ -33,6 +33,8 @@ import { ContractTabs } from '../../components/contract/contract-tabs'
|
||||||
import { FirstArgument } from '../../../common/util/types'
|
import { FirstArgument } from '../../../common/util/types'
|
||||||
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
||||||
import { contractTextDetails } from '../../components/contract/contract-details'
|
import { contractTextDetails } from '../../components/contract/contract-details'
|
||||||
|
import { useWindowSize } from '../../hooks/use-window-size'
|
||||||
|
import Confetti from 'react-confetti'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: {
|
export async function getStaticPropz(props: {
|
||||||
|
@ -86,9 +88,21 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
|
||||||
const { backToHome } = props
|
const { backToHome } = props
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
const { width, height } = useWindowSize()
|
||||||
|
|
||||||
const contract = useContractWithPreload(props.contract)
|
const contract = useContractWithPreload(props.contract)
|
||||||
const { bets, comments } = props
|
const { bets, comments } = props
|
||||||
|
const [showConfetti, setShowConfetti] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldSeeConfetti = !!(
|
||||||
|
user &&
|
||||||
|
contract &&
|
||||||
|
contract.creatorId === user.id &&
|
||||||
|
Date.now() - contract.createdTime < 10 * 1000
|
||||||
|
)
|
||||||
|
setShowConfetti(shouldSeeConfetti)
|
||||||
|
}, [contract, user])
|
||||||
|
|
||||||
// Sort for now to see if bug is fixed.
|
// Sort for now to see if bug is fixed.
|
||||||
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||||
|
@ -119,6 +133,15 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page rightSidebar={rightSidebar}>
|
<Page rightSidebar={rightSidebar}>
|
||||||
|
{showConfetti && (
|
||||||
|
<Confetti
|
||||||
|
width={width ? width : 500}
|
||||||
|
height={height ? height : 500}
|
||||||
|
recycle={false}
|
||||||
|
numberOfPieces={300}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{ogCardProps && (
|
{ogCardProps && (
|
||||||
<SEO
|
<SEO
|
||||||
title={question}
|
title={question}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { TagsList } from '../components/tags-list'
|
||||||
import { Row } from '../components/layout/row'
|
import { Row } from '../components/layout/row'
|
||||||
import { MAX_DESCRIPTION_LENGTH, outcomeType } from '../../common/contract'
|
import { MAX_DESCRIPTION_LENGTH, outcomeType } from '../../common/contract'
|
||||||
import { formatMoney } from '../../common/util/format'
|
import { formatMoney } from '../../common/util/format'
|
||||||
|
import { useHasCreatedContractToday } from '../hooks/use-has-created-contract-today'
|
||||||
|
|
||||||
export default function Create() {
|
export default function Create() {
|
||||||
const [question, setQuestion] = useState('')
|
const [question, setQuestion] = useState('')
|
||||||
|
@ -70,6 +71,9 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
const tags = parseWordsAsTags(tagText)
|
const tags = parseWordsAsTags(tagText)
|
||||||
|
|
||||||
const [ante, setAnte] = useState(FIXED_ANTE)
|
const [ante, setAnte] = useState(FIXED_ANTE)
|
||||||
|
|
||||||
|
const deservesDailyFreeMarket = !useHasCreatedContractToday(creator)
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// if (ante === null && creator) {
|
// if (ante === null && creator) {
|
||||||
// const initialAnte = creator.balance < 100 ? MINIMUM_ANTE : 100
|
// const initialAnte = creator.balance < 100 ? MINIMUM_ANTE : 100
|
||||||
|
@ -95,7 +99,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
ante !== undefined &&
|
ante !== undefined &&
|
||||||
ante !== null &&
|
ante !== null &&
|
||||||
ante >= MINIMUM_ANTE &&
|
ante >= MINIMUM_ANTE &&
|
||||||
ante <= balance &&
|
(ante <= balance || deservesDailyFreeMarket) &&
|
||||||
// closeTime must be in the future
|
// closeTime must be in the future
|
||||||
closeTime &&
|
closeTime &&
|
||||||
closeTime > Date.now()
|
closeTime > Date.now()
|
||||||
|
@ -246,10 +250,14 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
text={`Cost to create your market. This amount is used to subsidize trading.`}
|
text={`Cost to create your market. This amount is used to subsidize trading.`}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
{deservesDailyFreeMarket ? (
|
||||||
<div className="label-text text-neutral pl-1">{formatMoney(ante)}</div>
|
<div className="label-text text-primary pl-1">FREE</div>
|
||||||
|
) : (
|
||||||
{ante > balance && (
|
<div className="label-text text-neutral pl-1">
|
||||||
|
{formatMoney(ante)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!deservesDailyFreeMarket && ante > balance && (
|
||||||
<div className="mb-2 mt-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide">
|
<div className="mb-2 mt-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide">
|
||||||
<span className="mr-2 text-red-500">Insufficient balance</span>
|
<span className="mr-2 text-red-500">Insufficient balance</span>
|
||||||
<button
|
<button
|
||||||
|
@ -278,7 +286,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn btn-primary',
|
'btn btn-primary capitalize',
|
||||||
isSubmitting && 'loading disabled'
|
isSubmitting && 'loading disabled'
|
||||||
)}
|
)}
|
||||||
disabled={isSubmitting || !isValid}
|
disabled={isSubmitting || !isValid}
|
||||||
|
|
|
@ -1,39 +1,33 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { SearchableGrid } from '../../components/contract/contracts-list'
|
import { SearchableGrid } from '../../components/contract/contracts-list'
|
||||||
import { Page } from '../../components/page'
|
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 { Contract, listAllContracts } from '../../lib/firebase/contracts'
|
import {
|
||||||
|
Contract,
|
||||||
export async function getStaticProps() {
|
listTaggedContractsCaseInsensitive,
|
||||||
const contracts = await listAllContracts().catch((_) => [])
|
} from '../../lib/firebase/contracts'
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
contracts,
|
|
||||||
},
|
|
||||||
|
|
||||||
revalidate: 60, // regenerate after a minute
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
return { paths: [], fallback: 'blocking' }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TagPage(props: { contracts: Contract[] }) {
|
export default function TagPage(props: { contracts: Contract[] }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { tag } = router.query as { tag: string }
|
const { tag } = router.query as { tag: string }
|
||||||
|
|
||||||
const contracts = useContracts()
|
// mqp: i wrote this in a panic to make the page literally work at all so if you
|
||||||
|
// want to e.g. listen for new contracts you may want to fix it up
|
||||||
|
const [contracts, setContracts] = useState<Contract[] | 'loading'>('loading')
|
||||||
|
useEffect(() => {
|
||||||
|
if (tag != null) {
|
||||||
|
listTaggedContractsCaseInsensitive(tag).then(setContracts)
|
||||||
|
}
|
||||||
|
}, [tag])
|
||||||
|
|
||||||
const taggedContracts = (contracts ?? props.contracts).filter((contract) =>
|
if (contracts === 'loading') return <></>
|
||||||
contract.lowercaseTags.includes(tag.toLowerCase())
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Title text={`#${tag}`} />
|
<Title text={`#${tag}`} />
|
||||||
<SearchableGrid contracts={taggedContracts} />
|
<SearchableGrid contracts={contracts} />
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -4441,6 +4441,13 @@ raw-body@2.4.2, raw-body@^2.2.0:
|
||||||
iconv-lite "0.4.24"
|
iconv-lite "0.4.24"
|
||||||
unpipe "1.0.0"
|
unpipe "1.0.0"
|
||||||
|
|
||||||
|
react-confetti@^6.0.1:
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.0.1.tgz#d4f57b5a021dd908a6243b8f63b6009b00818d10"
|
||||||
|
integrity sha512-ZpOTBrqSNhWE4rRXCZ6E6U+wGd7iYHF5MGrqwikoiBpgBq9Akdu0DcLW+FdFnLjyZYC+VfAiV2KeFgYRMyMrkA==
|
||||||
|
dependencies:
|
||||||
|
tween-functions "^1.2.0"
|
||||||
|
|
||||||
react-dom@17.0.2:
|
react-dom@17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||||
|
@ -5154,6 +5161,11 @@ tsutils@^3.21.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.8.1"
|
tslib "^1.8.1"
|
||||||
|
|
||||||
|
tween-functions@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
|
||||||
|
integrity sha1-GuOlDnxguz3vd06scHrLynO7w/8=
|
||||||
|
|
||||||
type-check@^0.4.0, type-check@~0.4.0:
|
type-check@^0.4.0, type-check@~0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user