From 9a11f557624867bf3d593d68deb69f7daa820c4a Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Wed, 13 Jul 2022 11:58:22 -0700 Subject: [PATCH] Rich content (#620) * Add TipTap editor and renderer components * Change market description editor to rich text * Type description as JSON, fix string-based logic - Delete make-predictions.tsx - Delete feed logic that showed descriptions * wip Fix API validation * fix type error * fix extension import (backend) In firebase, typescript compiles imports into common js imports like `const StarterKit = require("@tiptap/starter-kit")` Even though StarterKit is exported from the cjs file, it gets imported as undefined. But it magically works if we import * If you're reading this in the future, consider replacing StarterKit with the entire list of extensions. * Stop load on fail create market, improve warning * Refactor editor as hook / fix infinite submit bug Move state of editor back up to parent We have to do this later anyways to allow parent to edit * Add images - display, paste + uploading * add uploading state of image * Fix placeholder, misc styling min height, quote * Fix appending to description * code review fixes: rename, refactor, chop carets * Add hint & upload button on new lines - bump to Tailwind 3.1 for arbitrary variants * clean up, run prettier * rename FileButton to FileUploadButton * add image extension as functions dependency --- common/contract.ts | 3 +- common/new-contract.ts | 13 +- common/package.json | 2 + common/util/parse.ts | 47 ++ functions/package.json | 3 + functions/src/create-contract.ts | 29 +- functions/src/on-create-contract.ts | 4 +- .../contract/contract-description.tsx | 24 +- web/components/contract/contract-details.tsx | 15 +- web/components/editor.tsx | 152 +++++++ web/components/feed/activity-items.ts | 1 - web/components/feed/feed-items.tsx | 11 +- web/components/file-upload-button.tsx | 26 ++ web/lib/firebase/storage.ts | 1 + web/package.json | 7 +- web/pages/[username]/[contractSlug].tsx | 10 +- web/pages/api/v0/_types.ts | 3 +- web/pages/contract-search-firestore.tsx | 1 - web/pages/create.tsx | 52 +-- web/pages/group/[...slugs]/index.tsx | 1 - web/pages/make-predictions.tsx | 292 ------------- yarn.lock | 404 ++++++++++++++++-- 22 files changed, 700 insertions(+), 401 deletions(-) create mode 100644 web/components/editor.tsx create mode 100644 web/components/file-upload-button.tsx delete mode 100644 web/pages/make-predictions.tsx diff --git a/common/contract.ts b/common/contract.ts index 3a90d01f..52ca91d6 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -1,5 +1,6 @@ import { Answer } from './answer' import { Fees } from './fees' +import { JSONContent } from '@tiptap/core' export type AnyMechanism = DPM | CPMM export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric @@ -20,7 +21,7 @@ export type Contract = { creatorAvatarUrl?: string question: string - description: string // More info about what the contract is about + description: string | JSONContent // More info about what the contract is about tags: string[] lowercaseTags: string[] visibility: 'public' | 'unlisted' diff --git a/common/new-contract.ts b/common/new-contract.ts index 6c89c8c4..abfafaf8 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -10,8 +10,9 @@ import { PseudoNumeric, } from './contract' import { User } from './user' -import { parseTags } from './util/parse' +import { parseTags, richTextToString } from './util/parse' import { removeUndefinedProps } from './util/object' +import { JSONContent } from '@tiptap/core' export function getNewContract( id: string, @@ -19,7 +20,7 @@ export function getNewContract( creator: User, question: string, outcomeType: outcomeType, - description: string, + description: JSONContent, initialProb: number, ante: number, closeTime: number, @@ -32,7 +33,11 @@ export function getNewContract( isLogScale: boolean ) { const tags = parseTags( - `${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}` + [ + question, + richTextToString(description), + ...extraTags.map((tag) => `#${tag}`), + ].join(' ') ) const lowercaseTags = tags.map((tag) => tag.toLowerCase()) @@ -56,7 +61,7 @@ export function getNewContract( creatorAvatarUrl: creator.avatarUrl, question: question.trim(), - description: description.trim(), + description, tags, lowercaseTags, visibility: 'public', diff --git a/common/package.json b/common/package.json index c8115d84..25992cb6 100644 --- a/common/package.json +++ b/common/package.json @@ -8,6 +8,8 @@ }, "sideEffects": false, "dependencies": { + "@tiptap/extension-image": "2.0.0-beta.30", + "@tiptap/starter-kit": "2.0.0-beta.190", "lodash": "4.17.21" }, "devDependencies": { diff --git a/common/util/parse.ts b/common/util/parse.ts index b73bdfb3..48b68fdd 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -1,4 +1,24 @@ import { MAX_TAG_LENGTH } from '../contract' +import { generateText, JSONContent } from '@tiptap/core' +// Tiptap starter extensions +import { Blockquote } from '@tiptap/extension-blockquote' +import { Bold } from '@tiptap/extension-bold' +import { BulletList } from '@tiptap/extension-bullet-list' +import { Code } from '@tiptap/extension-code' +import { CodeBlock } from '@tiptap/extension-code-block' +import { Document } from '@tiptap/extension-document' +import { HardBreak } from '@tiptap/extension-hard-break' +import { Heading } from '@tiptap/extension-heading' +import { History } from '@tiptap/extension-history' +import { HorizontalRule } from '@tiptap/extension-horizontal-rule' +import { Italic } from '@tiptap/extension-italic' +import { ListItem } from '@tiptap/extension-list-item' +import { OrderedList } from '@tiptap/extension-ordered-list' +import { Paragraph } from '@tiptap/extension-paragraph' +import { Strike } from '@tiptap/extension-strike' +import { Text } from '@tiptap/extension-text' +// other tiptap extensions +import { Image } from '@tiptap/extension-image' export function parseTags(text: string) { const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi @@ -27,3 +47,30 @@ export function parseWordsAsTags(text: string) { .join(' ') return parseTags(taggedText) } + +// can't just do [StarterKit, Image...] because it doesn't work with cjs imports +export const exhibitExts = [ + Blockquote, + Bold, + BulletList, + Code, + CodeBlock, + Document, + HardBreak, + Heading, + History, + HorizontalRule, + Italic, + ListItem, + OrderedList, + Paragraph, + Strike, + Text, + + Image, +] +// export const exhibitExts = [StarterKit as unknown as Extension, Image] + +export function richTextToString(text?: JSONContent) { + return !text ? '' : generateText(text, exhibitExts) +} diff --git a/functions/package.json b/functions/package.json index 4c9f4338..d7ebb663 100644 --- a/functions/package.json +++ b/functions/package.json @@ -24,6 +24,9 @@ "dependencies": { "@amplitude/node": "1.10.0", "@google-cloud/functions-framework": "3.1.2", + "@tiptap/core": "2.0.0-beta.181", + "@tiptap/extension-image": "2.0.0-beta.30", + "@tiptap/starter-kit": "2.0.0-beta.190", "firebase-admin": "10.0.0", "firebase-functions": "3.21.2", "lodash": "4.17.21", diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 9f14ea7a..c8cfc7c4 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -5,7 +5,6 @@ import { CPMMBinaryContract, Contract, FreeResponseContract, - MAX_DESCRIPTION_LENGTH, MAX_QUESTION_LENGTH, MAX_TAG_LENGTH, NumericContract, @@ -29,10 +28,34 @@ import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants' import { User } from '../../common/user' import { Group, MAX_ID_LENGTH } from '../../common/group' import { getPseudoProbability } from '../../common/pseudo-numeric' +import { JSONContent } from '@tiptap/core' + +const descScehma: z.ZodType = z.lazy(() => + z.intersection( + z.record(z.any()), + z.object({ + type: z.string().optional(), + attrs: z.record(z.any()).optional(), + content: z.array(descScehma).optional(), + marks: z + .array( + z.intersection( + z.record(z.any()), + z.object({ + type: z.string(), + attrs: z.record(z.any()).optional(), + }) + ) + ) + .optional(), + text: z.string().optional(), + }) + ) +) const bodySchema = z.object({ question: z.string().min(1).max(MAX_QUESTION_LENGTH), - description: z.string().max(MAX_DESCRIPTION_LENGTH), + description: descScehma.optional(), tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(), closeTime: zTimestamp().refine( (date) => date.getTime() > new Date().getTime(), @@ -131,7 +154,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => { user, question, outcomeType, - description, + description ?? {}, initialProb ?? 0, ante, closeTime.getTime(), diff --git a/functions/src/on-create-contract.ts b/functions/src/on-create-contract.ts index 20c7ceba..28682793 100644 --- a/functions/src/on-create-contract.ts +++ b/functions/src/on-create-contract.ts @@ -2,6 +2,8 @@ import * as functions from 'firebase-functions' import { getUser } from './utils' import { createNotification } from './create-notification' import { Contract } from '../../common/contract' +import { richTextToString } from '../../common/util/parse' +import { JSONContent } from '@tiptap/core' export const onCreateContract = functions.firestore .document('contracts/{contractId}') @@ -18,7 +20,7 @@ export const onCreateContract = functions.firestore 'created', contractCreator, eventId, - contract.description, + richTextToString(contract.description as JSONContent), contract ) }) diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx index d8b657cb..a427afe1 100644 --- a/web/components/contract/contract-description.tsx +++ b/web/components/contract/contract-description.tsx @@ -5,12 +5,13 @@ import Textarea from 'react-expanding-textarea' import { CATEGORY_LIST } from '../../../common/categories' import { Contract } from 'common/contract' -import { parseTags } from 'common/util/parse' +import { parseTags, exhibitExts } from 'common/util/parse' import { useAdmin } from 'web/hooks/use-admin' import { updateContract } from 'web/lib/firebase/contracts' import { Row } from '../layout/row' -import { Linkify } from '../linkify' import { TagsList } from '../tags-list' +import { Content } from '../editor' +import { Editor } from '@tiptap/react' export function ContractDescription(props: { contract: Contract @@ -21,22 +22,31 @@ export function ContractDescription(props: { const descriptionTimestamp = () => `${dayjs().format('MMM D, h:mma')}: ` const isAdmin = useAdmin() + const desc = contract.description ?? '' + // Append the new description (after a newline) async function saveDescription(newText: string) { - const newDescription = `${contract.description}\n\n${newText}`.trim() + const editor = new Editor({ content: desc, extensions: exhibitExts }) + editor + .chain() + .focus('end') + .insertContent('

') + .insertContent(newText.trim()) + .run() + const tags = parseTags( - `${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}` + `${editor.getText()} ${contract.tags.map((tag) => `#${tag}`).join(' ')}` ) const lowercaseTags = tags.map((tag) => tag.toLowerCase()) await updateContract(contract.id, { - description: newDescription, + description: editor.getJSON(), tags, lowercaseTags, }) } - if (!isCreator && !contract.description.trim()) return null + if (!isCreator) return null const { tags } = contract const categories = tags.filter((tag) => @@ -50,7 +60,7 @@ export function ContractDescription(props: { className )} > - + {categories.length > 0 && (
diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index f908918e..b4d67520 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -31,6 +31,8 @@ import { DAY_MS } from 'common/util/time' import { useGroupsWithContract } from 'web/hooks/use-group' import { ShareIconButton } from 'web/components/share-icon-button' import { useUser } from 'web/hooks/use-user' +import { Editor } from '@tiptap/react' +import { exhibitExts } from 'common/util/parse' export type ShowTime = 'resolve-date' | 'close-date' @@ -268,13 +270,20 @@ function EditableCloseDate(props: { const newCloseTime = dayjs(closeDate).valueOf() if (newCloseTime === closeTime) setIsEditingCloseTime(false) else if (newCloseTime > Date.now()) { - const { description } = contract + const content = contract.description const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a') - const newDescription = `${description}\n\nClose date updated to ${formattedCloseDate}` + + const editor = new Editor({ content, extensions: exhibitExts }) + editor + .chain() + .focus('end') + .insertContent('

') + .insertContent(`Close date updated to ${formattedCloseDate}`) + .run() updateContract(contract.id, { closeTime: newCloseTime, - description: newDescription, + description: editor.getJSON(), }) setIsEditingCloseTime(false) diff --git a/web/components/editor.tsx b/web/components/editor.tsx new file mode 100644 index 00000000..bd4d97c0 --- /dev/null +++ b/web/components/editor.tsx @@ -0,0 +1,152 @@ +import CharacterCount from '@tiptap/extension-character-count' +import Placeholder from '@tiptap/extension-placeholder' +import { + useEditor, + EditorContent, + FloatingMenu, + JSONContent, + Content, + Editor, +} from '@tiptap/react' +import StarterKit from '@tiptap/starter-kit' +import { Image } from '@tiptap/extension-image' +import clsx from 'clsx' +import { useEffect } from 'react' +import { Linkify } from './linkify' +import { uploadImage } from 'web/lib/firebase/storage' +import { useMutation } from 'react-query' +import { exhibitExts } from 'common/util/parse' +import { FileUploadButton } from './file-upload-button' + +const proseClass = + 'prose prose-sm prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none' + +export function useTextEditor(props: { + placeholder?: string + max?: number + defaultValue?: Content + disabled?: boolean +}) { + const { placeholder, max, defaultValue = '', disabled } = props + + const editorClass = clsx( + proseClass, + 'box-content min-h-[6em] textarea textarea-bordered' + ) + + const editor = useEditor({ + editorProps: { attributes: { class: editorClass } }, + extensions: [ + StarterKit.configure({ + heading: { levels: [1, 2, 3] }, + }), + Placeholder.configure({ + placeholder, + emptyEditorClass: + 'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0', + }), + CharacterCount.configure({ limit: max }), + Image, + ], + content: defaultValue, + }) + + const upload = useUploadMutation(editor) + + editor?.setOptions({ + editorProps: { + handlePaste(view, event) { + const imageFiles = Array.from(event.clipboardData?.files ?? []).filter( + (file) => file.type.startsWith('image') + ) + + if (!imageFiles.length) { + return // if no files pasted, use default paste handler + } + + event.preventDefault() + upload.mutate(imageFiles) + }, + }, + }) + + useEffect(() => { + editor?.setEditable(!disabled) + }, [editor, disabled]) + + return { editor, upload } +} + +export function TextEditor(props: { + editor: Editor | null + upload: ReturnType +}) { + const { editor, upload } = props + + return ( + <> + {/* hide placeholder when focused */} +
+ {editor && ( + + Type *anything* or even paste or{' '} + + upload an image + + + )} + +
+ {upload.isLoading && Uploading image...} + {upload.isError && ( + Error uploading image :( + )} + + ) +} + +const useUploadMutation = (editor: Editor | null) => + useMutation( + (files: File[]) => + Promise.all(files.map((file) => uploadImage('default', file))), + { + onSuccess(urls) { + if (!editor) return + let trans = editor.view.state.tr + urls.forEach((src: any) => { + const node = editor.view.state.schema.nodes.image.create({ src }) + trans = trans.insert(editor.view.state.selection.to, node) + }) + editor.view.dispatch(trans) + }, + } + ) + +function RichContent(props: { content: JSONContent }) { + const { content } = props + const editor = useEditor({ + editorProps: { attributes: { class: proseClass } }, + extensions: exhibitExts, + content, + editable: false, + }) + useEffect(() => void editor?.commands?.setContent(content), [editor, content]) + + return +} + +// backwards compatibility: we used to store content as strings +export function Content(props: { content: JSONContent | string }) { + const { content } = props + return typeof content === 'string' ? ( + + ) : ( + + ) +} diff --git a/web/components/feed/activity-items.ts b/web/components/feed/activity-items.ts index 68dfcb2d..511767c6 100644 --- a/web/components/feed/activity-items.ts +++ b/web/components/feed/activity-items.ts @@ -37,7 +37,6 @@ export type DescriptionItem = BaseActivityItem & { export type QuestionItem = BaseActivityItem & { type: 'question' - showDescription: boolean contractPath?: string } diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index a9618f8c..ff5f5440 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -31,7 +31,6 @@ import { FeedAnswerCommentGroup } from 'web/components/feed/feed-answer-comment- import { FeedCommentThread, CommentInput, - TruncatedComment, } from 'web/components/feed/feed-comments' import { FeedBet } from 'web/components/feed/feed-bets' import { CPMMBinaryContract, NumericContract } from 'common/contract' @@ -104,10 +103,9 @@ export function FeedItem(props: { item: ActivityItem }) { export function FeedQuestion(props: { contract: Contract - showDescription: boolean contractPath?: string }) { - const { contract, showDescription } = props + const { contract } = props const { creatorName, creatorUsername, @@ -163,13 +161,6 @@ export function FeedQuestion(props: { /> )} - {showDescription && ( - - )}
) diff --git a/web/components/file-upload-button.tsx b/web/components/file-upload-button.tsx new file mode 100644 index 00000000..3ff15d91 --- /dev/null +++ b/web/components/file-upload-button.tsx @@ -0,0 +1,26 @@ +import { useRef } from 'react' + +/** button that opens file upload window */ +export function FileUploadButton(props: { + onFiles: (files: File[]) => void + className?: string + children?: React.ReactNode +}) { + const { onFiles, className, children } = props + const ref = useRef(null) + return ( + <> + + onFiles(Array.from(e.target.files || []))} + /> + + ) +} diff --git a/web/lib/firebase/storage.ts b/web/lib/firebase/storage.ts index e7794580..5293a6bc 100644 --- a/web/lib/firebase/storage.ts +++ b/web/lib/firebase/storage.ts @@ -7,6 +7,7 @@ import { const storage = getStorage() +// TODO: compress large images export const uploadImage = async ( username: string, file: File, diff --git a/web/package.json b/web/package.json index 454db57c..f81950bf 100644 --- a/web/package.json +++ b/web/package.json @@ -24,6 +24,11 @@ "@nivo/core": "0.74.0", "@nivo/line": "0.74.0", "@react-query-firebase/firestore": "0.4.2", + "@tiptap/extension-character-count": "2.0.0-beta.31", + "@tiptap/extension-image": "2.0.0-beta.30", + "@tiptap/extension-placeholder": "2.0.0-beta.53", + "@tiptap/react": "2.0.0-beta.114", + "@tiptap/starter-kit": "2.0.0-beta.190", "algoliasearch": "4.13.0", "clsx": "1.1.1", "cors": "2.8.5", @@ -61,7 +66,7 @@ "next-sitemap": "^2.5.14", "postcss": "8.3.5", "prettier-plugin-tailwindcss": "^0.1.5", - "tailwindcss": "3.0.1", + "tailwindcss": "3.1.6", "tsc-files": "1.1.3" }, "lint-staged": { diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index e8b290f3..bfe13837 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -45,6 +45,7 @@ import { useTracking } from 'web/hooks/use-tracking' import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns' import { useRouter } from 'next/router' import { useLiquidity } from 'web/hooks/use-liquidity' +import { richTextToString } from 'common/util/parse' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { @@ -396,15 +397,18 @@ const getOpenGraphProps = (contract: Contract) => { creatorUsername, outcomeType, creatorAvatarUrl, + description: desc, } = contract const probPercent = outcomeType === 'BINARY' ? getBinaryProbPercent(contract) : undefined + const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc) + const description = resolution - ? `Resolved ${resolution}. ${contract.description}` + ? `Resolved ${resolution}. ${stringDesc}` : probPercent - ? `${probPercent} chance. ${contract.description}` - : contract.description + ? `${probPercent} chance. ${stringDesc}` + : stringDesc return { question, diff --git a/web/pages/api/v0/_types.ts b/web/pages/api/v0/_types.ts index 7f52077d..5b9a7dab 100644 --- a/web/pages/api/v0/_types.ts +++ b/web/pages/api/v0/_types.ts @@ -6,6 +6,7 @@ import { Contract } from 'common/contract' import { User } from 'common/user' import { removeUndefinedProps } from 'common/util/object' import { ENV_CONFIG } from 'common/envs/constants' +import { JSONContent } from '@tiptap/core' export type LiteMarket = { // Unique identifer for this market @@ -20,7 +21,7 @@ export type LiteMarket = { // Market attributes. All times are in milliseconds since epoch closeTime?: number question: string - description: string + description: string | JSONContent tags: string[] url: string outcomeType: string diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index 8cd80f7a..2fa4844e 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -36,7 +36,6 @@ export default function ContractSearchFirestore(props: { let matches = (contracts ?? []).filter( (c) => check(c.question) || - check(c.description) || check(c.creatorName) || check(c.creatorUsername) || check(c.lowercaseTags.map((tag) => `#${tag}`).join(' ')) || diff --git a/web/pages/create.tsx b/web/pages/create.tsx index fd071310..705ef0eb 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -26,6 +26,7 @@ import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' import { track } from 'web/lib/service/analytics' import { GroupSelector } from 'web/components/groups/group-selector' import { User } from 'common/user' +import { TextEditor, useTextEditor } from 'web/components/editor' type NewQuestionParams = { groupId?: string @@ -101,13 +102,11 @@ export function NewContract(props: { (params?.outcomeType as outcomeType) ?? 'BINARY' ) const [initialProb] = useState(50) - const [bottomRef, setBottomRef] = useState(null) const [minString, setMinString] = useState(params?.min ?? '') const [maxString, setMaxString] = useState(params?.max ?? '') const [isLogScale, setIsLogScale] = useState(!!params?.isLogScale) const [initialValueString, setInitialValueString] = useState(initValue) - const [description, setDescription] = useState(params?.description ?? '') useEffect(() => { if (groupId && creator) getGroup(groupId).then((group) => { @@ -152,9 +151,6 @@ export function NewContract(props: { // get days from today until the end of this year: const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day') - const hasUnsavedChanges = !isSubmitting && Boolean(question || description) - useWarnUnsavedChanges(hasUnsavedChanges) - const isValid = (outcomeType === 'BINARY' ? initialProb >= 5 && initialProb <= 95 : true) && question.length > 0 && @@ -175,6 +171,20 @@ export function NewContract(props: { min < initialValue && initialValue < max)) + const descriptionPlaceholder = + outcomeType === 'BINARY' + ? `e.g. This question resolves to "YES" if they receive the majority of votes...` + : `e.g. I will choose the answer according to...` + + const { editor, upload } = useTextEditor({ + max: MAX_DESCRIPTION_LENGTH, + placeholder: descriptionPlaceholder, + disabled: isSubmitting, + }) + + const isEditorFilled = editor != null && !editor.isEmpty + useWarnUnsavedChanges(!isSubmitting && (Boolean(question) || isEditorFilled)) + function setCloseDateInDays(days: number) { const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD') setCloseDate(newCloseDate) @@ -183,14 +193,13 @@ export function NewContract(props: { async function submit() { // TODO: Tell users why their contract is invalid if (!creator || !isValid) return - setIsSubmitting(true) try { const result = await createMarket( removeUndefinedProps({ question, outcomeType, - description, + description: editor?.getJSON(), initialProb, ante, closeTime, @@ -213,15 +222,11 @@ export function NewContract(props: { await router.push(contractPath(result as Contract)) } catch (e) { - console.log('error creating contract', e) + console.error('error creating contract', e, (e as any).details) + setIsSubmitting(false) } } - const descriptionPlaceholder = - outcomeType === 'BINARY' - ? `e.g. This question resolves to "YES" if they receive the majority of votes...` - : `e.g. I will choose the answer according to...` - if (!creator) return <> return ( @@ -396,25 +401,12 @@ export function NewContract(props: { -
-