Create and show a date market as well

This commit is contained in:
James Grugett 2022-09-27 17:11:29 -04:00
parent d30a5c453b
commit 0ca38d39fd
6 changed files with 161 additions and 55 deletions

View File

@ -14,6 +14,7 @@ export type DateDoc = Post & {
birthday: number birthday: number
photoUrl: string photoUrl: string
type: 'date-doc' type: 'date-doc'
contractSlug: string
} }
export const MAX_POST_TITLE_LENGTH = 480 export const MAX_POST_TITLE_LENGTH = 480

View File

@ -14,7 +14,7 @@ import {
export { APIError } from '../../common/api' export { APIError } from '../../common/api'
type Output = Record<string, unknown> type Output = Record<string, unknown>
type AuthedUser = { export type AuthedUser = {
uid: string uid: string
creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser }) creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser })
} }

View File

@ -16,7 +16,7 @@ import { slugify } from '../../common/util/slugify'
import { randomString } from '../../common/util/random' import { randomString } from '../../common/util/random'
import { chargeUser, getContract, isProd } from './utils' import { chargeUser, getContract, isProd } from './utils'
import { APIError, newEndpoint, validate, zTimestamp } from './api' import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api'
import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy' import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy'
import { import {
@ -92,7 +92,11 @@ const multipleChoiceSchema = z.object({
answers: z.string().trim().min(1).array().min(2), answers: z.string().trim().min(1).array().min(2),
}) })
export const createmarket = newEndpoint({}, async (req, auth) => { export const createmarket = newEndpoint({}, (req, auth) => {
return createMarketHelper(req.body, auth)
})
export async function createMarketHelper(body: any, auth: AuthedUser) {
const { const {
question, question,
description, description,
@ -101,16 +105,13 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
outcomeType, outcomeType,
groupId, groupId,
visibility = 'public', visibility = 'public',
} = validate(bodySchema, req.body) } = validate(bodySchema, body)
let min, max, initialProb, isLogScale, answers let min, max, initialProb, isLogScale, answers
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') { if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
let initialValue let initialValue
;({ min, max, initialValue, isLogScale } = validate( ;({ min, max, initialValue, isLogScale } = validate(numericSchema, body))
numericSchema,
req.body
))
if (max - min <= 0.01 || initialValue <= min || initialValue >= max) if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
throw new APIError(400, 'Invalid range.') throw new APIError(400, 'Invalid range.')
@ -126,11 +127,11 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
} }
if (outcomeType === 'BINARY') { if (outcomeType === 'BINARY') {
;({ initialProb } = validate(binarySchema, req.body)) ;({ initialProb } = validate(binarySchema, body))
} }
if (outcomeType === 'MULTIPLE_CHOICE') { if (outcomeType === 'MULTIPLE_CHOICE') {
;({ answers } = validate(multipleChoiceSchema, req.body)) ;({ answers } = validate(multipleChoiceSchema, body))
} }
const userDoc = await firestore.collection('users').doc(auth.uid).get() const userDoc = await firestore.collection('users').doc(auth.uid).get()
@ -186,17 +187,17 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
// convert string descriptions into JSONContent // convert string descriptions into JSONContent
const newDescription = const newDescription =
typeof description === 'string' !description || typeof description === 'string'
? { ? {
type: 'doc', type: 'doc',
content: [ content: [
{ {
type: 'paragraph', type: 'paragraph',
content: [{ type: 'text', text: description }], content: [{ type: 'text', text: description || ' ' }],
}, },
], ],
} }
: description ?? {} : description
const contract = getNewContract( const contract = getNewContract(
contractRef.id, contractRef.id,
@ -323,7 +324,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
} }
return contract return contract
}) }
const getSlug = async (question: string) => { const getSlug = async (question: string) => {
const proposedSlug = slugify(question) const proposedSlug = slugify(question)

View File

@ -7,6 +7,9 @@ import { Post, MAX_POST_TITLE_LENGTH } from '../../common/post'
import { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from './api'
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
import { z } from 'zod' import { z } from 'zod'
import { removeUndefinedProps } from '../../common/util/object'
import { createMarketHelper } from './create-market'
import { DAY_MS } from '../../common/util/time'
const contentSchema: z.ZodType<JSONContent> = z.lazy(() => const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection( z.intersection(
@ -35,11 +38,19 @@ const postSchema = z.object({
title: z.string().min(1).max(MAX_POST_TITLE_LENGTH), title: z.string().min(1).max(MAX_POST_TITLE_LENGTH),
content: contentSchema, content: contentSchema,
groupId: z.string().optional(), groupId: z.string().optional(),
bounty: z.number().optional(),
birthday: z.number().optional(),
photoUrl: z.string().optional(),
type: z.string().optional(),
question: z.string().optional(),
}) })
export const createpost = newEndpoint({}, async (req, auth) => { export const createpost = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore() const firestore = admin.firestore()
const { title, content, groupId } = validate(postSchema, req.body) const { title, content, groupId, question, ...otherProps } = validate(
postSchema,
req.body
)
const creator = await getUser(auth.uid) const creator = await getUser(auth.uid)
if (!creator) if (!creator)
@ -51,14 +62,33 @@ export const createpost = newEndpoint({}, async (req, auth) => {
const postRef = firestore.collection('posts').doc() const postRef = firestore.collection('posts').doc()
const post: Post = { let contractSlug
if (question) {
const closeTime = Date.now() + DAY_MS * 30 * 3
const result = await createMarketHelper(
{
question,
closeTime,
outcomeType: 'BINARY',
visibility: 'unlisted',
initialProb: 50,
},
auth
)
contractSlug = result.slug
}
const post: Post = removeUndefinedProps({
...otherProps,
id: postRef.id, id: postRef.id,
creatorId: creator.id, creatorId: creator.id,
slug, slug,
title, title,
createdTime: Date.now(), createdTime: Date.now(),
content: content, content: content,
} contractSlug,
})
await postRef.create(post) await postRef.create(post)
if (groupId) { if (groupId) {

View File

@ -1,5 +1,6 @@
import Router from 'next/router' import Router from 'next/router'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Textarea from 'react-expanding-textarea'
import { DateDoc } from 'common/post' import { DateDoc } from 'common/post'
import { useTextEditor, TextEditor } from 'web/components/editor' import { useTextEditor, TextEditor } from 'web/components/editor'
@ -15,6 +16,7 @@ import { MINUTE_MS } from 'common/util/time'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { uploadImage } from 'web/lib/firebase/storage' import { uploadImage } from 'web/lib/firebase/storage'
import { LoadingIndicator } from 'web/components/loading-indicator' import { LoadingIndicator } from 'web/components/loading-indicator'
import { MAX_QUESTION_LENGTH } from 'common/contract'
export default function CreateDateDocPage() { export default function CreateDateDocPage() {
const user = useUser() const user = useUser()
@ -25,8 +27,11 @@ export default function CreateDateDocPage() {
const title = `${user?.name}'s Date Doc` const title = `${user?.name}'s Date Doc`
const [birthday, setBirthday] = useState<undefined | string>(undefined) const [birthday, setBirthday] = useState<undefined | string>(undefined)
const [photoUrl, setPhotoUrl] = useState(user?.avatarUrl ?? '') const [photoUrl, setPhotoUrl] = useState('')
const [avatarLoading, setAvatarLoading] = useState(false) const [avatarLoading, setAvatarLoading] = useState(false)
const [question, setQuestion] = useState(
'Will I find a partner in the next 3 months?'
)
const [error, setError] = useState('') const [error, setError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@ -37,7 +42,12 @@ export default function CreateDateDocPage() {
const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined
const isValid = const isValid =
user && birthday && photoUrl && editor && editor.isEmpty === false user &&
birthday &&
photoUrl &&
editor &&
editor.isEmpty === false &&
question
const fileHandler = async (event: any) => { const fileHandler = async (event: any) => {
if (!user) return if (!user) return
@ -53,21 +63,24 @@ export default function CreateDateDocPage() {
}) })
.catch(() => { .catch(() => {
setAvatarLoading(false) setAvatarLoading(false)
setPhotoUrl(user.avatarUrl || '') setPhotoUrl('')
}) })
} }
async function saveDateDoc() { async function saveDateDoc() {
if (!editor || !birthdayTime) return if (!editor || !birthdayTime) return
const newPost: Omit<DateDoc, 'id' | 'creatorId' | 'createdTime' | 'slug'> = const newPost: Omit<
{ DateDoc,
'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug'
> & { question: string } = {
title, title,
content: editor.getJSON(), content: editor.getJSON(),
bounty: 0, bounty: 0,
birthday: birthdayTime, birthday: birthdayTime,
photoUrl, photoUrl,
type: 'date-doc', type: 'date-doc',
question,
} }
const result = await createPost(newPost).catch((e) => { const result = await createPost(newPost).catch((e) => {
@ -83,7 +96,7 @@ export default function CreateDateDocPage() {
return ( return (
<Page> <Page>
<div className="mx-auto w-full max-w-3xl"> <div className="mx-auto w-full max-w-3xl">
<div className="rounded-lg px-6 py-4 sm:py-0"> <div className="rounded-lg px-6 py-4 pb-4 sm:py-0">
<Row className="mb-8 items-center justify-between"> <Row className="mb-8 items-center justify-between">
<Title className="!my-0 text-blue-500" text="Your Date Doc" /> <Title className="!my-0 text-blue-500" text="Your Date Doc" />
<Button <Button
@ -100,8 +113,8 @@ export default function CreateDateDocPage() {
</Button> </Button>
</Row> </Row>
<Col className="gap-6"> <Col className="gap-8">
<Col className="max-w-[150px] justify-start gap-4"> <Col className="max-w-[160px] justify-start gap-4">
<div className="">Birthday</div> <div className="">Birthday</div>
<input <input
type={'date'} type={'date'}
@ -126,7 +139,7 @@ export default function CreateDateDocPage() {
src={photoUrl} src={photoUrl}
width={80} width={80}
height={80} height={80}
className="flex h-[80px] w-[80px] items-center justify-center rounded-full bg-gray-400 object-cover" className="flex h-[80px] w-[80px] items-center justify-center rounded-lg bg-gray-400 object-cover"
/> />
)} )}
<input <input
@ -147,6 +160,22 @@ export default function CreateDateDocPage() {
</div> </div>
<TextEditor editor={editor} upload={upload} /> <TextEditor editor={editor} upload={upload} />
</Col> </Col>
<Col className="gap-4">
<div className="">
Finally, we'll create an (unlisted) prediction market!
</div>
<Col className="gap-2">
<Textarea
className="input input-bordered resize-none"
maxLength={MAX_QUESTION_LENGTH}
value={question}
onChange={(e) => setQuestion(e.target.value || '')}
/>
<div className="ml-2 text-gray-500">Cost: M$100</div>
</Col>
</Col>
</Col> </Col>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { PlusCircleIcon } from '@heroicons/react/outline' import { PlusCircleIcon } from '@heroicons/react/outline'
import dayjs from 'dayjs'
import { getDateDocs } from 'web/lib/firebase/posts' import { getDateDocs } from 'web/lib/firebase/posts'
import { DateDoc } from 'common/post' import { DateDoc } from 'common/post'
@ -11,28 +12,40 @@ import { useUser } from 'web/hooks/use-user'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { Button } from 'web/components/button' import { Button } from 'web/components/button'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { getUser, User } from 'web/lib/firebase/users'
import { DOMAIN } from 'common/envs/constants'
export async function getStaticProps() { export async function getStaticProps() {
const dateDocs = await getDateDocs() const dateDocs = await getDateDocs()
const docCreators = await Promise.all(
dateDocs.map((d) => getUser(d.creatorId))
)
return { return {
props: { props: {
dateDocs, dateDocs,
docCreators,
}, },
revalidate: 60, // regenerate after a minute revalidate: 60, // regenerate after a minute
} }
} }
export default function DatePage(props: { dateDocs: DateDoc[] }) { export default function DatePage(props: {
const { dateDocs } = props dateDocs: DateDoc[]
docCreators: User[]
}) {
const { dateDocs, docCreators } = props
const user = useUser() const user = useUser()
const hasDoc = dateDocs.some((d) => d.creatorId === user?.id)
return ( return (
<Page> <Page>
<div className="mx-auto w-full max-w-3xl "> <div className="mx-auto w-full max-w-3xl">
<Row className="items-center justify-between"> <Row className="items-center justify-between">
<Title className="!my-0 px-2 text-blue-500" text="Date docs" /> <Title className="!my-0 px-2 text-blue-500" text="Date docs" />
{!hasDoc && (
<SiteLink href="/date/create" className="!no-underline"> <SiteLink href="/date/create" className="!no-underline">
<Button className="flex flex-row gap-1" color="blue"> <Button className="flex flex-row gap-1" color="blue">
<PlusCircleIcon <PlusCircleIcon
@ -42,11 +55,16 @@ export default function DatePage(props: { dateDocs: DateDoc[] }) {
New New
</Button> </Button>
</SiteLink> </SiteLink>
)}
</Row> </Row>
<Spacer h={2} /> <Spacer h={6} />
<Col className="gap-4"> <Col className="gap-4">
{dateDocs.map((dateDoc) => ( {dateDocs.map((dateDoc, i) => (
<DateDoc key={dateDoc.id} dateDoc={dateDoc} /> <DateDoc
key={dateDoc.id}
dateDoc={dateDoc}
creator={docCreators[i]}
/>
))} ))}
</Col> </Col>
</div> </div>
@ -54,15 +72,42 @@ export default function DatePage(props: { dateDocs: DateDoc[] }) {
) )
} }
function DateDoc(props: { dateDoc: DateDoc }) { function DateDoc(props: { dateDoc: DateDoc; creator: User }) {
const { dateDoc } = props const { dateDoc, creator } = props
const { content } = dateDoc const { content, birthday, photoUrl, contractSlug } = dateDoc
const { name, username } = creator
const age = dayjs().diff(birthday, 'year')
const marketUrl = `https://${DOMAIN}/${username}/${contractSlug}`
return ( return (
<div className="rounded-lg bg-white px-6 py-4 sm:py-0"> <Col className="rounded-lg bg-white px-6 py-6">
<div className="form-control w-full py-2"> <Col className="gap-2 self-center">
<Row>
<img
className="w-full max-w-lg rounded-lg object-cover"
src={photoUrl}
alt={name}
/>
</Row>
<Row className="gap-4 text-2xl">
<div>
{name}, {age}
</div>
</Row>
</Col>
<Spacer h={6} />
<Content content={content} /> <Content content={content} />
<Spacer h={6} />
<div className="mt-10 w-full rounded-xl bg-gradient-to-r from-blue-200 via-purple-200 to-indigo-300 p-5">
<iframe
height="405"
src={marketUrl}
title=""
frameBorder="0"
className="w-full rounded-xl bg-white p-10"
></iframe>
</div> </div>
</div> </Col>
) )
} }