Create and show a date market as well
This commit is contained in:
parent
d30a5c453b
commit
0ca38d39fd
|
@ -14,6 +14,7 @@ export type DateDoc = Post & {
|
|||
birthday: number
|
||||
photoUrl: string
|
||||
type: 'date-doc'
|
||||
contractSlug: string
|
||||
}
|
||||
|
||||
export const MAX_POST_TITLE_LENGTH = 480
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
export { APIError } from '../../common/api'
|
||||
|
||||
type Output = Record<string, unknown>
|
||||
type AuthedUser = {
|
||||
export type AuthedUser = {
|
||||
uid: string
|
||||
creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser })
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { slugify } from '../../common/util/slugify'
|
|||
import { randomString } from '../../common/util/random'
|
||||
|
||||
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 {
|
||||
|
@ -92,7 +92,11 @@ const multipleChoiceSchema = z.object({
|
|||
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 {
|
||||
question,
|
||||
description,
|
||||
|
@ -101,16 +105,13 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
outcomeType,
|
||||
groupId,
|
||||
visibility = 'public',
|
||||
} = validate(bodySchema, req.body)
|
||||
} = validate(bodySchema, body)
|
||||
|
||||
let min, max, initialProb, isLogScale, answers
|
||||
|
||||
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
||||
let initialValue
|
||||
;({ min, max, initialValue, isLogScale } = validate(
|
||||
numericSchema,
|
||||
req.body
|
||||
))
|
||||
;({ min, max, initialValue, isLogScale } = validate(numericSchema, body))
|
||||
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
|
||||
throw new APIError(400, 'Invalid range.')
|
||||
|
||||
|
@ -126,11 +127,11 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
}
|
||||
|
||||
if (outcomeType === 'BINARY') {
|
||||
;({ initialProb } = validate(binarySchema, req.body))
|
||||
;({ initialProb } = validate(binarySchema, body))
|
||||
}
|
||||
|
||||
if (outcomeType === 'MULTIPLE_CHOICE') {
|
||||
;({ answers } = validate(multipleChoiceSchema, req.body))
|
||||
;({ answers } = validate(multipleChoiceSchema, body))
|
||||
}
|
||||
|
||||
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
|
||||
const newDescription =
|
||||
typeof description === 'string'
|
||||
!description || typeof description === 'string'
|
||||
? {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: description }],
|
||||
content: [{ type: 'text', text: description || ' ' }],
|
||||
},
|
||||
],
|
||||
}
|
||||
: description ?? {}
|
||||
: description
|
||||
|
||||
const contract = getNewContract(
|
||||
contractRef.id,
|
||||
|
@ -323,7 +324,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
}
|
||||
|
||||
return contract
|
||||
})
|
||||
}
|
||||
|
||||
const getSlug = async (question: string) => {
|
||||
const proposedSlug = slugify(question)
|
||||
|
|
|
@ -7,6 +7,9 @@ import { Post, MAX_POST_TITLE_LENGTH } from '../../common/post'
|
|||
import { APIError, newEndpoint, validate } from './api'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
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(() =>
|
||||
z.intersection(
|
||||
|
@ -35,11 +38,19 @@ const postSchema = z.object({
|
|||
title: z.string().min(1).max(MAX_POST_TITLE_LENGTH),
|
||||
content: contentSchema,
|
||||
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) => {
|
||||
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)
|
||||
if (!creator)
|
||||
|
@ -51,14 +62,33 @@ export const createpost = newEndpoint({}, async (req, auth) => {
|
|||
|
||||
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,
|
||||
creatorId: creator.id,
|
||||
slug,
|
||||
title,
|
||||
createdTime: Date.now(),
|
||||
content: content,
|
||||
}
|
||||
contractSlug,
|
||||
})
|
||||
|
||||
await postRef.create(post)
|
||||
if (groupId) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Router from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
|
||||
import { DateDoc } from 'common/post'
|
||||
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 { uploadImage } from 'web/lib/firebase/storage'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
import { MAX_QUESTION_LENGTH } from 'common/contract'
|
||||
|
||||
export default function CreateDateDocPage() {
|
||||
const user = useUser()
|
||||
|
@ -25,8 +27,11 @@ export default function CreateDateDocPage() {
|
|||
|
||||
const title = `${user?.name}'s Date Doc`
|
||||
const [birthday, setBirthday] = useState<undefined | string>(undefined)
|
||||
const [photoUrl, setPhotoUrl] = useState(user?.avatarUrl ?? '')
|
||||
const [photoUrl, setPhotoUrl] = useState('')
|
||||
const [avatarLoading, setAvatarLoading] = useState(false)
|
||||
const [question, setQuestion] = useState(
|
||||
'Will I find a partner in the next 3 months?'
|
||||
)
|
||||
|
||||
const [error, setError] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
@ -37,7 +42,12 @@ export default function CreateDateDocPage() {
|
|||
|
||||
const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined
|
||||
const isValid =
|
||||
user && birthday && photoUrl && editor && editor.isEmpty === false
|
||||
user &&
|
||||
birthday &&
|
||||
photoUrl &&
|
||||
editor &&
|
||||
editor.isEmpty === false &&
|
||||
question
|
||||
|
||||
const fileHandler = async (event: any) => {
|
||||
if (!user) return
|
||||
|
@ -53,21 +63,24 @@ export default function CreateDateDocPage() {
|
|||
})
|
||||
.catch(() => {
|
||||
setAvatarLoading(false)
|
||||
setPhotoUrl(user.avatarUrl || '')
|
||||
setPhotoUrl('')
|
||||
})
|
||||
}
|
||||
|
||||
async function saveDateDoc() {
|
||||
if (!editor || !birthdayTime) return
|
||||
|
||||
const newPost: Omit<DateDoc, 'id' | 'creatorId' | 'createdTime' | 'slug'> =
|
||||
{
|
||||
const newPost: Omit<
|
||||
DateDoc,
|
||||
'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug'
|
||||
> & { question: string } = {
|
||||
title,
|
||||
content: editor.getJSON(),
|
||||
bounty: 0,
|
||||
birthday: birthdayTime,
|
||||
photoUrl,
|
||||
type: 'date-doc',
|
||||
question,
|
||||
}
|
||||
|
||||
const result = await createPost(newPost).catch((e) => {
|
||||
|
@ -83,7 +96,7 @@ export default function CreateDateDocPage() {
|
|||
return (
|
||||
<Page>
|
||||
<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">
|
||||
<Title className="!my-0 text-blue-500" text="Your Date Doc" />
|
||||
<Button
|
||||
|
@ -100,8 +113,8 @@ export default function CreateDateDocPage() {
|
|||
</Button>
|
||||
</Row>
|
||||
|
||||
<Col className="gap-6">
|
||||
<Col className="max-w-[150px] justify-start gap-4">
|
||||
<Col className="gap-8">
|
||||
<Col className="max-w-[160px] justify-start gap-4">
|
||||
<div className="">Birthday</div>
|
||||
<input
|
||||
type={'date'}
|
||||
|
@ -126,7 +139,7 @@ export default function CreateDateDocPage() {
|
|||
src={photoUrl}
|
||||
width={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
|
||||
|
@ -147,6 +160,22 @@ export default function CreateDateDocPage() {
|
|||
</div>
|
||||
<TextEditor editor={editor} upload={upload} />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Page } from 'web/components/page'
|
||||
import { PlusCircleIcon } from '@heroicons/react/outline'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { getDateDocs } from 'web/lib/firebase/posts'
|
||||
import { DateDoc } from 'common/post'
|
||||
|
@ -11,28 +12,40 @@ import { useUser } from 'web/hooks/use-user'
|
|||
import { Row } from 'web/components/layout/row'
|
||||
import { Button } from 'web/components/button'
|
||||
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() {
|
||||
const dateDocs = await getDateDocs()
|
||||
const docCreators = await Promise.all(
|
||||
dateDocs.map((d) => getUser(d.creatorId))
|
||||
)
|
||||
|
||||
return {
|
||||
props: {
|
||||
dateDocs,
|
||||
docCreators,
|
||||
},
|
||||
|
||||
revalidate: 60, // regenerate after a minute
|
||||
}
|
||||
}
|
||||
|
||||
export default function DatePage(props: { dateDocs: DateDoc[] }) {
|
||||
const { dateDocs } = props
|
||||
export default function DatePage(props: {
|
||||
dateDocs: DateDoc[]
|
||||
docCreators: User[]
|
||||
}) {
|
||||
const { dateDocs, docCreators } = props
|
||||
const user = useUser()
|
||||
|
||||
const hasDoc = dateDocs.some((d) => d.creatorId === user?.id)
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="mx-auto w-full max-w-3xl">
|
||||
<Row className="items-center justify-between">
|
||||
<Title className="!my-0 px-2 text-blue-500" text="Date docs" />
|
||||
{!hasDoc && (
|
||||
<SiteLink href="/date/create" className="!no-underline">
|
||||
<Button className="flex flex-row gap-1" color="blue">
|
||||
<PlusCircleIcon
|
||||
|
@ -42,11 +55,16 @@ export default function DatePage(props: { dateDocs: DateDoc[] }) {
|
|||
New
|
||||
</Button>
|
||||
</SiteLink>
|
||||
)}
|
||||
</Row>
|
||||
<Spacer h={2} />
|
||||
<Spacer h={6} />
|
||||
<Col className="gap-4">
|
||||
{dateDocs.map((dateDoc) => (
|
||||
<DateDoc key={dateDoc.id} dateDoc={dateDoc} />
|
||||
{dateDocs.map((dateDoc, i) => (
|
||||
<DateDoc
|
||||
key={dateDoc.id}
|
||||
dateDoc={dateDoc}
|
||||
creator={docCreators[i]}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
</div>
|
||||
|
@ -54,15 +72,42 @@ export default function DatePage(props: { dateDocs: DateDoc[] }) {
|
|||
)
|
||||
}
|
||||
|
||||
function DateDoc(props: { dateDoc: DateDoc }) {
|
||||
const { dateDoc } = props
|
||||
const { content } = dateDoc
|
||||
function DateDoc(props: { dateDoc: DateDoc; creator: User }) {
|
||||
const { dateDoc, creator } = props
|
||||
const { content, birthday, photoUrl, contractSlug } = dateDoc
|
||||
const { name, username } = creator
|
||||
|
||||
const age = dayjs().diff(birthday, 'year')
|
||||
const marketUrl = `https://${DOMAIN}/${username}/${contractSlug}`
|
||||
|
||||
return (
|
||||
<div className="rounded-lg bg-white px-6 py-4 sm:py-0">
|
||||
<div className="form-control w-full py-2">
|
||||
<Col className="rounded-lg bg-white px-6 py-6">
|
||||
<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} />
|
||||
<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>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user