Merge branch 'main' into show-comments-position

This commit is contained in:
Ian Philips 2022-04-29 08:04:52 -06:00
commit 3e600f45b5
11 changed files with 158 additions and 48 deletions

View File

@ -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)

View File

@ -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>
)}
</>
) )
} }

View File

@ -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>

View File

@ -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>
) )

View 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
}

View File

@ -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)

View File

@ -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"
}, },

View File

@ -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}

View File

@ -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}

View File

@ -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>
) )
} }

View File

@ -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"