Compare commits

..

46 Commits
main2 ... main

Author SHA1 Message Date
Phil
c762869b83
Migrate prod bot (#1052)
* Updated dev config to point to new Twitch dev bot hosting location.

* Updated prod config to point to new Twitch bot hosting location.
2022-10-14 16:41:06 +01:00
FRC
aaa09f49c0
New component to add arbitrary (but static) react embeds to posts programatically + midterms component (#1051) 2022-10-14 16:21:54 +01:00
FRC
4d214c01b4
Tipping in posts (#1045)
* Tipping in posts

* Rm itemType field
2022-10-14 13:05:07 +01:00
ingawei
0a70652667
fixed tip button bug (#1049)
* fixed tip button bug
2022-10-14 02:20:32 -05:00
mqp
b4162a0896 Auto-prettification 2022-10-14 06:49:23 +00:00
ingawei
2ecece02c3 Auto-remove unused imports 2022-10-14 06:48:40 +00:00
ingawei
4359ad0530
added cancelling tipping and lil coin (#1047)
* added cancelling tipping and lil coin
* forced timeout to fix weird toast bug
2022-10-14 01:47:51 -05:00
ingawei
3f8988bf27
Inga/colorful answer comments (#1040)
* changing answers in comments to match colors, no longer indenting those responses
2022-10-14 00:07:54 -05:00
Austin Chen
41a46aad9b Update stability-client to fix /dream 2022-10-13 21:44:22 -07:00
James Grugett
4097082c75
Dynamically choose outcome in Position tooltip (#1048) 2022-10-13 23:29:57 -05:00
mantikoros
58cd0e57bd track tournaments 2022-10-13 21:22:32 -05:00
Austin Chen
3a63503161 Fix @/% suggestion regression
See https://github.com/ueberdosis/tiptap/pull/3239
2022-10-13 19:06:03 -07:00
mantikoros
abd06f272b contract card: show traders instead of volume 2022-10-13 21:05:21 -05:00
Austin Chen
ab1c3020da Warn users about Dream breakage 2022-10-13 18:45:09 -07:00
Sinclair Chen
c044460a91
Don't break build on unused import (#1046)
* Fix lint warnings

* Let vercel build when unused import
2022-10-13 18:16:22 -07:00
mantikoros
7a6725ee77 contract card: less busy design 2022-10-13 19:56:16 -05:00
Austin Chen
823d1ddd4c Update stability-client 2022-10-13 17:01:05 -07:00
Austin Chen
7d490e0de1 Expand image on hover 2022-10-13 16:23:25 -07:00
Sinclair Chen
96d2255cb1 fix find and replace mistake 2022-10-13 15:15:45 -07:00
Sinclair Chen
4a139c5cc2 Fix limit prob styling 2022-10-13 14:55:48 -07:00
James Grugett
47eb8abed0 Don't include loans in email payout message 2022-10-13 16:05:40 -05:00
Sinclair Chen
903fcc83b3 Fix form-control styling 2022-10-13 13:20:04 -07:00
Austin Chen
0615bb2d4b
List ManifoldDream as a bot 2022-10-13 13:12:22 -07:00
mantikoros
3508c94634 tip button: fix tip color 2022-10-13 14:33:41 -05:00
mantikoros
29c0dfe3fe tip button: remove fill color 2022-10-13 14:27:47 -05:00
mantikoros
18a3b66164 dream label 2022-10-13 14:16:30 -05:00
James Grugett
9e4f41253f Filter out resolved markets from daily movers 2022-10-13 13:49:48 -05:00
Sinclair Chen
546b0231e7
Die Daisy (#1042)
* un-daisy labels

* un-daisy inputs

* un-daisy input groups

* fixup input

* un-daisy selects

* un-daisy slider

* Uninstall daisy. Migrate colors

* un-daisy tables

* fix input error styling

* lint
2022-10-13 11:23:42 -07:00
Phil
8bb44593f3
Updated dev config to point to new Twitch dev bot hosting location. (#1044) 2022-10-13 17:19:50 +01:00
Pico2x
2c2bc61788 Fix bug with parsing in abritrary react components 2022-10-13 16:56:35 +01:00
Pico2x
34c9dbb3e7 Revert "Revert "New implementation of market card embeddings (#1025)""
This reverts commit d6525bae9f.
2022-10-13 16:25:15 +01:00
Ian Philips
d6525bae9f Revert "New implementation of market card embeddings (#1025)"
This reverts commit 3fc53112b9.
2022-10-13 09:19:28 -06:00
Ian Philips
da32a756a8 Need not follow contract for tag notification 2022-10-13 08:41:33 -06:00
Ian Philips
fa476c78dd Handle numeric outcomes in movers 2022-10-13 08:18:16 -06:00
Ian Philips
e7ba7e715f default cursor on open answer 2022-10-13 08:04:01 -06:00
Ian Philips
9bf82c6082 Match colors on portfolio 2022-10-13 08:00:04 -06:00
Ian Philips
3e1876f0dc Add flaggedByUsernames to firestore rules 2022-10-13 07:58:14 -06:00
ingawei
5ba4a9dce7
Inga/tip button (#1043)
* added tip jar
* made market actions/comments and manalink buttons IconButtons
2022-10-13 01:53:26 -05:00
James Grugett
4e5b78f4ee Use in-memory store for home featured section data 2022-10-12 21:15:40 -05:00
James Grugett
bc6fab399e Make text grayer 2022-10-12 20:51:48 -05:00
James Grugett
c2d112e516 Position => shares 2022-10-12 20:51:20 -05:00
ingawei
3cbe8ad8bb
Inga/comment bounty fix (#1041)
* fixed bounty button in comments
2022-10-12 20:34:07 -05:00
ingawei
6226291e02 made comments smaller 2022-10-12 17:53:37 -07:00
Austin Chen
fa4dba4da3 Document the new /comment endpoint 2022-10-12 17:26:00 -07:00
Austin Chen
9eff69be75
Add /createcomment API endpoint (#946)
* /dream api: Upload StableDiffusion image to Firestore

* Minor tweaks

* Set content type on uploaded image

This makes it so the image doesn't auto-download when opened in a new tab

* Allow users to dream directly from within Manifold

* Remove unused import

* Implement a /comment endpoint which supports html and markdown

* Upgrade @tiptap/core to latest

* Update all tiptap deps to beta.199

* Add @tiptap/suggestion

* Import @tiptap/html in the right place

* ... add deps everywhere

So I have no idea how common deps work apparently

* Add tiptap/suggestion too

* Clean up dream

* More cleanups

* Rework /comment endpoint

* Move API to /comment

* Change imports in case that matters

* Add a couple todos

* Dynamically import micromark

* Parallellize gsutil with -m option

* Adding comments via api working, editor.tsx erroring out

* Unused import

* Remove disabled state from useTextEditor

Co-authored-by: Ian Philips <iansphilips@gmail.com>
2022-10-12 17:25:17 -07:00
James Grugett
789bec2a4f Expand replies by default (quick fix for comment links not working) 2022-10-12 17:49:04 -05:00
100 changed files with 1920 additions and 1111 deletions

View File

@ -26,7 +26,7 @@ module.exports = {
caughtErrorsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_',
}, },
], ],
'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-imports': 'warn',
}, },
}, },
], ],

View File

@ -595,7 +595,8 @@ In addition to housing impact litigation, we provide free legal aid, education a
photo: photo:
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2Fdefault%2Fci2h3hStFM.47?alt=media&token=0d2cdc3d-e4d8-4f5e-8f23-4a586b6ff637', 'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2Fdefault%2Fci2h3hStFM.47?alt=media&token=0d2cdc3d-e4d8-4f5e-8f23-4a586b6ff637',
preview: 'Donate supplies to soldiers in Ukraine', preview: 'Donate supplies to soldiers in Ukraine',
description: 'Donate supplies to soldiers in Ukraine, including tourniquets and plate carriers.', description:
'Donate supplies to soldiers in Ukraine, including tourniquets and plate carriers.',
}, },
].map((charity) => { ].map((charity) => {
const slug = charity.name.toLowerCase().replace(/\s/g, '-') const slug = charity.name.toLowerCase().replace(/\s/g, '-')

View File

@ -10,6 +10,7 @@ export type AnyOutcomeType =
| PseudoNumeric | PseudoNumeric
| FreeResponse | FreeResponse
| Numeric | Numeric
export type AnyContractType = export type AnyContractType =
| (CPMM & Binary) | (CPMM & Binary)
| (CPMM & PseudoNumeric) | (CPMM & PseudoNumeric)

View File

@ -16,7 +16,6 @@ export const DEV_CONFIG: EnvConfig = {
cloudRunId: 'w3txbmd3ba', cloudRunId: 'w3txbmd3ba',
cloudRunRegion: 'uc', cloudRunRegion: 'uc',
amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3', amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3',
// this is Phil's deployment twitchBotEndpoint: 'https://dev-twitch-bot.manifold.markets',
twitchBotEndpoint: 'https://king-prawn-app-5btyw.ondigitalocean.app',
sprigEnvironmentId: 'Tu7kRZPm7daP', sprigEnvironmentId: 'Tu7kRZPm7daP',
} }

View File

@ -70,7 +70,7 @@ export const PROD_CONFIG: EnvConfig = {
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7', appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
measurementId: 'G-SSFK1Q138D', measurementId: 'G-SSFK1Q138D',
}, },
twitchBotEndpoint: 'https://twitch-bot-nggbo3neva-uc.a.run.app', twitchBotEndpoint: 'https://twitch-bot.manifold.markets',
cloudRunId: 'nggbo3neva', cloudRunId: 'nggbo3neva',
cloudRunRegion: 'uc', cloudRunRegion: 'uc',
adminEmails: [ adminEmails: [

View File

@ -1,8 +1,9 @@
export type Like = { export type Like = {
id: string // will be id of the object liked, i.e. contract.id id: string // will be id of the object liked, i.e. contract.id
userId: string userId: string
type: 'contract' type: 'contract' | 'post'
createdTime: number createdTime: number
tipTxnId?: string // only holds most recent tip txn id tipTxnId?: string // only holds most recent tip txn id
} }
export const LIKE_TIP_AMOUNT = 10 export const LIKE_TIP_AMOUNT = 10
export const TIP_UNDO_DURATION = 2000

View File

@ -8,11 +8,13 @@
}, },
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@tiptap/core": "2.0.0-beta.182", "@tiptap/core": "2.0.0-beta.199",
"@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-image": "2.0.0-beta.199",
"@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-link": "2.0.0-beta.199",
"@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/extension-mention": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.191", "@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"lodash": "4.17.21" "lodash": "4.17.21"
}, },
"devDependencies": { "devDependencies": {

View File

@ -13,6 +13,9 @@ export type Post = {
creatorName: string creatorName: string
creatorUsername: string creatorUsername: string
creatorAvatarUrl?: string creatorAvatarUrl?: string
likedByUserIds?: string[]
likedByUserCount?: number
} }
export type DateDoc = Post & { export type DateDoc = Post & {

View File

@ -1,4 +1,5 @@
import { generateText, JSONContent } from '@tiptap/core' import { generateText, JSONContent, Node } from '@tiptap/core'
import { generateJSON } from '@tiptap/html'
// Tiptap starter extensions // Tiptap starter extensions
import { Blockquote } from '@tiptap/extension-blockquote' import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold' import { Bold } from '@tiptap/extension-bold'
@ -51,6 +52,26 @@ export function parseMentions(data: JSONContent): string[] {
return uniq(mentions) return uniq(mentions)
} }
// TODO: this is a hack to get around the fact that tiptap doesn't have a
// way to add a node view without bundling in tsx
function skippableComponent(name: string): Node<any, any> {
return Node.create({
name,
group: 'block',
content: 'inline*',
parseHTML() {
return [
{
tag: 'grid-cards-component',
},
]
},
})
}
const stringParseExts = [ const stringParseExts = [
// StarterKit extensions // StarterKit extensions
Blockquote, Blockquote,
@ -78,6 +99,8 @@ const stringParseExts = [
renderText: ({ node }) => renderText: ({ node }) =>
'[embed]' + node.attrs.src ? `(${node.attrs.src})` : '', '[embed]' + node.attrs.src ? `(${node.attrs.src})` : '',
}), }),
skippableComponent('gridCardsComponent'),
skippableComponent('staticReactEmbedComponent'),
TiptapTweet.extend({ renderText: () => '[tweet]' }), TiptapTweet.extend({ renderText: () => '[tweet]' }),
TiptapSpoiler.extend({ renderHTML: () => ['span', '[spoiler]', 0] }), TiptapSpoiler.extend({ renderHTML: () => ['span', '[spoiler]', 0] }),
] ]
@ -86,3 +109,7 @@ export function richTextToString(text?: JSONContent) {
if (!text) return '' if (!text) return ''
return generateText(text, stringParseExts) return generateText(text, stringParseExts)
} }
export function htmlToRichText(html: string) {
return generateJSON(html, stringParseExts)
}

View File

@ -680,6 +680,17 @@ $ curl https://manifold.markets/api/v0/market/{marketId}/sell -X POST \
--data-raw '{"outcome": "YES", "shares": 10}' --data-raw '{"outcome": "YES", "shares": 10}'
``` ```
### `POST /v0/comment`
Creates a comment in the specified market. Only supports top-level comments for now.
Parameters:
- `contractId`: Required. The ID of the market to comment on.
- `content`: The comment to post, formatted as [TipTap json](https://tiptap.dev/guide/output#option-1-json), OR
- `html`: The comment to post, formatted as an HTML string, OR
- `markdown`: The comment to post, formatted as a markdown string.
### `GET /v0/bets` ### `GET /v0/bets`
Gets a list of bets, ordered by creation date descending. Gets a list of bets, ordered by creation date descending.

View File

@ -110,7 +110,7 @@ service cloud.firestore {
match /contracts/{contractId} { match /contracts/{contractId} {
allow read; allow read;
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks']); .hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks', 'flaggedByUsernames']);
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['description', 'closeTime', 'question', 'visibility', 'unlistedById']) .hasOnly(['description', 'closeTime', 'question', 'visibility', 'unlistedById'])
&& resource.data.creatorId == request.auth.uid; && resource.data.creatorId == request.auth.uid;

View File

@ -26,7 +26,7 @@ module.exports = {
caughtErrorsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_',
}, },
], ],
'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-imports': 'warn',
}, },
}, },
], ],

View File

@ -15,9 +15,9 @@
"dev": "nodemon src/serve.ts", "dev": "nodemon src/serve.ts",
"localDbScript": "firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export", "localDbScript": "firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export",
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export", "serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export",
"db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export", "db:update-local-from-remote": "yarn db:backup-remote && gsutil -m rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
"db:backup-local": "firebase emulators:export --force ./firestore_export", "db:backup-local": "firebase emulators:export --force ./firestore_export",
"db:rename-remote-backup-folder": "gsutil mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)", "db:rename-remote-backup-folder": "gsutil -m mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)",
"db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/", "db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/",
"verify": "(cd .. && yarn verify)", "verify": "(cd .. && yarn verify)",
"verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty" "verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty"
@ -26,11 +26,13 @@
"dependencies": { "dependencies": {
"@amplitude/node": "1.10.0", "@amplitude/node": "1.10.0",
"@google-cloud/functions-framework": "3.1.2", "@google-cloud/functions-framework": "3.1.2",
"@tiptap/core": "2.0.0-beta.182", "@tiptap/core": "2.0.0-beta.199",
"@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-image": "2.0.0-beta.199",
"@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-link": "2.0.0-beta.199",
"@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/extension-mention": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.191", "@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"cors": "2.8.5", "cors": "2.8.5",
"dayjs": "1.11.4", "dayjs": "1.11.4",
"express": "4.18.1", "express": "4.18.1",
@ -38,6 +40,7 @@
"firebase-functions": "3.21.2", "firebase-functions": "3.21.2",
"lodash": "4.17.21", "lodash": "4.17.21",
"mailgun-js": "0.22.0", "mailgun-js": "0.22.0",
"marked": "4.1.1",
"module-alias": "2.2.2", "module-alias": "2.2.2",
"node-fetch": "2", "node-fetch": "2",
"stripe": "8.194.0", "stripe": "8.194.0",
@ -45,6 +48,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/mailgun-js": "0.22.12", "@types/mailgun-js": "0.22.12",
"@types/marked": "4.0.7",
"@types/module-alias": "2.0.1", "@types/module-alias": "2.0.1",
"@types/node-fetch": "2.6.2", "@types/node-fetch": "2.6.2",
"firebase-functions-test": "0.3.3", "firebase-functions-test": "0.3.3",

View File

@ -0,0 +1,105 @@
import * as admin from 'firebase-admin'
import { getContract, getUser, log } from './utils'
import { APIError, newEndpoint, validate } from './api'
import { JSONContent } from '@tiptap/core'
import { z } from 'zod'
import { removeUndefinedProps } from '../../common/util/object'
import { htmlToRichText } from '../../common/util/parse'
import { marked } from 'marked'
const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection(
z.record(z.any()),
z.object({
type: z.string().optional(),
attrs: z.record(z.any()).optional(),
content: z.array(contentSchema).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 postSchema = z.object({
contractId: z.string(),
content: contentSchema.optional(),
html: z.string().optional(),
markdown: z.string().optional(),
})
const MAX_COMMENT_JSON_LENGTH = 20000
// For now, only supports creating a new top-level comment on a contract.
// Replies, posts, chats are not supported yet.
export const createcomment = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore()
const { contractId, content, html, markdown } = validate(postSchema, req.body)
const creator = await getUser(auth.uid)
const contract = await getContract(contractId)
if (!creator) {
throw new APIError(400, 'No user exists with the authenticated user ID.')
}
if (!contract) {
throw new APIError(400, 'No contract exists with the given ID.')
}
let contentJson = null
if (content) {
contentJson = content
} else if (html) {
console.log('html', html)
contentJson = htmlToRichText(html)
} else if (markdown) {
const markedParse = marked.parse(markdown)
log('parsed', markedParse)
contentJson = htmlToRichText(markedParse)
log('json', contentJson)
}
if (!contentJson) {
throw new APIError(400, 'No comment content provided.')
}
if (JSON.stringify(contentJson).length > MAX_COMMENT_JSON_LENGTH) {
throw new APIError(
400,
`Comment is too long; should be less than ${MAX_COMMENT_JSON_LENGTH} as a JSON string.`
)
}
const ref = firestore.collection(`contracts/${contractId}/comments`).doc()
const comment = removeUndefinedProps({
id: ref.id,
content: contentJson,
createdTime: Date.now(),
userId: creator.id,
userName: creator.name,
userUsername: creator.username,
userAvatarUrl: creator.avatarUrl,
// OnContract fields
commentType: 'contract',
contractId: contractId,
contractSlug: contract.slug,
contractQuestion: contract.question,
})
await ref.set(comment)
return { status: 'success', comment }
})

View File

@ -197,6 +197,7 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async (
return await notificationRef.set(removeUndefinedProps(notification)) return await notificationRef.set(removeUndefinedProps(notification))
} }
const needNotFollowContractReasons = ['tagged_user']
const stillFollowingContract = (userId: string) => { const stillFollowingContract = (userId: string) => {
return contractFollowersIds.includes(userId) return contractFollowersIds.includes(userId)
} }
@ -205,7 +206,12 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async (
userId: string, userId: string,
reason: notification_reason_types reason: notification_reason_types
) => { ) => {
if (!stillFollowingContract(userId) || sourceUser.id == userId) return if (
(!stillFollowingContract(userId) &&
!needNotFollowContractReasons.includes(reason)) ||
sourceUser.id == userId
)
return
const privateUser = await getPrivateUser(userId) const privateUser = await getPrivateUser(userId)
if (!privateUser) return if (!privateUser) return
const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser( const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser(

View File

@ -103,6 +103,7 @@ export const createpost = newEndpoint({}, async (req, auth) => {
creatorName: creator.name, creatorName: creator.name,
creatorUsername: creator.username, creatorUsername: creator.username,
creatorAvatarUrl: creator.avatarUrl, creatorAvatarUrl: creator.avatarUrl,
itemType: 'post',
}) })
await postRef.create(post) await postRef.create(post)

View File

@ -65,6 +65,7 @@ import { sellbet } from './sell-bet'
import { sellshares } from './sell-shares' import { sellshares } from './sell-shares'
import { claimmanalink } from './claim-manalink' import { claimmanalink } from './claim-manalink'
import { createmarket } from './create-market' import { createmarket } from './create-market'
import { createcomment } from './create-comment'
import { addcommentbounty, awardcommentbounty } from './update-comment-bounty' import { addcommentbounty, awardcommentbounty } from './update-comment-bounty'
import { creategroup } from './create-group' import { creategroup } from './create-group'
import { resolvemarket } from './resolve-market' import { resolvemarket } from './resolve-market'
@ -94,6 +95,7 @@ const claimManalinkFunction = toCloudFunction(claimmanalink)
const createMarketFunction = toCloudFunction(createmarket) const createMarketFunction = toCloudFunction(createmarket)
const addSubsidyFunction = toCloudFunction(addsubsidy) const addSubsidyFunction = toCloudFunction(addsubsidy)
const addCommentBounty = toCloudFunction(addcommentbounty) const addCommentBounty = toCloudFunction(addcommentbounty)
const createCommentFunction = toCloudFunction(createcomment)
const awardCommentBounty = toCloudFunction(awardcommentbounty) const awardCommentBounty = toCloudFunction(awardcommentbounty)
const createGroupFunction = toCloudFunction(creategroup) const createGroupFunction = toCloudFunction(creategroup)
const resolveMarketFunction = toCloudFunction(resolvemarket) const resolveMarketFunction = toCloudFunction(resolvemarket)
@ -130,6 +132,7 @@ export {
acceptChallenge as acceptchallenge, acceptChallenge as acceptchallenge,
createPostFunction as createpost, createPostFunction as createpost,
saveTwitchCredentials as savetwitchcredentials, saveTwitchCredentials as savetwitchcredentials,
createCommentFunction as createcomment,
addCommentBounty as addcommentbounty, addCommentBounty as addcommentbounty,
awardCommentBounty as awardcommentbounty, awardCommentBounty as awardcommentbounty,
updateMetricsFunction as updatemetrics, updateMetricsFunction as updatemetrics,

View File

@ -171,12 +171,12 @@ export const resolveMarket = async (
const openBets = bets.filter((b) => !b.isSold && !b.sale) const openBets = bets.filter((b) => !b.isSold && !b.sale)
const loanPayouts = getLoanPayouts(openBets) const loanPayouts = getLoanPayouts(openBets)
const payouts = [ const payoutsWithoutLoans = [
{ userId: creatorId, payout: creatorPayout, deposit: creatorPayout }, { userId: creatorId, payout: creatorPayout, deposit: creatorPayout },
...liquidityPayouts.map((p) => ({ ...p, deposit: p.payout })), ...liquidityPayouts.map((p) => ({ ...p, deposit: p.payout })),
...traderPayouts, ...traderPayouts,
...loanPayouts,
] ]
const payouts = [...payoutsWithoutLoans, ...loanPayouts]
if (!isProd()) if (!isProd())
console.log( console.log(
@ -208,7 +208,7 @@ export const resolveMarket = async (
await undoUniqueBettorRewardsIfCancelResolution(contract, outcome) await undoUniqueBettorRewardsIfCancelResolution(contract, outcome)
await revalidateStaticProps(getContractPath(contract)) await revalidateStaticProps(getContractPath(contract))
const userPayoutsWithoutLoans = groupPayoutsByUser(payouts) const userPayoutsWithoutLoans = groupPayoutsByUser(payoutsWithoutLoans)
const userInvestments = mapValues( const userInvestments = mapValues(
groupBy(bets, (bet) => bet.userId), groupBy(bets, (bet) => bet.userId),

View File

@ -19,6 +19,7 @@ import { sellbet } from './sell-bet'
import { sellshares } from './sell-shares' import { sellshares } from './sell-shares'
import { claimmanalink } from './claim-manalink' import { claimmanalink } from './claim-manalink'
import { createmarket } from './create-market' import { createmarket } from './create-market'
import { createcomment } from './create-comment'
import { creategroup } from './create-group' import { creategroup } from './create-group'
import { resolvemarket } from './resolve-market' import { resolvemarket } from './resolve-market'
import { unsubscribe } from './unsubscribe' import { unsubscribe } from './unsubscribe'
@ -53,6 +54,7 @@ addJsonEndpointRoute('/transact', transact)
addJsonEndpointRoute('/changeuserinfo', changeuserinfo) addJsonEndpointRoute('/changeuserinfo', changeuserinfo)
addJsonEndpointRoute('/createuser', createuser) addJsonEndpointRoute('/createuser', createuser)
addJsonEndpointRoute('/createanswer', createanswer) addJsonEndpointRoute('/createanswer', createanswer)
addJsonEndpointRoute('/createcomment', createcomment)
addJsonEndpointRoute('/placebet', placebet) addJsonEndpointRoute('/placebet', placebet)
addJsonEndpointRoute('/cancelbet', cancelbet) addJsonEndpointRoute('/cancelbet', cancelbet)
addJsonEndpointRoute('/sellbet', sellbet) addJsonEndpointRoute('/sellbet', sellbet)

View File

@ -22,8 +22,9 @@ module.exports = {
'@next/next/no-typos': 'off', '@next/next/no-typos': 'off',
'linebreak-style': ['error', 'unix'], 'linebreak-style': ['error', 'unix'],
'lodash/import-scope': [2, 'member'], 'lodash/import-scope': [2, 'member'],
'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-imports': 'warn',
}, },
ignorePatterns: ['/public/mtg/*'],
env: { env: {
browser: true, browser: true,
node: true, node: true,

View File

@ -35,7 +35,7 @@ export function AddFundsModal(props: {
<div className="text-xl">{manaToUSD(amountSelected)}</div> <div className="text-xl">{manaToUSD(amountSelected)}</div>
</div> </div>
<div className="modal-action"> <div className="flex">
<Button color="gray-white" onClick={() => setOpen(false)}> <Button color="gray-white" onClick={() => setOpen(false)}>
Back Back
</Button> </Button>

View File

@ -7,6 +7,8 @@ import { ENV_CONFIG } from 'common/envs/constants'
import { Row } from './layout/row' import { Row } from './layout/row'
import { AddFundsModal } from './add-funds-modal' import { AddFundsModal } from './add-funds-modal'
import { Input } from './input' import { Input } from './input'
import Slider from 'rc-slider'
import 'rc-slider/assets/index.css'
export function AmountInput(props: { export function AmountInput(props: {
amount: number | undefined amount: number | undefined
@ -40,17 +42,13 @@ export function AmountInput(props: {
return ( return (
<> <>
<Col className={className}> <Col className={clsx('relative', className)}>
<label className="font-sm md:font-lg relative"> <label className="font-sm md:font-lg relative">
<span className="text-greyscale-4 absolute top-1/2 my-auto ml-2 -translate-y-1/2"> <span className="text-greyscale-4 absolute top-1/2 my-auto ml-2 -translate-y-1/2">
{label} {label}
</span> </span>
<Input <Input
className={clsx( className={clsx('w-24 pl-9 !text-base md:w-auto', inputClassName)}
'w-24 pl-9 !text-base md:w-auto',
error && 'input-error',
inputClassName
)}
ref={inputRef} ref={inputRef}
type="text" type="text"
pattern="[0-9]*" pattern="[0-9]*"
@ -58,13 +56,14 @@ export function AmountInput(props: {
placeholder="0" placeholder="0"
maxLength={6} maxLength={6}
value={amount ?? ''} value={amount ?? ''}
error={!!error}
disabled={disabled} disabled={disabled}
onChange={(e) => onAmountChange(e.target.value)} onChange={(e) => onAmountChange(e.target.value)}
/> />
</label> </label>
{error && ( {error && (
<div className="absolute mt-11 whitespace-nowrap text-xs font-medium tracking-wide text-red-500"> <div className="absolute -bottom-5 whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
{error === 'Insufficient balance' ? ( {error === 'Insufficient balance' ? (
<> <>
Not enough funds. Not enough funds.
@ -148,7 +147,7 @@ export function BuyAmountInput(props: {
return ( return (
<> <>
<Row className="gap-4"> <Row className="items-center gap-4">
<AmountInput <AmountInput
amount={amount} amount={amount}
onChange={onAmountChange} onChange={onAmountChange}
@ -160,14 +159,23 @@ export function BuyAmountInput(props: {
inputRef={inputRef} inputRef={inputRef}
/> />
{showSlider && ( {showSlider && (
<input <Slider
type="range" min={0}
min="0" max={205}
max="205"
value={getRaw(amount ?? 0)} value={getRaw(amount ?? 0)}
onChange={(e) => onAmountChange(parseRaw(parseInt(e.target.value)))} onChange={(value) => onAmountChange(parseRaw(value as number))}
className="range range-lg only-thumb my-auto align-middle xl:hidden" className="mx-4 !h-4 xl:hidden [&>.rc-slider-rail]:bg-gray-200 [&>.rc-slider-track]:bg-indigo-400 [&>.rc-slider-handle]:bg-indigo-400"
step="5" railStyle={{ height: 16, top: 0, left: 0 }}
trackStyle={{ height: 16, top: 0 }}
handleStyle={{
height: 32,
width: 32,
opacity: 1,
border: 'none',
boxShadow: 'none',
top: -2,
}}
step={5}
/> />
)} )}
</Row> </Row>

View File

@ -100,8 +100,8 @@ export function AnswerItem(props: {
</div> </div>
))} ))}
{showChoice ? ( {showChoice ? (
<div className="form-control py-1"> <div className="flex flex-col py-1">
<label className="label cursor-pointer gap-3"> <label className="cursor-pointer gap-3 px-1 py-2">
<span className="">Choose this answer</span> <span className="">Choose this answer</span>
{showChoice === 'radio' && ( {showChoice === 'radio' && (
<input <input

View File

@ -27,6 +27,13 @@ import { CHOICE_ANSWER_COLORS } from '../charts/contract/choice'
import { useChartAnswers } from '../charts/contract/choice' import { useChartAnswers } from '../charts/contract/choice'
import { ChatIcon } from '@heroicons/react/outline' import { ChatIcon } from '@heroicons/react/outline'
export function getAnswerColor(answer: Answer, answersArray: string[]) {
const colorIndex = answersArray.indexOf(answer.text)
return colorIndex != undefined && colorIndex < CHOICE_ANSWER_COLORS.length
? CHOICE_ANSWER_COLORS[colorIndex]
: '#B1B1C7'
}
export function AnswersPanel(props: { export function AnswersPanel(props: {
contract: FreeResponseContract | MultipleChoiceContract contract: FreeResponseContract | MultipleChoiceContract
onAnswerCommentClick: (answer: Answer) => void onAnswerCommentClick: (answer: Answer) => void
@ -107,8 +114,8 @@ export function AnswersPanel(props: {
? 'checkbox' ? 'checkbox'
: undefined : undefined
const colorSortedAnswer = useChartAnswers(contract).map( const answersArray = useChartAnswers(contract).map(
(value, _index) => value.text (answer, _index) => answer.text
) )
return ( return (
@ -139,8 +146,8 @@ export function AnswersPanel(props: {
key={item.id} key={item.id}
answer={item} answer={item}
contract={contract} contract={contract}
colorIndex={colorSortedAnswer.indexOf(item.text)}
onAnswerCommentClick={onAnswerCommentClick} onAnswerCommentClick={onAnswerCommentClick}
color={getAnswerColor(item, answersArray)}
/> />
))} ))}
{hasZeroBetAnswers && !showAllAnswers && ( {hasZeroBetAnswers && !showAllAnswers && (
@ -185,18 +192,14 @@ export function AnswersPanel(props: {
function OpenAnswer(props: { function OpenAnswer(props: {
contract: FreeResponseContract | MultipleChoiceContract contract: FreeResponseContract | MultipleChoiceContract
answer: Answer answer: Answer
colorIndex: number | undefined color: string
onAnswerCommentClick: (answer: Answer) => void onAnswerCommentClick: (answer: Answer) => void
}) { }) {
const { answer, contract, colorIndex, onAnswerCommentClick } = props const { answer, contract, onAnswerCommentClick, color } = props
const { username, avatarUrl, text } = answer const { username, avatarUrl, text } = answer
const prob = getDpmOutcomeProbability(contract.totalShares, answer.id) const prob = getDpmOutcomeProbability(contract.totalShares, answer.id)
const probPercent = formatPercent(prob) const probPercent = formatPercent(prob)
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const color =
colorIndex != undefined && colorIndex < CHOICE_ANSWER_COLORS.length
? CHOICE_ANSWER_COLORS[colorIndex] + '55' // semi-transparent
: '#B1B1C755'
const colorWidth = 100 * Math.max(prob, 0.01) const colorWidth = 100 * Math.max(prob, 0.01)
return ( return (
@ -217,7 +220,7 @@ function OpenAnswer(props: {
tradingAllowed(contract) ? 'text-greyscale-7' : 'text-greyscale-5' tradingAllowed(contract) ? 'text-greyscale-7' : 'text-greyscale-5'
)} )}
style={{ style={{
background: `linear-gradient(to right, ${color} ${colorWidth}%, #FBFBFF ${colorWidth}%)`, background: `linear-gradient(to right, ${color}90 ${colorWidth}%, #FBFBFF ${colorWidth}%)`,
}} }}
> >
<Row className="z-20 -mb-1 justify-between gap-2 py-2 px-3"> <Row className="z-20 -mb-1 justify-between gap-2 py-2 px-3">
@ -227,10 +230,7 @@ function OpenAnswer(props: {
username={username} username={username}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
/> />
<Linkify <Linkify className="text-md whitespace-pre-line" text={text} />
className="text-md cursor-pointer whitespace-pre-line"
text={text}
/>
</Row> </Row>
<Row className="gap-2"> <Row className="gap-2">
<div className="my-auto text-xl">{probPercent}</div> <div className="my-auto text-xl">{probPercent}</div>

View File

@ -5,7 +5,6 @@ import { awardCommentBounty } from 'web/lib/firebase/api'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { Row } from './layout/row' import { Row } from './layout/row'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { TextButton } from 'web/components/text-button'
import { COMMENT_BOUNTY_AMOUNT } from 'common/economy' import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
@ -37,10 +36,17 @@ export function AwardBountyButton(prop: {
const canUp = me && me.id !== comment.userId && contract.creatorId === me.id const canUp = me && me.id !== comment.userId && contract.creatorId === me.id
if (!canUp) return <div /> if (!canUp) return <div />
return ( return (
<Row className={clsx('-ml-2 items-center gap-0.5', !canUp ? '-ml-6' : '')}> <Row
<TextButton className={'font-bold'} onClick={submit}> className={clsx('my-auto items-center gap-0.5', !canUp ? '-ml-6' : '')}
>
<button
className={
'rounded-full border border-indigo-400 bg-indigo-50 py-0.5 px-2 text-xs text-indigo-400 transition-colors hover:bg-indigo-400 hover:text-white'
}
onClick={submit}
>
Award {formatMoney(COMMENT_BOUNTY_AMOUNT)} Award {formatMoney(COMMENT_BOUNTY_AMOUNT)}
</TextButton> </button>
</Row> </Row>
) )
} }

View File

@ -92,10 +92,7 @@ export function BetInline(props: {
/> />
<BuyAmountInput <BuyAmountInput
className="-mb-4" className="-mb-4"
inputClassName={clsx( inputClassName="w-20 !text-base"
'input-sm w-20 !text-base',
error && 'input-error'
)}
amount={amount} amount={amount}
onChange={setAmount} onChange={setAmount}
error="" // handle error ourselves error="" // handle error ourselves

View File

@ -631,9 +631,9 @@ function LimitOrderPanel(props: {
return ( return (
<Col className={hidden ? 'hidden' : ''}> <Col className={hidden ? 'hidden' : ''}>
<Row className="mt-1 items-center gap-4"> <Row className="mt-1 mb-4 items-center gap-4">
<Col className="gap-2"> <Col className="gap-2">
<div className="relative ml-1 text-sm text-gray-500"> <div className="text-sm text-gray-500">
Buy {isPseudoNumeric ? <HigherLabel /> : <YesLabel />} up to Buy {isPseudoNumeric ? <HigherLabel /> : <YesLabel />} up to
</div> </div>
<ProbabilityOrNumericInput <ProbabilityOrNumericInput
@ -641,10 +641,11 @@ function LimitOrderPanel(props: {
prob={lowLimitProb} prob={lowLimitProb}
setProb={setLowLimitProb} setProb={setLowLimitProb}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
placeholder="10"
/> />
</Col> </Col>
<Col className="gap-2"> <Col className="gap-2">
<div className="ml-1 text-sm text-gray-500"> <div className="text-sm text-gray-500">
Buy {isPseudoNumeric ? <LowerLabel /> : <NoLabel />} down to Buy {isPseudoNumeric ? <LowerLabel /> : <NoLabel />} down to
</div> </div>
<ProbabilityOrNumericInput <ProbabilityOrNumericInput
@ -652,6 +653,7 @@ function LimitOrderPanel(props: {
prob={highLimitProb} prob={highLimitProb}
setProb={setHighLimitProb} setProb={setHighLimitProb}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
placeholder="90"
/> />
</Col> </Col>
</Row> </Row>
@ -980,11 +982,11 @@ export function SellPanel(props: {
<Col className="mt-3 w-full gap-3 text-sm"> <Col className="mt-3 w-full gap-3 text-sm">
<Row className="items-center justify-between gap-2 text-gray-500"> <Row className="items-center justify-between gap-2 text-gray-500">
Sale amount Sale amount
<span className="text-neutral">{formatMoney(saleValue)}</span> <span className="text-gray-700">{formatMoney(saleValue)}</span>
</Row> </Row>
<Row className="items-center justify-between gap-2 text-gray-500"> <Row className="items-center justify-between gap-2 text-gray-500">
Profit Profit
<span className="text-neutral">{formatMoney(profit)}</span> <span className="text-gray-700">{formatMoney(profit)}</span>
</Row> </Row>
<Row className="items-center justify-between"> <Row className="items-center justify-between">
<div className="text-gray-500"> <div className="text-gray-500">
@ -1000,11 +1002,11 @@ export function SellPanel(props: {
<> <>
<Row className="mt-6 items-center justify-between gap-2 text-gray-500"> <Row className="mt-6 items-center justify-between gap-2 text-gray-500">
Loan payment Loan payment
<span className="text-neutral">{formatMoney(-loanPaid)}</span> <span className="text-gray-700">{formatMoney(-loanPaid)}</span>
</Row> </Row>
<Row className="items-center justify-between gap-2 text-gray-500"> <Row className="items-center justify-between gap-2 text-gray-500">
Net proceeds Net proceeds
<span className="text-neutral">{formatMoney(netProceeds)}</span> <span className="text-gray-700">{formatMoney(netProceeds)}</span>
</Row> </Row>
</> </>
)} )}

View File

@ -25,10 +25,8 @@ export function BetsSummary(props: {
const isBinary = outcomeType === 'BINARY' const isBinary = outcomeType === 'BINARY'
const bets = props.userBets.filter((b) => !b.isAnte) const bets = props.userBets.filter((b) => !b.isAnte)
const { profitPercent, payout, profit, invested } = getContractBetMetrics( const { profitPercent, payout, profit, invested, hasShares } =
contract, getContractBetMetrics(contract, bets)
bets
)
const excludeSales = bets.filter((b) => !b.isSold && !b.sale) const excludeSales = bets.filter((b) => !b.isSold && !b.sale)
const yesWinnings = sumBy(excludeSales, (bet) => const yesWinnings = sumBy(excludeSales, (bet) =>
@ -39,6 +37,7 @@ export function BetsSummary(props: {
) )
const position = yesWinnings - noWinnings const position = yesWinnings - noWinnings
const outcome = hasShares ? (position > 0 ? 'YES' : 'NO') : undefined
const prob = isBinary ? getProbability(contract) : 0 const prob = isBinary ? getProbability(contract) : 0
const expectation = prob * yesWinnings + (1 - prob) * noWinnings const expectation = prob * yesWinnings + (1 - prob) * noWinnings
@ -60,7 +59,9 @@ export function BetsSummary(props: {
<Col> <Col>
<div className="whitespace-nowrap text-sm text-gray-500"> <div className="whitespace-nowrap text-sm text-gray-500">
Position{' '} Position{' '}
<InfoTooltip text="Number of shares you own on net. 1 YES share = M$1 if the market resolves YES." /> <InfoTooltip
text={`Number of shares you own on net. 1 ${outcome} share = M$1 if the market resolves ${outcome}.`}
/>
</div> </div>
<div className="whitespace-nowrap"> <div className="whitespace-nowrap">
{position > 1e-7 ? ( {position > 1e-7 ? (

View File

@ -52,6 +52,8 @@ import {
} from 'web/hooks/use-persistent-state' } from 'web/hooks/use-persistent-state'
import { safeLocalStorage } from 'web/lib/util/local' import { safeLocalStorage } from 'web/lib/util/local'
import { ExclamationIcon } from '@heroicons/react/outline' import { ExclamationIcon } from '@heroicons/react/outline'
import { Select } from './select'
import { Table } from './table'
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all' type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all'
@ -200,8 +202,7 @@ export function BetsList(props: { user: User }) {
</Row> </Row>
<Row className="gap-2"> <Row className="gap-2">
<select <Select
className="border-greyscale-4 self-start overflow-hidden rounded border px-2 py-2 text-sm"
value={filter} value={filter}
onChange={(e) => setFilter(e.target.value as BetFilter)} onChange={(e) => setFilter(e.target.value as BetFilter)}
> >
@ -211,10 +212,9 @@ export function BetsList(props: { user: User }) {
<option value="closed">Closed</option> <option value="closed">Closed</option>
<option value="resolved">Resolved</option> <option value="resolved">Resolved</option>
<option value="all">All</option> <option value="all">All</option>
</select> </Select>
<select <Select
className="border-greyscale-4 self-start overflow-hidden rounded px-2 py-2 text-sm"
value={sort} value={sort}
onChange={(e) => setSort(e.target.value as BetSort)} onChange={(e) => setSort(e.target.value as BetSort)}
> >
@ -222,7 +222,7 @@ export function BetsList(props: { user: User }) {
<option value="value">Value</option> <option value="value">Value</option>
<option value="profit">Profit</option> <option value="profit">Profit</option>
<option value="closeTime">Close date</option> <option value="closeTime">Close date</option>
</select> </Select>
</Row> </Row>
</Col> </Col>
@ -451,7 +451,7 @@ export function ContractBetsTable(props: {
</> </>
)} )}
<table className="table-zebra table-compact table w-full text-gray-500"> <Table>
<thead> <thead>
<tr className="p-2"> <tr className="p-2">
<th></th> <th></th>
@ -480,7 +480,7 @@ export function ContractBetsTable(props: {
/> />
))} ))}
</tbody> </tbody>
</table> </Table>
</div> </div>
) )
} }
@ -551,7 +551,7 @@ function BetRow(props: {
return ( return (
<tr> <tr>
<td className="text-neutral"> <td className="text-gray-700">
{isYourBet && {isYourBet &&
!isCPMM && !isCPMM &&
!isResolved && !isResolved &&

View File

@ -82,3 +82,39 @@ export function Button(props: {
</button> </button>
) )
} }
export function IconButton(props: {
className?: string
onClick?: MouseEventHandler<any> | undefined
children?: ReactNode
size?: SizeType
type?: 'button' | 'reset' | 'submit'
disabled?: boolean
loading?: boolean
}) {
const {
children,
className,
onClick,
size = 'md',
type = 'button',
disabled = false,
loading,
} = props
return (
<button
type={type}
className={clsx(
'inline-flex items-center justify-center transition-colors disabled:cursor-not-allowed',
sizeClasses[size],
'disabled:text-greyscale-2 text-greyscale-5 hover:text-greyscale-6',
className
)}
disabled={disabled || loading}
onClick={onClick}
>
{children}
</button>
)
}

View File

@ -171,7 +171,7 @@ function CreateChallengeForm(props: {
<div>You'll bet:</div> <div>You'll bet:</div>
<Row <Row
className={ className={
'form-control w-full max-w-xs items-center justify-between gap-4 pr-3' 'w-full max-w-xs items-center justify-between gap-4 pr-3'
} }
> >
<AmountInput <AmountInput

View File

@ -8,10 +8,12 @@ import { useEffect, useState } from 'react'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { MAX_COMMENT_LENGTH } from 'web/lib/firebase/comments' import { MAX_COMMENT_LENGTH } from 'web/lib/firebase/comments'
import Curve from 'web/public/custom-components/curve' import Curve from 'web/public/custom-components/curve'
import { getAnswerColor } from './answers/answers-panel'
import { Avatar } from './avatar' import { Avatar } from './avatar'
import { TextEditor, useTextEditor } from './editor' import { TextEditor, useTextEditor } from './editor'
import { CommentsAnswer } from './feed/feed-answer-comment-group' import { CommentsAnswer } from './feed/feed-answer-comment-group'
import { ContractCommentInput } from './feed/feed-comments' import { ContractCommentInput } from './feed/feed-comments'
import { Col } from './layout/col'
import { Row } from './layout/row' import { Row } from './layout/row'
import { LoadingIndicator } from './loading-indicator' import { LoadingIndicator } from './loading-indicator'
@ -81,20 +83,30 @@ export function AnswerCommentInput(props: {
contract: Contract<AnyContractType> contract: Contract<AnyContractType>
answerResponse: Answer answerResponse: Answer
onCancelAnswerResponse?: () => void onCancelAnswerResponse?: () => void
answersArray: string[]
}) { }) {
const { contract, answerResponse, onCancelAnswerResponse } = props const { contract, answerResponse, onCancelAnswerResponse, answersArray } =
props
const replyTo = { const replyTo = {
id: answerResponse.id, id: answerResponse.id,
username: answerResponse.username, username: answerResponse.username,
} }
const color = getAnswerColor(answerResponse, answersArray)
return ( return (
<> <>
<CommentsAnswer answer={answerResponse} contract={contract} /> <Col>
<Row> <Row className="relative">
<div className="ml-1"> <div className="absolute -bottom-1 left-1.5">
<Curve size={28} strokeWidth={1} color="#D8D8EB" /> <Curve size={32} strokeWidth={1} color="#D8D8EB" />
</div> </div>
<div className="ml-[38px]">
<CommentsAnswer
answer={answerResponse}
contract={contract}
color={color}
/>
</div>
</Row>
<div className="relative w-full pt-1"> <div className="relative w-full pt-1">
<ContractCommentInput <ContractCommentInput
contract={contract} contract={contract}
@ -107,7 +119,7 @@ export function AnswerCommentInput(props: {
<XCircleIcon className="text-greyscale-5 hover:text-greyscale-6 absolute -top-1 -right-2 h-5 w-5" /> <XCircleIcon className="text-greyscale-5 hover:text-greyscale-6 absolute -top-1 -right-2 h-5 w-5" />
</button> </button>
</div> </div>
</Row> </Col>
</> </>
) )
} }

View File

@ -42,6 +42,7 @@ import { Button } from './button'
import { Modal } from './layout/modal' import { Modal } from './layout/modal'
import { Title } from './title' import { Title } from './title'
import { Input } from './input' import { Input } from './input'
import { Select } from './select'
export const SORTS = [ export const SORTS = [
{ label: 'Newest', value: 'newest' }, { label: 'Newest', value: 'newest' },
@ -437,7 +438,7 @@ function ContractSearchControls(props: {
} }
return ( return (
<Col className={clsx('bg-base-200 top-0 z-20 gap-3 pb-3', className)}> <Col className={clsx('bg-greyscale-1 top-0 z-20 gap-3 pb-3', className)}>
<Row className="gap-1 sm:gap-2"> <Row className="gap-1 sm:gap-2">
<Input <Input
type="text" type="text"
@ -543,8 +544,7 @@ export function SearchFilters(props: {
return ( return (
<div className={className}> <div className={className}>
<select <Select
className="select select-bordered"
value={filter} value={filter}
onChange={(e) => selectFilter(e.target.value as filter)} onChange={(e) => selectFilter(e.target.value as filter)}
> >
@ -552,10 +552,9 @@ export function SearchFilters(props: {
<option value="closed">Closed</option> <option value="closed">Closed</option>
<option value="resolved">Resolved</option> <option value="resolved">Resolved</option>
<option value="all">All</option> <option value="all">All</option>
</select> </Select>
{!hideOrderSelector && ( {!hideOrderSelector && (
<select <Select
className="select select-bordered"
value={sort} value={sort}
onChange={(e) => selectSort(e.target.value as Sort)} onChange={(e) => selectSort(e.target.value as Sort)}
> >
@ -564,7 +563,7 @@ export function SearchFilters(props: {
{option.label} {option.label}
</option> </option>
))} ))}
</select> </Select>
)} )}
</div> </div>
) )

View File

@ -1,7 +1,11 @@
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link' import Link from 'next/link'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { formatLargeNumber, formatPercent } from 'common/util/format' import {
formatLargeNumber,
formatPercent,
formatWithCommas,
} from 'common/util/format'
import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts' import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { import {
@ -35,10 +39,10 @@ import { trackCallback } from 'web/lib/service/analytics'
import { getMappedValue } from 'common/pseudo-numeric' import { getMappedValue } from 'common/pseudo-numeric'
import { Tooltip } from '../tooltip' import { Tooltip } from '../tooltip'
import { SiteLink } from '../site-link' import { SiteLink } from '../site-link'
import { ProbChange } from './prob-change-table' import { ProbOrNumericChange } from './prob-change-table'
import { Card } from '../card' import { Card } from '../card'
import { ProfitBadgeMana } from '../profit-badge'
import { floatingEqual } from 'common/util/math' import { floatingEqual } from 'common/util/math'
import { ENV_CONFIG } from 'common/envs/constants'
export function ContractCard(props: { export function ContractCard(props: {
contract: Contract contract: Contract
@ -396,14 +400,25 @@ export function ContractCardProbChange(props: {
className?: string className?: string
}) { }) {
const { noLinkAvatar, showPosition, className } = props const { noLinkAvatar, showPosition, className } = props
const yesOutcomeLabel =
props.contract.outcomeType === 'PSEUDO_NUMERIC' ? 'HIGHER' : 'YES'
const noOutcomeLabel =
props.contract.outcomeType === 'PSEUDO_NUMERIC' ? 'LOWER' : 'NO'
const contract = useContractWithPreload(props.contract) as CPMMBinaryContract const contract = useContractWithPreload(props.contract) as CPMMBinaryContract
const user = useUser() const user = useUser()
const metrics = useUserContractMetrics(user?.id, contract.id) const metrics = useUserContractMetrics(user?.id, contract.id)
const dayMetrics = metrics && metrics.from && metrics.from.day const dayMetrics = metrics && metrics.from && metrics.from.day
const outcome = const binaryOutcome =
metrics && floatingEqual(metrics.totalShares.NO ?? 0, 0) ? 'YES' : 'NO' metrics && floatingEqual(metrics.totalShares.NO ?? 0, 0) ? 'YES' : 'NO'
const displayedProfit = dayMetrics
? ENV_CONFIG.moneyMoniker +
(dayMetrics.profit > 0 ? '+' : '') +
dayMetrics.profit.toFixed(0)
: undefined
return ( return (
<Card className={clsx(className, 'mb-4')}> <Card className={clsx(className, 'mb-4')}>
<AvatarDetails <AvatarDetails
@ -418,7 +433,7 @@ export function ContractCardProbChange(props: {
> >
<span className="line-clamp-3">{contract.question}</span> <span className="line-clamp-3">{contract.question}</span>
</SiteLink> </SiteLink>
<ProbChange className="py-2 pr-4" contract={contract} /> <ProbOrNumericChange className="py-2 pr-4" contract={contract} />
</Row> </Row>
{showPosition && metrics && metrics.hasShares && ( {showPosition && metrics && metrics.hasShares && (
<Row <Row
@ -426,19 +441,11 @@ export function ContractCardProbChange(props: {
'items-center justify-between gap-4 pl-6 pr-4 pb-2 text-sm' 'items-center justify-between gap-4 pl-6 pr-4 pb-2 text-sm'
)} )}
> >
<Row className="gap-1 text-gray-700"> <Row className="gap-1 text-gray-400">
<div className="text-gray-500">Position</div> You: {formatWithCommas(metrics.totalShares[binaryOutcome])}{' '}
{Math.floor(metrics.totalShares[outcome])} {outcome} {binaryOutcome === 'YES' ? yesOutcomeLabel : noOutcomeLabel} shares
<span className="ml-1.5">{displayedProfit} today</span>
</Row> </Row>
{dayMetrics && (
<>
<Row className="items-center">
<div className="mr-1 text-gray-500">Daily profit</div>
<ProfitBadgeMana amount={dayMetrics.profit} gray />
</Row>
</>
)}
</Row> </Row>
)} )}
</Card> </Card>

View File

@ -45,13 +45,11 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) {
const { contract, isAdmin } = props const { contract, isAdmin } = props
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
const [editingQ, setEditingQ] = useState(false) const [editingQ, setEditingQ] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
// key: `description ${contract.id}`, // key: `description ${contract.id}`,
max: MAX_DESCRIPTION_LENGTH, max: MAX_DESCRIPTION_LENGTH,
defaultValue: contract.description, defaultValue: contract.description,
disabled: isSubmitting,
}) })
async function saveDescription() { async function saveDescription() {
@ -66,10 +64,8 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) {
<Row className="gap-2"> <Row className="gap-2">
<Button <Button
onClick={async () => { onClick={async () => {
setIsSubmitting(true)
await saveDescription() await saveDescription()
setEditing(false) setEditing(false)
setIsSubmitting(false)
}} }}
> >
Save Save

View File

@ -3,6 +3,7 @@ import {
ExclamationIcon, ExclamationIcon,
PencilIcon, PencilIcon,
PlusCircleIcon, PlusCircleIcon,
UserGroupIcon,
} from '@heroicons/react/solid' } from '@heroicons/react/solid'
import clsx from 'clsx' import clsx from 'clsx'
import { Editor } from '@tiptap/react' import { Editor } from '@tiptap/react'
@ -50,8 +51,13 @@ export function MiscDetails(props: {
hideGroupLink?: boolean hideGroupLink?: boolean
}) { }) {
const { contract, showTime, hideGroupLink } = props const { contract, showTime, hideGroupLink } = props
const { volume, closeTime, isResolved, createdTime, resolutionTime } = const {
contract closeTime,
isResolved,
createdTime,
resolutionTime,
uniqueBettorCount,
} = contract
const isClient = useIsClient() const isClient = useIsClient()
const isNew = createdTime > Date.now() - DAY_MS && !isResolved const isNew = createdTime > Date.now() - DAY_MS && !isResolved
@ -75,8 +81,11 @@ export function MiscDetails(props: {
<FeaturedContractBadge /> <FeaturedContractBadge />
) : (contract.openCommentBounties ?? 0) > 0 ? ( ) : (contract.openCommentBounties ?? 0) > 0 ? (
<BountiedContractBadge /> <BountiedContractBadge />
) : volume > 0 || !isNew ? ( ) : !isNew || (uniqueBettorCount ?? 0) > 1 ? (
<Row className={'shrink-0'}>{formatMoney(volume)} bet</Row> <Row className={'shrink-0'}>
<UserGroupIcon className="mr-1 h-4 w-4" />
{uniqueBettorCount} trader{uniqueBettorCount !== 1 ? 's' : ''}
</Row>
) : ( ) : (
<NewContractBadge /> <NewContractBadge />
)} )}

View File

@ -19,11 +19,10 @@ import ShortToggle from '../widgets/short-toggle'
import { DuplicateContractButton } from '../duplicate-contract-button' import { DuplicateContractButton } from '../duplicate-contract-button'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { BETTORS, User } from 'common/user' import { BETTORS, User } from 'common/user'
import { Button } from '../button' import { IconButton } from '../button'
import { AddLiquidityButton } from './add-liquidity-button' import { AddLiquidityButton } from './add-liquidity-button'
import { Tooltip } from '../tooltip'
export const contractDetailsButtonClassName = import { Table } from '../table'
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
export function ContractInfoDialog(props: { export function ContractInfoDialog(props: {
contract: Contract contract: Contract
@ -84,23 +83,23 @@ export function ContractInfoDialog(props: {
return ( return (
<> <>
<Button <Tooltip text="Market details" placement="bottom" noTap noFade>
size="sm" <IconButton
color="gray-white" size="2xs"
className={clsx(contractDetailsButtonClassName, className)} className={clsx(className)}
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
> >
<DotsHorizontalIcon <DotsHorizontalIcon
className={clsx('h-5 w-5 flex-shrink-0')} className={clsx('h-5 w-5 flex-shrink-0')}
aria-hidden="true" aria-hidden="true"
/> />
</Button> </IconButton>
<Modal open={open} setOpen={setOpen}> <Modal open={open} setOpen={setOpen}>
<Col className="gap-4 rounded bg-white p-6"> <Col className="gap-4 rounded bg-white p-6">
<Title className="!mt-0 !mb-0" text="This Market" /> <Title className="!mt-0 !mb-0" text="This Market" />
<table className="table-compact table-zebra table w-full text-gray-500"> <Table>
<tbody> <tbody>
<tr> <tr>
<td>Type</td> <td>Type</td>
@ -186,7 +185,8 @@ export function ContractInfoDialog(props: {
<td> <td>
{mechanism === 'cpmm-1' && outcomeType === 'BINARY' {mechanism === 'cpmm-1' && outcomeType === 'BINARY'
? `${Math.round(pool.YES)} YES, ${Math.round(pool.NO)} NO` ? `${Math.round(pool.YES)} YES, ${Math.round(pool.NO)} NO`
: mechanism === 'cpmm-1' && outcomeType === 'PSEUDO_NUMERIC' : mechanism === 'cpmm-1' &&
outcomeType === 'PSEUDO_NUMERIC'
? `${Math.round(pool.YES)} HIGHER, ${Math.round( ? `${Math.round(pool.YES)} HIGHER, ${Math.round(
pool.NO pool.NO
)} LOWER` )} LOWER`
@ -239,7 +239,7 @@ export function ContractInfoDialog(props: {
</tr> </tr>
)} )}
</tbody> </tbody>
</table> </Table>
<Row className="flex-wrap"> <Row className="flex-wrap">
{mechanism === 'cpmm-1' && ( {mechanism === 'cpmm-1' && (
@ -249,6 +249,7 @@ export function ContractInfoDialog(props: {
</Row> </Row>
</Col> </Col>
</Modal> </Modal>
</Tooltip>
</> </>
) )
} }

View File

@ -32,7 +32,7 @@ export function ContractReportResolution(props: { contract: Contract }) {
} }
const flagClass = clsx( const flagClass = clsx(
'mx-2 flex flex-col items-center gap-1 w-6 h-6 rounded-md !bg-gray-100 px-2 py-1 hover:bg-gray-300', 'mx-2 flex flex-col items-center gap-1 w-6 h-6 rounded-md !bg-gray-100 px-1 py-2 hover:bg-gray-300',
userReported ? '!text-red-500' : '!text-gray-500' userReported ? '!text-red-500' : '!text-gray-500'
) )

View File

@ -2,11 +2,11 @@ import { memo, useState } from 'react'
import { Pagination } from 'web/components/pagination' import { Pagination } from 'web/components/pagination'
import { FeedBet } from '../feed/feed-bets' import { FeedBet } from '../feed/feed-bets'
import { FeedLiquidity } from '../feed/feed-liquidity' import { FeedLiquidity } from '../feed/feed-liquidity'
import { CommentsAnswer } from '../feed/feed-answer-comment-group' import { FreeResponseComments } from '../feed/feed-answer-comment-group'
import { FeedCommentThread, ContractCommentInput } from '../feed/feed-comments' import { FeedCommentThread, ContractCommentInput } from '../feed/feed-comments'
import { groupBy, sortBy, sum } from 'lodash' import { groupBy, sortBy, sum } from 'lodash'
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
import { Contract } from 'common/contract' import { AnyContractType, Contract } from 'common/contract'
import { PAST_BETS } from 'common/user' import { PAST_BETS } from 'common/user'
import { ContractBetsTable } from '../bets-list' import { ContractBetsTable } from '../bets-list'
import { Spacer } from '../layout/spacer' import { Spacer } from '../layout/spacer'
@ -35,9 +35,7 @@ import {
} from 'web/hooks/use-persistent-state' } from 'web/hooks/use-persistent-state'
import { safeLocalStorage } from 'web/lib/util/local' import { safeLocalStorage } from 'web/lib/util/local'
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon' import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
import Curve from 'web/public/custom-components/curve'
import { Answer } from 'common/answer' import { Answer } from 'common/answer'
import { AnswerCommentInput } from '../comment-input'
export function ContractTabs(props: { export function ContractTabs(props: {
contract: Contract contract: Contract
@ -139,95 +137,27 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
) )
const topLevelComments = commentsByParent['_'] ?? [] const topLevelComments = commentsByParent['_'] ?? []
const sortRow = comments.length > 0 && (
<Row className="mb-4 items-center justify-end gap-4">
<BountiedContractSmallBadge contract={contract} showAmount />
<Row className="items-center gap-1">
<div className="text-greyscale-4 text-sm">Sort by:</div>
<button
className="text-greyscale-6 w-20 text-sm"
onClick={() => setSort(sort === 'Newest' ? 'Best' : 'Newest')}
>
<Tooltip
text={sort === 'Best' ? 'Highest tips + bounties first.' : ''}
>
<Row className="items-center gap-1">
{sort}
<TriangleDownFillIcon className=" h-2 w-2" />
</Row>
</Tooltip>
</button>
</Row>
</Row>
)
if (contract.outcomeType === 'FREE_RESPONSE') {
return ( return (
<> <>
<ContractCommentInput className="mb-5" contract={contract} /> <ContractCommentInput className="mb-5" contract={contract} />
{sortRow} <SortRow
{answerResponse && ( comments={comments}
<AnswerCommentInput contract={contract}
sort={sort}
onSortClick={() => setSort(sort === 'Newest' ? 'Best' : 'Newest')}
/>
{contract.outcomeType === 'FREE_RESPONSE' && (
<FreeResponseComments
contract={contract} contract={contract}
answerResponse={answerResponse} answerResponse={answerResponse}
onCancelAnswerResponse={onCancelAnswerResponse} onCancelAnswerResponse={onCancelAnswerResponse}
/> topLevelComments={topLevelComments}
)} commentsByParent={commentsByParent}
{topLevelComments.map((parent) => {
if (parent.answerOutcome === undefined) {
return (
<FeedCommentThread
key={parent.id}
contract={contract}
parentComment={parent}
threadComments={sortBy(
commentsByParent[parent.id] ?? [],
(c) => c.createdTime
)}
tips={tips} tips={tips}
/> />
)
}
const answer = contract.answers.find(
(answer) => answer.id === parent.answerOutcome
)
if (answer === undefined) {
console.error('Could not find answer that matches ID')
return <></>
}
return (
<>
<Row className="gap-2">
<CommentsAnswer answer={answer} contract={contract} />
</Row>
<Row>
<div className="ml-1">
<Curve size={28} strokeWidth={1} color="#D8D8EB" />
</div>
<div className="w-full pt-1">
<FeedCommentThread
key={parent.id}
contract={contract}
parentComment={parent}
threadComments={sortBy(
commentsByParent[parent.id] ?? [],
(c) => c.createdTime
)} )}
tips={tips} {contract.outcomeType !== 'FREE_RESPONSE' &&
/> topLevelComments.map((parent) => (
</div>
</Row>
</>
)
})}
</>
)
} else {
return (
<>
<ContractCommentInput className="mb-5" contract={contract} />
{sortRow}
{topLevelComments.map((parent) => (
<FeedCommentThread <FeedCommentThread
key={parent.id} key={parent.id}
contract={contract} contract={contract}
@ -241,7 +171,6 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
))} ))}
</> </>
) )
}
}) })
const BetsTabContent = memo(function BetsTabContent(props: { const BetsTabContent = memo(function BetsTabContent(props: {
@ -310,3 +239,33 @@ const BetsTabContent = memo(function BetsTabContent(props: {
</> </>
) )
}) })
export function SortRow(props: {
comments: ContractComment[]
contract: Contract<AnyContractType>
sort: 'Best' | 'Newest'
onSortClick: () => void
}) {
const { comments, contract, sort, onSortClick } = props
if (comments.length <= 0) {
return <></>
}
return (
<Row className="mb-4 items-center justify-end gap-4">
<BountiedContractSmallBadge contract={contract} showAmount />
<Row className="items-center gap-1">
<div className="text-greyscale-4 text-sm">Sort by:</div>
<button className="text-greyscale-6 w-20 text-sm" onClick={onSortClick}>
<Tooltip
text={sort === 'Best' ? 'Highest tips + bounties first.' : ''}
>
<Row className="items-center gap-1">
{sort}
<TriangleDownFillIcon className=" h-2 w-2" />
</Row>
</Tooltip>
</button>
</Row>
</Row>
)
}

View File

@ -2,11 +2,11 @@ import { ShareIcon } from '@heroicons/react/outline'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { Contract } from 'web/lib/firebase/contracts' import { Contract } from 'web/lib/firebase/contracts'
import React, { useState } from 'react' import React, { useState } from 'react'
import { Button } from 'web/components/button' import { IconButton } from 'web/components/button'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { ShareModal } from './share-modal' import { ShareModal } from './share-modal'
import { FollowMarketButton } from 'web/components/follow-market-button' import { FollowMarketButton } from 'web/components/follow-market-button'
import { LikeMarketButton } from 'web/components/contract/like-market-button' import { LikeItemButton } from 'web/components/contract/like-item-button'
import { ContractInfoDialog } from 'web/components/contract/contract-info-dialog' import { ContractInfoDialog } from 'web/components/contract/contract-info-dialog'
import { Tooltip } from '../tooltip' import { Tooltip } from '../tooltip'
@ -16,15 +16,14 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
const [isShareOpen, setShareOpen] = useState(false) const [isShareOpen, setShareOpen] = useState(false)
return ( return (
<Row> <Row className="gap-1">
<FollowMarketButton contract={contract} user={user} /> <FollowMarketButton contract={contract} user={user} />
<LikeMarketButton contract={contract} user={user} /> <LikeItemButton item={contract} user={user} itemType={'contract'} />
<Tooltip text="Share" placement="bottom" noTap noFade> <Tooltip text="Share" placement="bottom" noTap noFade>
<Button <IconButton
size="sm" size="2xs"
color="gray-white"
className={'flex'} className={'flex'}
onClick={() => setShareOpen(true)} onClick={() => setShareOpen(true)}
> >
@ -35,7 +34,7 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
contract={contract} contract={contract}
user={user} user={user}
/> />
</Button> </IconButton>
</Tooltip> </Tooltip>
<ContractInfoDialog contract={contract} user={user} /> <ContractInfoDialog contract={contract} user={user} />

View File

@ -0,0 +1,71 @@
import React, { useMemo, useState } from 'react'
import { User } from 'common/user'
import { useUserLikes } from 'web/hooks/use-likes'
import toast from 'react-hot-toast'
import { likeItem } from 'web/lib/firebase/likes'
import { LIKE_TIP_AMOUNT, TIP_UNDO_DURATION } from 'common/like'
import { firebaseLogin } from 'web/lib/firebase/users'
import { useItemTipTxns } from 'web/hooks/use-tip-txns'
import { sum } from 'lodash'
import { TipButton } from './tip-button'
import { Contract } from 'common/contract'
import { Post } from 'common/post'
import { TipToast } from '../tipper'
export function LikeItemButton(props: {
item: Contract | Post
user: User | null | undefined
itemType: string
}) {
const { item, user, itemType } = props
const tips = useItemTipTxns(item.id)
const totalTipped = useMemo(() => {
return sum(tips.map((tip) => tip.amount))
}, [tips])
const likes = useUserLikes(user?.id)
const [isLiking, setIsLiking] = useState(false)
const userLikedItemIds = likes
?.filter((l) => l.type === 'contract' || l.type === 'post')
.map((l) => l.id)
const onLike = async () => {
if (!user) return firebaseLogin()
setIsLiking(true)
const timeoutId = setTimeout(() => {
likeItem(user, item, itemType).catch(() => setIsLiking(false))
}, 3000)
toast.custom(
() => (
<TipToast
userName={item.creatorUsername}
onUndoClick={() => {
clearTimeout(timeoutId)
}}
/>
),
{ duration: TIP_UNDO_DURATION }
)
}
return (
<TipButton
onClick={onLike}
tipAmount={LIKE_TIP_AMOUNT}
totalTipped={totalTipped}
userTipped={
!!user &&
(isLiking ||
userLikedItemIds?.includes(item.id) ||
(!likes && !!item.likedByUserIds?.includes(user.id)))
}
disabled={item.creatorId === user?.id}
/>
)
}

View File

@ -1,56 +0,0 @@
import React, { useMemo, useState } from 'react'
import { Contract } from 'common/contract'
import { User } from 'common/user'
import { useUserLikes } from 'web/hooks/use-likes'
import toast from 'react-hot-toast'
import { formatMoney } from 'common/util/format'
import { likeContract } from 'web/lib/firebase/likes'
import { LIKE_TIP_AMOUNT } from 'common/like'
import { firebaseLogin } from 'web/lib/firebase/users'
import { useMarketTipTxns } from 'web/hooks/use-tip-txns'
import { sum } from 'lodash'
import { TipButton } from './tip-button'
export function LikeMarketButton(props: {
contract: Contract
user: User | null | undefined
}) {
const { contract, user } = props
const tips = useMarketTipTxns(contract.id)
const totalTipped = useMemo(() => {
return sum(tips.map((tip) => tip.amount))
}, [tips])
const likes = useUserLikes(user?.id)
const [isLiking, setIsLiking] = useState(false)
const userLikedContractIds = likes
?.filter((l) => l.type === 'contract')
.map((l) => l.id)
const onLike = async () => {
if (!user) return firebaseLogin()
setIsLiking(true)
likeContract(user, contract).catch(() => setIsLiking(false))
toast(`You tipped ${contract.creatorName} ${formatMoney(LIKE_TIP_AMOUNT)}!`)
}
return (
<TipButton
onClick={onLike}
tipAmount={LIKE_TIP_AMOUNT}
totalTipped={totalTipped}
userTipped={
!!user &&
(isLiking ||
userLikedContractIds?.includes(contract.id) ||
(!likes && !!contract.likedByUserIds?.includes(user.id)))
}
disabled={contract.creatorId === user?.id}
/>
)
}

View File

@ -7,12 +7,14 @@ import { formatPercent } from 'common/util/format'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { LoadingIndicator } from '../loading-indicator' import { LoadingIndicator } from '../loading-indicator'
import { ContractCardProbChange } from './contract-card' import { ContractCardProbChange } from './contract-card'
import { formatNumericProbability } from 'common/pseudo-numeric'
export function ProfitChangeTable(props: { export function ProfitChangeTable(props: {
contracts: CPMMBinaryContract[] contracts: CPMMBinaryContract[]
metrics: ContractMetrics[] metrics: ContractMetrics[]
maxRows?: number
}) { }) {
const { contracts, metrics } = props const { contracts, metrics, maxRows } = props
const contractProfit = metrics.map( const contractProfit = metrics.map(
(m) => [m.contractId, m.from?.day.profit ?? 0] as const (m) => [m.contractId, m.from?.day.profit ?? 0] as const
@ -26,7 +28,7 @@ export function ProfitChangeTable(props: {
positiveProfit.map(([contractId]) => positiveProfit.map(([contractId]) =>
contracts.find((c) => c.id === contractId) contracts.find((c) => c.id === contractId)
) )
) ).slice(0, maxRows)
const negativeProfit = sortBy( const negativeProfit = sortBy(
contractProfit.filter(([, profit]) => profit < 0), contractProfit.filter(([, profit]) => profit < 0),
@ -36,7 +38,7 @@ export function ProfitChangeTable(props: {
negativeProfit.map(([contractId]) => negativeProfit.map(([contractId]) =>
contracts.find((c) => c.id === contractId) contracts.find((c) => c.id === contractId)
) )
) ).slice(0, maxRows)
if (positive.length === 0 && negative.length === 0) if (positive.length === 0 && negative.length === 0)
return <div className="px-4 text-gray-500">None</div> return <div className="px-4 text-gray-500">None</div>
@ -118,7 +120,7 @@ export function ProbChangeTable(props: {
) )
} }
export function ProbChange(props: { export function ProbOrNumericChange(props: {
contract: CPMMContract contract: CPMMContract
className?: string className?: string
}) { }) {
@ -127,13 +129,17 @@ export function ProbChange(props: {
prob, prob,
probChanges: { day: change }, probChanges: { day: change },
} = contract } = contract
const number =
contract.outcomeType === 'PSEUDO_NUMERIC'
? formatNumericProbability(prob, contract)
: null
const color = change >= 0 ? 'text-teal-500' : 'text-red-400' const color = change >= 0 ? 'text-teal-500' : 'text-red-400'
return ( return (
<Col className={clsx('flex flex-col items-end', className)}> <Col className={clsx('flex flex-col items-end', className)}>
<div className="mb-0.5 mr-0.5 text-2xl"> <div className="mb-0.5 mr-0.5 text-2xl">
{formatPercent(Math.round(100 * prob) / 100)} {number ? number : formatPercent(Math.round(100 * prob) / 100)}
</div> </div>
<div className={clsx('text-base', color)}> <div className={clsx('text-base', color)}>
{(change > 0 ? '+' : '') + (change * 100).toFixed(0) + '%'} {(change > 0 ? '+' : '') + (change * 100).toFixed(0) + '%'}

View File

@ -1,10 +1,10 @@
import clsx from 'clsx' import clsx from 'clsx'
import { HeartIcon } from '@heroicons/react/outline'
import { Button } from 'web/components/button'
import { formatMoney, shortFormatNumber } from 'common/util/format' import { formatMoney, shortFormatNumber } from 'common/util/format'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { Tooltip } from '../tooltip' import { Tooltip } from '../tooltip'
import TipJar from 'web/public/custom-components/tipJar'
import { useState } from 'react'
import Coin from 'web/public/custom-components/coin'
export function TipButton(props: { export function TipButton(props: {
tipAmount: number tipAmount: number
@ -14,55 +14,84 @@ export function TipButton(props: {
isCompact?: boolean isCompact?: boolean
disabled?: boolean disabled?: boolean
}) { }) {
const { tipAmount, totalTipped, userTipped, isCompact, onClick, disabled } = const { tipAmount, totalTipped, userTipped, onClick, disabled } = props
props
const tipDisplay = shortFormatNumber(Math.ceil(totalTipped / 10)) const tipDisplay = shortFormatNumber(Math.ceil(totalTipped / 10))
const [hover, setHover] = useState(false)
return ( return (
<Tooltip <Tooltip
text={ text={
disabled disabled
? `Tips (${formatMoney(totalTipped)})` ? `Total tips ${formatMoney(totalTipped)}`
: `Tip ${formatMoney(tipAmount)}` : `Tip ${formatMoney(tipAmount)}`
} }
placement="bottom" placement="bottom"
noTap noTap
noFade noFade
> >
<Button <button
size={'sm'}
className={clsx(
'max-w-xs self-center',
isCompact && 'px-0 py-0',
disabled && 'hover:bg-inherit'
)}
color={'gray-white'}
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
>
<Col className={'relative items-center sm:flex-row'}>
<HeartIcon
className={clsx( className={clsx(
'h-5 w-5', 'px-2 py-1 text-xs', //2xs button
totalTipped > 0 ? 'mr-2' : '', 'text-greyscale-5 transition-transform disabled:cursor-not-allowed',
userTipped ? 'fill-teal-500 text-teal-500' : '' !disabled ? 'hover:text-greyscale-6' : ''
)} )}
/> onMouseOver={() => {
{totalTipped > 0 && ( if (!disabled) {
setHover(true)
}
}}
onMouseLeave={() => setHover(false)}
>
<Col className={clsx('relative')}>
<div <div
className={clsx( className={clsx(
'bg-greyscale-5 absolute ml-3.5 mt-2 h-4 w-4 rounded-full align-middle text-white sm:mt-3 sm:h-5 sm:w-5 sm:px-1', 'absolute transition-all',
tipDisplay.length > 2 hover ? 'left-[6px] -top-[9px]' : 'left-[8px] -top-[10px]'
? 'text-[0.4rem] sm:text-[0.5rem]'
: 'sm:text-2xs text-[0.5rem]'
)} )}
> >
{tipDisplay} <Coin
size={10}
color={
hover && !userTipped
? '#66667C'
: userTipped
? '#4f46e5'
: '#9191a7'
}
strokeWidth={2}
/>
</div> </div>
<TipJar
size={18}
color={
hover && !disabled && !userTipped
? '#66667C'
: userTipped
? '#4f46e5'
: '#9191a7'
}
/>
<div
className={clsx(
userTipped && 'text-indigo-600',
' absolute top-[2px] text-[0.5rem]',
tipDisplay.length === 1
? 'left-[7px]'
: tipDisplay.length === 2
? 'left-[4.5px]'
: tipDisplay.length > 2
? 'left-[4px] top-[2.5px] text-[0.35rem]'
: ''
)} )}
>
{totalTipped > 0 ? tipDisplay : ''}
</div>
</Col> </Col>
</Button> </button>
</Tooltip> </Tooltip>
) )
} }

View File

@ -22,7 +22,6 @@ export function CreatePost(props: { group?: Group }) {
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
key: `post ${group?.id || ''}`, key: `post ${group?.id || ''}`,
disabled: isSubmitting,
}) })
const isValid = const isValid =
@ -56,8 +55,8 @@ export function CreatePost(props: { group?: Group }) {
<div className="rounded-lg px-6 py-4 sm:py-0"> <div className="rounded-lg px-6 py-4 sm:py-0">
<Title className="!mt-0" text="Create a post" /> <Title className="!mt-0" text="Create a post" />
<form> <form>
<div className="form-control w-full"> <div className="flex w-full flex-col">
<label className="label"> <label className="px-1 py-2">
<span className="mb-1"> <span className="mb-1">
Title<span className={'text-red-700'}> *</span> Title<span className={'text-red-700'}> *</span>
</span> </span>
@ -70,7 +69,7 @@ export function CreatePost(props: { group?: Group }) {
onChange={(e) => setTitle(e.target.value || '')} onChange={(e) => setTitle(e.target.value || '')}
/> />
<Spacer h={6} /> <Spacer h={6} />
<label className="label"> <label className="px-1 py-2">
<span className="mb-1"> <span className="mb-1">
Subtitle<span className={'text-red-700'}> *</span> Subtitle<span className={'text-red-700'}> *</span>
</span> </span>
@ -83,7 +82,7 @@ export function CreatePost(props: { group?: Group }) {
onChange={(e) => setSubtitle(e.target.value || '')} onChange={(e) => setSubtitle(e.target.value || '')}
/> />
<Spacer h={6} /> <Spacer h={6} />
<label className="label"> <label className="px-1 py-2">
<span className="mb-1"> <span className="mb-1">
Content<span className={'text-red-700'}> *</span> Content<span className={'text-red-700'}> *</span>
</span> </span>

View File

@ -22,6 +22,7 @@ import { linkClass } from './site-link'
import { DisplayMention } from './editor/mention' import { DisplayMention } from './editor/mention'
import { DisplayContractMention } from './editor/contract-mention' import { DisplayContractMention } from './editor/contract-mention'
import GridComponent from './editor/tiptap-grid-cards' import GridComponent from './editor/tiptap-grid-cards'
import StaticReactEmbedComponent from './editor/tiptap-static-react-embed'
import Iframe from 'common/util/tiptap-iframe' import Iframe from 'common/util/tiptap-iframe'
import TiptapTweet from './editor/tiptap-tweet' import TiptapTweet from './editor/tiptap-tweet'
@ -52,7 +53,7 @@ import { debounce } from 'lodash'
const DisplayImage = Image.configure({ const DisplayImage = Image.configure({
HTMLAttributes: { HTMLAttributes: {
class: 'max-h-60', class: 'max-h-60 hover:max-h-[120rem] transition-all',
}, },
}) })
@ -81,6 +82,7 @@ export const editorExtensions = (simple = false): Extensions => [
DisplayMention, DisplayMention,
DisplayContractMention, DisplayContractMention,
GridComponent, GridComponent,
StaticReactEmbedComponent,
Iframe, Iframe,
TiptapTweet, TiptapTweet,
TiptapSpoiler.configure({ TiptapSpoiler.configure({
@ -90,18 +92,17 @@ export const editorExtensions = (simple = false): Extensions => [
const proseClass = clsx( const proseClass = clsx(
'prose prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', 'prose prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed',
'font-light prose-a:font-light prose-blockquote:font-light' 'font-light prose-a:font-light prose-blockquote:font-light prose-sm'
) )
export function useTextEditor(props: { export function useTextEditor(props: {
placeholder?: string placeholder?: string
max?: number max?: number
defaultValue?: Content defaultValue?: Content
disabled?: boolean
simple?: boolean simple?: boolean
key?: string // unique key for autosave. If set, plz call `clearContent(true)` on submit to clear autosave key?: string // unique key for autosave. If set, plz call `clearContent(true)` on submit to clear autosave
}) { }) {
const { placeholder, max, defaultValue, disabled, simple, key } = props const { placeholder, max, defaultValue, simple, key } = props
const [content, saveContent] = usePersistentState<JSONContent | undefined>( const [content, saveContent] = usePersistentState<JSONContent | undefined>(
undefined, undefined,
@ -169,10 +170,6 @@ export function useTextEditor(props: {
}, },
}) })
useEffect(() => {
editor?.setEditable(!disabled)
}, [editor, disabled])
return { editor, upload } return { editor, upload }
} }
@ -263,7 +260,8 @@ export function TextEditor(props: {
<> <>
{/* hide placeholder when focused */} {/* hide placeholder when focused */}
<div className="relative w-full [&:focus-within_p.is-empty]:before:content-none"> <div className="relative w-full [&:focus-within_p.is-empty]:before:content-none">
<div className="rounded-lg border border-gray-300 bg-white shadow-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"> {/* matches input styling */}
<div className="rounded-lg border border-gray-300 bg-white shadow-sm transition-colors focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500">
<FloatingMenu editor={editor} /> <FloatingMenu editor={editor} />
<EditorContent editor={editor} /> <EditorContent editor={editor} />
{/* Toolbar, with buttons for images and embeds */} {/* Toolbar, with buttons for images and embeds */}
@ -367,6 +365,7 @@ export function RichContent(props: {
DisplayMention, DisplayMention,
DisplayContractMention, DisplayContractMention,
GridComponent, GridComponent,
StaticReactEmbedComponent,
Iframe, Iframe,
TiptapTweet, TiptapTweet,
TiptapSpoiler.configure({ TiptapSpoiler.configure({

View File

@ -14,6 +14,7 @@ const beginsWith = (text: string, query: string) =>
export const contractMentionSuggestion: Suggestion = { export const contractMentionSuggestion: Suggestion = {
char: '%', char: '%',
allowSpaces: true, allowSpaces: true,
allowedPrefixes: [' '],
pluginKey: new PluginKey('contract-mention'), pluginKey: new PluginKey('contract-mention'),
items: async ({ query }) => items: async ({ query }) =>
orderBy( orderBy(

View File

@ -120,7 +120,10 @@ function DreamTab(props: {
<div className="text-sm">This may take ~10 seconds...</div> <div className="text-sm">This may take ~10 seconds...</div>
)} )}
{/* TODO: Allow the user to choose their own modifiers */} {/* TODO: Allow the user to choose their own modifiers */}
<div className="pt-2 text-sm text-gray-400">Modifiers: {MODIFIERS}</div> <div className="pt-2 text-sm text-gray-400">
Commission a custom image using AI.
</div>
<div className="pt-2 text-xs text-gray-400">Modifiers: {MODIFIERS}</div>
{/* Show the current imageUrl */} {/* Show the current imageUrl */}
{/* TODO: Keep the other generated images, so the user can play with different attempts. */} {/* TODO: Keep the other generated images, so the user can play with different attempts. */}

View File

@ -14,6 +14,7 @@ const beginsWith = (text: string, query: string) =>
// copied from https://tiptap.dev/api/nodes/mention#usage // copied from https://tiptap.dev/api/nodes/mention#usage
export const mentionSuggestion: Suggestion = { export const mentionSuggestion: Suggestion = {
allowedPrefixes: [' '],
items: async ({ query }) => items: async ({ query }) =>
orderBy( orderBy(
(await getCachedUsers()).filter((u) => (await getCachedUsers()).filter((u) =>

View File

@ -0,0 +1,44 @@
import { mergeAttributes, Node } from '@tiptap/core'
import React from 'react'
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'
import { StaticReactEmbed } from '../static-react-embed'
export default Node.create({
name: 'staticReactEmbedComponent',
group: 'block',
atom: true,
addAttributes() {
return {
embedName: '',
}
},
parseHTML() {
return [
{
tag: 'static-react-embed-component',
},
]
},
renderHTML({ HTMLAttributes }) {
return ['static-react-embed-component', mergeAttributes(HTMLAttributes)]
},
addNodeView() {
return ReactNodeViewRenderer(StaticReactEmbedComponent)
},
})
export function StaticReactEmbedComponent(props: any) {
const embedName = props.node.attrs.embedName
return (
<NodeViewWrapper className="static-react-embed-component">
<StaticReactEmbed embedName={embedName} />
</NodeViewWrapper>
)
}

View File

@ -7,7 +7,7 @@ export const ExpandingInput = (props: Parameters<typeof Textarea>[0]) => {
return ( return (
<Textarea <Textarea
className={clsx( className={clsx(
'textarea textarea-bordered resize-none text-[16px] md:text-[14px]', 'resize-none rounded-md border border-gray-300 bg-white px-4 text-[16px] leading-loose shadow-sm transition-colors focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 disabled:cursor-not-allowed disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 md:text-[14px]',
className className
)} )}
{...rest} {...rest}

View File

@ -1,32 +1,60 @@
import { Answer } from 'common/answer' import { Answer } from 'common/answer'
import { Contract } from 'common/contract' import {
import React, { useEffect, useRef } from 'react' Contract,
FreeResponseContract,
MultipleChoiceContract,
} from 'common/contract'
import React, { useEffect, useRef, useState } from 'react'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { Avatar } from 'web/components/avatar'
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { UserLink } from 'web/components/user-link' import { UserLink } from 'web/components/user-link'
import { FeedCommentThread } from './feed-comments'
import { AnswerCommentInput } from '../comment-input'
import { ContractComment } from 'common/comment'
import { Dictionary, sortBy } from 'lodash'
import { getAnswerColor } from '../answers/answers-panel'
import Curve from 'web/public/custom-components/curve'
import { CommentTipMap } from 'web/hooks/use-tip-txns'
import { useChartAnswers } from '../charts/contract/choice'
export function CommentsAnswer(props: { answer: Answer; contract: Contract }) { export function CommentsAnswer(props: {
const { answer, contract } = props answer: Answer
const { username, avatarUrl, name, text } = answer contract: Contract
color: string
}) {
const { answer, contract, color } = props
const { username, name, text } = answer
const answerElementId = `answer-${answer.id}` const answerElementId = `answer-${answer.id}`
const router = useRouter()
const highlighted = router.asPath.endsWith(`#${answerElementId}`) const { isReady, asPath } = useRouter()
const [highlighted, setHighlighted] = useState(false)
const answerRef = useRef<HTMLDivElement>(null) const answerRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
if (highlighted && answerRef.current != null) { if (isReady && asPath.endsWith(`#${answerElementId}`)) {
setHighlighted(true)
}
}, [isReady, asPath, answerElementId])
useEffect(() => {
if (highlighted && answerRef.current) {
answerRef.current.scrollIntoView(true) answerRef.current.scrollIntoView(true)
} }
}, [highlighted]) }, [highlighted])
return ( return (
<Col className="bg-greyscale-2 w-fit gap-1 rounded-t-xl rounded-bl-xl py-2 px-4"> <Row>
<div
className="w-2"
style={{
background: color ? color : '#B1B1C7',
}}
/>
<Col className="w-fit bg-gray-100 py-1 pl-2 pr-2">
<Row className="gap-2"> <Row className="gap-2">
<Avatar username={username} avatarUrl={avatarUrl} size="xxs" /> <div className="text-greyscale-4 text-xs">
<div className="text-greyscale-6 text-xs">
<UserLink username={username} name={name} /> answered <UserLink username={username} name={name} /> answered
<CopyLinkDateTimeComponent <CopyLinkDateTimeComponent
prefix={contract.creatorUsername} prefix={contract.creatorUsername}
@ -38,5 +66,89 @@ export function CommentsAnswer(props: { answer: Answer; contract: Contract }) {
</Row> </Row>
<div className="text-sm">{text}</div> <div className="text-sm">{text}</div>
</Col> </Col>
</Row>
)
}
export function FreeResponseComments(props: {
contract: FreeResponseContract | MultipleChoiceContract
answerResponse: Answer | undefined
onCancelAnswerResponse?: () => void
topLevelComments: ContractComment[]
commentsByParent: Dictionary<[ContractComment, ...ContractComment[]]>
tips: CommentTipMap
}) {
const {
contract,
answerResponse,
onCancelAnswerResponse,
topLevelComments,
commentsByParent,
tips,
} = props
const answersArray = useChartAnswers(contract).map((answer) => answer.text)
return (
<>
{answerResponse && (
<AnswerCommentInput
contract={contract}
answerResponse={answerResponse}
onCancelAnswerResponse={onCancelAnswerResponse}
answersArray={answersArray}
/>
)}
{topLevelComments.map((parent) => {
if (parent.answerOutcome === undefined) {
return (
<FeedCommentThread
key={parent.id}
contract={contract}
parentComment={parent}
threadComments={sortBy(
commentsByParent[parent.id] ?? [],
(c) => c.createdTime
)}
tips={tips}
/>
)
}
const answer = contract.answers.find(
(answer) => answer.id === parent.answerOutcome
)
if (answer === undefined) {
console.error('Could not find answer that matches ID')
return <></>
}
const color = getAnswerColor(answer, answersArray)
return (
<>
<Row className="relative">
<div className="absolute -bottom-1 left-1.5">
<Curve size={32} strokeWidth={1} color="#D8D8EB" />
</div>
<div className="ml-[38px]">
<CommentsAnswer
answer={answer}
contract={contract}
color={color}
/>
</div>
</Row>
<div className="w-full pt-1">
<FeedCommentThread
key={parent.id}
contract={contract}
parentComment={parent}
threadComments={sortBy(
commentsByParent[parent.id] ?? [],
(c) => c.createdTime
)}
tips={tips}
/>
</div>
</>
)
})}
</>
) )
} }

View File

@ -24,7 +24,7 @@ import { UserLink } from 'web/components/user-link'
import { CommentInput } from '../comment-input' import { CommentInput } from '../comment-input'
import { AwardBountyButton } from 'web/components/award-bounty-button' import { AwardBountyButton } from 'web/components/award-bounty-button'
import { ReplyIcon } from '@heroicons/react/solid' import { ReplyIcon } from '@heroicons/react/solid'
import { Button } from '../button' import { IconButton } from '../button'
import { ReplyToggle } from '../comments/reply-toggle' import { ReplyToggle } from '../comments/reply-toggle'
export type ReplyTo = { id: string; username: string } export type ReplyTo = { id: string; username: string }
@ -37,7 +37,7 @@ export function FeedCommentThread(props: {
}) { }) {
const { contract, threadComments, tips, parentComment } = props const { contract, threadComments, tips, parentComment } = props
const [replyTo, setReplyTo] = useState<ReplyTo>() const [replyTo, setReplyTo] = useState<ReplyTo>()
const [seeReplies, setSeeReplies] = useState(false) const [seeReplies, setSeeReplies] = useState(true)
const user = useUser() const user = useUser()
const onSubmitComment = useEvent(() => setReplyTo(undefined)) const onSubmitComment = useEvent(() => setReplyTo(undefined))
@ -154,33 +154,43 @@ export function ParentFeedComment(props: {
numComments={numComments} numComments={numComments}
onClick={onSeeReplyClick} onClick={onSeeReplyClick}
/> />
<Row className="grow justify-end gap-2"> <CommentActions
onReplyClick={onReplyClick}
comment={comment}
showTip={showTip}
myTip={myTip}
totalTip={totalTip}
contract={contract}
/>
</Row>
</Col>
</Row>
)
}
export function CommentActions(props: {
onReplyClick?: (comment: ContractComment) => void
comment: ContractComment
showTip?: boolean
myTip?: number
totalTip?: number
contract: Contract
}) {
const { onReplyClick, comment, showTip, myTip, totalTip, contract } = props
return (
<Row className="grow justify-end">
{onReplyClick && ( {onReplyClick && (
<Button <IconButton size={'xs'} onClick={() => onReplyClick(comment)}>
size={'sm'}
className={clsx(
'hover:bg-greyscale-2 mt-0 mb-1 max-w-xs px-0 py-0'
)}
color={'gray-white'}
onClick={() => onReplyClick(comment)}
>
<ReplyIcon className="h-5 w-5" /> <ReplyIcon className="h-5 w-5" />
</Button> </IconButton>
)} )}
{showTip && ( {showTip && (
<Tipper <Tipper comment={comment} myTip={myTip ?? 0} totalTip={totalTip ?? 0} />
comment={comment}
myTip={myTip ?? 0}
totalTip={totalTip ?? 0}
/>
)} )}
{(contract.openCommentBounties ?? 0) > 0 && ( {(contract.openCommentBounties ?? 0) > 0 && (
<AwardBountyButton comment={comment} contract={contract} /> <AwardBountyButton comment={comment} contract={contract} />
)} )}
</Row> </Row>
</Row>
</Col>
</Row>
) )
} }
@ -233,30 +243,14 @@ export const FeedComment = memo(function FeedComment(props: {
content={content || text} content={content || text}
smallImage smallImage
/> />
<Row className="grow justify-end gap-2"> <CommentActions
{onReplyClick && ( onReplyClick={onReplyClick}
<Button
size={'sm'}
className={clsx(
'hover:bg-greyscale-2 mt-0 mb-1 max-w-xs px-0 py-0'
)}
color={'gray-white'}
onClick={() => onReplyClick(comment)}
>
<ReplyIcon className="h-5 w-5" />
</Button>
)}
{showTip && (
<Tipper
comment={comment} comment={comment}
myTip={myTip ?? 0} showTip={showTip}
totalTip={totalTip ?? 0} myTip={myTip}
totalTip={totalTip}
contract={contract}
/> />
)}
{(contract.openCommentBounties ?? 0) > 0 && (
<AwardBountyButton comment={comment} contract={contract} />
)}
</Row>
</Col> </Col>
</Row> </Row>
) )

View File

@ -1,4 +1,4 @@
import { Button } from 'web/components/button' import { IconButton } from 'web/components/button'
import { import {
Contract, Contract,
followContract, followContract,
@ -33,9 +33,8 @@ export const FollowMarketButton = (props: {
noTap noTap
noFade noFade
> >
<Button <IconButton
size={'sm'} size="2xs"
color={'gray-white'}
onClick={async () => { onClick={async () => {
if (!user) return firebaseLogin() if (!user) return firebaseLogin()
if (followers?.includes(user.id)) { if (followers?.includes(user.id)) {
@ -65,18 +64,12 @@ export const FollowMarketButton = (props: {
> >
{watching ? ( {watching ? (
<Col className={'items-center gap-x-2 sm:flex-row'}> <Col className={'items-center gap-x-2 sm:flex-row'}>
<EyeOffIcon <EyeOffIcon className={clsx('h-5 w-5')} aria-hidden="true" />
className={clsx('h-5 w-5 sm:h-6 sm:w-6')}
aria-hidden="true"
/>
{/* Unwatch */} {/* Unwatch */}
</Col> </Col>
) : ( ) : (
<Col className={'items-center gap-x-2 sm:flex-row'}> <Col className={'items-center gap-x-2 sm:flex-row'}>
<EyeIcon <EyeIcon className={clsx('h-5 w-5')} aria-hidden="true" />
className={clsx('h-5 w-5 sm:h-6 sm:w-6')}
aria-hidden="true"
/>
{/* Watch */} {/* Watch */}
</Col> </Col>
)} )}
@ -87,7 +80,7 @@ export const FollowMarketButton = (props: {
followers?.includes(user?.id ?? 'nope') ? 'watched' : 'unwatched' followers?.includes(user?.id ?? 'nope') ? 'watched' : 'unwatched'
} a question!`} } a question!`}
/> />
</Button> </IconButton>
</Tooltip> </Tooltip>
) )
} }

View File

@ -103,7 +103,7 @@ export function CreateGroupButton(props: {
</Col> </Col>
{errorText && <div className={'text-error'}>{errorText}</div>} {errorText && <div className={'text-error'}>{errorText}</div>}
<div className="form-control w-full"> <div className="flex w-full flex-col">
<label className="mb-2 ml-1 mt-0">Group name</label> <label className="mb-2 ml-1 mt-0">Group name</label>
<Input <Input
placeholder={'Your group name'} placeholder={'Your group name'}

View File

@ -51,8 +51,8 @@ export function EditGroupButton(props: { group: Group; className?: string }) {
</Button> </Button>
<Modal open={open} setOpen={updateOpen}> <Modal open={open} setOpen={updateOpen}>
<div className="h-full rounded-md bg-white p-8"> <div className="h-full rounded-md bg-white p-8">
<div className="form-control w-full"> <div className="flex w-full flex-col">
<label className="label"> <label className="px-1 py-2">
<span className="mb-1">Group name</span> <span className="mb-1">Group name</span>
</label> </label>
@ -66,8 +66,8 @@ export function EditGroupButton(props: { group: Group; className?: string }) {
<Spacer h={4} /> <Spacer h={4} />
<div className="form-control w-full"> <div className="flex w-full flex-col">
<label className="label"> <label className="px-1 py-2">
<span className="mb-0">Add members</span> <span className="mb-0">Add members</span>
</label> </label>
<FilterSelectUsers <FilterSelectUsers
@ -77,7 +77,7 @@ export function EditGroupButton(props: { group: Group; className?: string }) {
/> />
</div> </div>
<div className="modal-action"> <div className="flex">
<Button <Button
color="red" color="red"
size="xs" size="xs"

View File

@ -33,11 +33,9 @@ export function GroupOverviewPost(props: {
function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) { function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {
const { group, post } = props const { group, post } = props
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
defaultValue: post?.content, defaultValue: post?.content,
disabled: isSubmitting,
}) })
async function savePost() { async function savePost() {
@ -76,10 +74,8 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {
<Row className="gap-2"> <Row className="gap-2">
<Button <Button
onClick={async () => { onClick={async () => {
setIsSubmitting(true)
await savePost() await savePost()
setEditing(false) setEditing(false)
setIsSubmitting(false)
}} }}
> >
Save Save

View File

@ -377,7 +377,7 @@ export function GroupAbout(props: {
<div className={'inline-flex items-center'}> <div className={'inline-flex items-center'}>
<div className="mr-1 text-gray-500">Created by</div> <div className="mr-1 text-gray-500">Created by</div>
<UserLink <UserLink
className="text-neutral" className="text-gray-700"
name={creator.name} name={creator.name}
username={creator.username} username={creator.username}
/> />

View File

@ -72,7 +72,7 @@ export function GroupSelector(props: {
) )
} }
return ( return (
<div className="form-control items-start"> <div className="flex flex-col items-start">
<Combobox <Combobox
as="div" as="div"
value={selectedGroup} value={selectedGroup}
@ -83,7 +83,7 @@ export function GroupSelector(props: {
{() => ( {() => (
<> <>
{showLabel && ( {showLabel && (
<Combobox.Label className="label justify-start gap-2 text-base"> <Combobox.Label className="justify-start gap-2 px-1 py-2 text-base">
Add to Group Add to Group
<InfoTooltip text="Question will be displayed alongside the other questions in the group." /> <InfoTooltip text="Question will be displayed alongside the other questions in the group." />
</Combobox.Label> </Combobox.Label>

View File

@ -2,21 +2,21 @@ import clsx from 'clsx'
import React from 'react' import React from 'react'
/** Text input. Wraps html `<input>` */ /** Text input. Wraps html `<input>` */
export const Input = (props: JSX.IntrinsicElements['input']) => { export const Input = (
const { className, ...rest } = props props: { error?: boolean } & JSX.IntrinsicElements['input']
) => {
const { error, className, ...rest } = props
return ( return (
<input <input
className={clsx('input input-bordered text-base md:text-sm', className)} className={clsx(
'h-12 rounded-md border bg-white px-4 shadow-sm transition-colors invalid:border-red-600 invalid:text-red-900 invalid:placeholder-red-300 focus:outline-none disabled:cursor-not-allowed disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 md:text-sm',
error
? 'border-red-300 text-red-900 placeholder-red-300 focus:border-red-600 focus:ring-red-500' // matches invalid: styles
: 'placeholder-greyscale-4 border-gray-300 focus:border-indigo-500 focus:ring-indigo-500',
className
)}
{...rest} {...rest}
/> />
) )
} }
/*
TODO: replace daisyui style with our own. For reference:
james: text-lg placeholder:text-gray-400
inga: placeholder:text-greyscale-4 border-greyscale-2 rounded-md
austin: border-gray-300 text-gray-400 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm
*/

View File

@ -2,6 +2,7 @@ import clsx from 'clsx'
import { Avatar } from './avatar' import { Avatar } from './avatar'
import { Row } from './layout/row' import { Row } from './layout/row'
import { SiteLink } from './site-link' import { SiteLink } from './site-link'
import { Table } from './table'
import { Title } from './title' import { Title } from './title'
interface LeaderboardEntry { interface LeaderboardEntry {
@ -31,9 +32,9 @@ export function Leaderboard<T extends LeaderboardEntry>(props: {
<div className="ml-2 text-gray-500">None yet</div> <div className="ml-2 text-gray-500">None yet</div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="table-zebra table-compact table w-full text-gray-500"> <Table>
<thead> <thead>
<tr className="p-2"> <tr>
<th>#</th> <th>#</th>
<th>Name</th> <th>Name</th>
{columns.map((column) => ( {columns.map((column) => (
@ -59,7 +60,7 @@ export function Leaderboard<T extends LeaderboardEntry>(props: {
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </Table>
</div> </div>
)} )}
</div> </div>

View File

@ -14,6 +14,7 @@ import { Row } from './layout/row'
import { LoadingIndicator } from './loading-indicator' import { LoadingIndicator } from './loading-indicator'
import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label' import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label'
import { Subtitle } from './subtitle' import { Subtitle } from './subtitle'
import { Table } from './table'
import { Title } from './title' import { Title } from './title'
export function LimitBets(props: { export function LimitBets(props: {
@ -74,7 +75,7 @@ export function LimitOrderTable(props: {
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
return ( return (
<table className="table-compact table w-full rounded text-gray-500"> <Table className="rounded">
<thead> <thead>
<tr> <tr>
{!isYou && <th></th>} {!isYou && <th></th>}
@ -89,7 +90,7 @@ export function LimitOrderTable(props: {
<LimitBet key={bet.id} bet={bet} contract={contract} isYou={isYou} /> <LimitBet key={bet.id} bet={bet} contract={contract} isYou={isYou} />
))} ))}
</tbody> </tbody>
</table> </Table>
) )
} }

View File

@ -9,9 +9,9 @@ import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { Claim, Manalink } from 'common/manalink' import { Claim, Manalink } from 'common/manalink'
import { ShareIconButton } from './share-icon-button' import { ShareIconButton } from './share-icon-button'
import { contractDetailsButtonClassName } from './contract/contract-info-dialog'
import { useUserById } from 'web/hooks/use-user' import { useUserById } from 'web/hooks/use-user'
import getManalinkUrl from 'web/get-manalink-url' import getManalinkUrl from 'web/get-manalink-url'
import { IconButton } from './button'
export type ManalinkInfo = { export type ManalinkInfo = {
expiresTime: number | null expiresTime: number | null
@ -123,7 +123,7 @@ export function ManalinkCardFromView(props: {
src="/logo-white.svg" src="/logo-white.svg"
/> />
</Col> </Col>
<Row className="relative w-full gap-1 rounded-b-lg bg-white px-4 py-2 text-lg"> <Row className="relative w-full rounded-b-lg bg-white px-4 py-2 align-middle text-lg">
<div <div
className={clsx( className={clsx(
'my-auto mb-1 w-full', 'my-auto mb-1 w-full',
@ -133,32 +133,23 @@ export function ManalinkCardFromView(props: {
{formatMoney(amount)} {formatMoney(amount)}
</div> </div>
<button <IconButton size="2xs" onClick={() => (window.location.href = qrUrl)}>
onClick={() => (window.location.href = qrUrl)}
className={clsx(contractDetailsButtonClassName)}
>
<QrcodeIcon className="h-6 w-6" /> <QrcodeIcon className="h-6 w-6" />
</button> </IconButton>
<ShareIconButton <ShareIconButton
toastClassName={'-left-48 min-w-[250%]'} toastClassName={'-left-48 min-w-[250%]'}
buttonClassName={'transition-colors'}
onCopyButtonClassName={
'bg-gray-200 text-gray-600 transition-none hover:bg-gray-200 hover:text-gray-600'
}
copyPayload={getManalinkUrl(link.slug)} copyPayload={getManalinkUrl(link.slug)}
/> />
<button <IconButton
size="xs"
onClick={() => setShowDetails(!showDetails)} onClick={() => setShowDetails(!showDetails)}
className={clsx( className={clsx(
contractDetailsButtonClassName, showDetails ? ' text-indigo-600 hover:text-indigo-700' : ''
showDetails
? 'bg-gray-200 text-gray-600 hover:bg-gray-200 hover:text-gray-600'
: ''
)} )}
> >
<DotsHorizontalIcon className="h-[24px] w-5" /> <DotsHorizontalIcon className="h-5 w-5" />
</button> </IconButton>
</Row> </Row>
</Col> </Col>
<div className="mt-2 mb-4 text-xs text-gray-500 md:text-sm"> <div className="mt-2 mb-4 text-xs text-gray-500 md:text-sm">

View File

@ -14,6 +14,7 @@ import { DuplicateIcon } from '@heroicons/react/outline'
import { QRCode } from '../qr-code' import { QRCode } from '../qr-code'
import { Input } from '../input' import { Input } from '../input'
import { ExpandingInput } from '../expanding-input' import { ExpandingInput } from '../expanding-input'
import { Select } from '../select'
export function CreateLinksButton(props: { export function CreateLinksButton(props: {
user: User user: User
@ -115,8 +116,8 @@ function CreateManalinkForm(props: {
> >
<Title className="!my-0" text="Create a Manalink" /> <Title className="!my-0" text="Create a Manalink" />
<div className="flex flex-col flex-wrap gap-x-5 gap-y-2"> <div className="flex flex-col flex-wrap gap-x-5 gap-y-2">
<div className="form-control flex-auto"> <div className="flex flex-auto flex-col">
<label className="label">Amount</label> <label className="px-1 py-2">Amount</label>
<div className="relative"> <div className="relative">
<span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> <span className="absolute mx-3 mt-3.5 text-sm text-gray-400">
M$ M$
@ -135,8 +136,8 @@ function CreateManalinkForm(props: {
</div> </div>
</div> </div>
<div className="flex flex-col gap-2 md:flex-row"> <div className="flex flex-col gap-2 md:flex-row">
<div className="form-control w-full md:w-1/2"> <div className="flex w-full flex-col md:w-1/2">
<label className="label">Uses</label> <label className="px-1 py-2">Uses</label>
<Input <Input
type="number" type="number"
min="1" min="1"
@ -148,10 +149,9 @@ function CreateManalinkForm(props: {
} }
/> />
</div> </div>
<div className="form-control w-full md:w-1/2"> <div className="flex w-full flex-col md:w-1/2">
<label className="label">Expires in</label> <label className="px-1 py-2">Expires in</label>
<select <Select
className="!select !select-bordered"
value={expiresIn} value={expiresIn}
defaultValue={defaultExpire} defaultValue={defaultExpire}
onChange={(e) => { onChange={(e) => {
@ -160,11 +160,11 @@ function CreateManalinkForm(props: {
}} }}
> >
{expireOptions} {expireOptions}
</select> </Select>
</div> </div>
</div> </div>
<div className="form-control w-full"> <div className="flex w-full flex-col">
<label className="label">Message</label> <label className="px-1 py-2">Message</label>
<ExpandingInput <ExpandingInput
placeholder={defaultMessage} placeholder={defaultMessage}
maxLength={200} maxLength={200}

View File

@ -0,0 +1,212 @@
import { useEffect } from 'react'
import { getContractFromSlug } from 'web/lib/firebase/contracts'
import {
StateElectionMap,
StateElectionMarket,
} from './usa-map/state-election-map'
import { useState } from 'react'
import { LoadingIndicator } from './loading-indicator'
import { CPMMBinaryContract } from 'common/contract'
export function MidtermsMaps(props: { mapType: string }) {
const { mapType } = props
const [contracts, setContracts] = useState<CPMMBinaryContract[] | null>(null)
useEffect(() => {
const getContracts = async () => {
if (props.mapType === 'senate') {
const senateContracts = await Promise.all(
senateMidterms.map((m) =>
getContractFromSlug(m.slug).then((c) => c ?? null)
)
)
setContracts(senateContracts as CPMMBinaryContract[])
} else if (props.mapType === 'governor') {
const governorContracts = await Promise.all(
governorMidterms.map((m) =>
getContractFromSlug(m.slug).then((c) => c ?? null)
)
)
setContracts(governorContracts as CPMMBinaryContract[])
}
}
getContracts()
}, [props.mapType, setContracts])
return contracts ? (
<StateElectionMap
markets={mapType == 'senate' ? senateMidterms : governorMidterms}
contracts={contracts}
/>
) : (
<LoadingIndicator />
)
}
const senateMidterms: StateElectionMarket[] = [
{
state: 'AZ',
creatorUsername: 'BTE',
slug: 'will-blake-masters-win-the-arizona',
isWinRepublican: true,
},
{
state: 'OH',
creatorUsername: 'BTE',
slug: 'will-jd-vance-win-the-ohio-senate-s',
isWinRepublican: true,
},
{
state: 'WI',
creatorUsername: 'BTE',
slug: 'will-ron-johnson-be-reelected-in-th',
isWinRepublican: true,
},
{
state: 'FL',
creatorUsername: 'BTE',
slug: 'will-marco-rubio-be-reelected-to-th',
isWinRepublican: true,
},
{
state: 'PA',
creatorUsername: 'MattP',
slug: 'will-dr-oz-be-elected-to-the-us-sen',
isWinRepublican: true,
},
{
state: 'GA',
creatorUsername: 'NcyRocks',
slug: 'will-a-democrat-win-the-2022-us-sen-3d2432ba6d79',
isWinRepublican: false,
},
{
state: 'NV',
creatorUsername: 'NcyRocks',
slug: 'will-a-democrat-win-the-2022-us-sen',
isWinRepublican: false,
},
{
state: 'NC',
creatorUsername: 'NcyRocks',
slug: 'will-a-democrat-win-the-2022-us-sen-6f1a901e1fcf',
isWinRepublican: false,
},
{
state: 'NH',
creatorUsername: 'NcyRocks',
slug: 'will-a-democrat-win-the-2022-us-sen-23194a72f1b7',
isWinRepublican: false,
},
{
state: 'UT',
creatorUsername: 'SG',
slug: 'will-mike-lee-win-the-2022-utah-sen',
isWinRepublican: true,
},
{
state: 'CO',
creatorUsername: 'SG',
slug: 'will-michael-bennet-win-the-2022-co',
isWinRepublican: false,
},
]
const governorMidterms: StateElectionMarket[] = [
{
state: 'TX',
creatorUsername: 'LarsDoucet',
slug: 'republicans-will-win-the-2022-texas',
isWinRepublican: true,
},
{
state: 'GA',
creatorUsername: 'MattP',
slug: 'will-stacey-abrams-win-the-2022-geo',
isWinRepublican: false,
},
{
state: 'FL',
creatorUsername: 'Tetraspace',
slug: 'if-charlie-crist-is-the-democratic',
isWinRepublican: false,
},
{
state: 'PA',
creatorUsername: 'JonathanMast',
slug: 'will-josh-shapiro-win-the-2022-penn',
isWinRepublican: false,
},
{
state: 'PA',
creatorUsername: 'JonathanMast',
slug: 'will-josh-shapiro-win-the-2022-penn',
isWinRepublican: false,
},
{
state: 'CO',
creatorUsername: 'ScottLawrence',
slug: 'will-jared-polis-be-reelected-as-co',
isWinRepublican: false,
},
{
state: 'OR',
creatorUsername: 'Tetraspace',
slug: 'if-tina-kotek-is-the-2022-democrati',
isWinRepublican: false,
},
{
state: 'MD',
creatorUsername: 'Tetraspace',
slug: 'if-wes-moore-is-the-2022-democratic',
isWinRepublican: false,
},
{
state: 'AK',
creatorUsername: 'SG',
slug: 'will-a-republican-win-the-2022-alas',
isWinRepublican: true,
},
{
state: 'AZ',
creatorUsername: 'SG',
slug: 'will-a-republican-win-the-2022-ariz',
isWinRepublican: true,
},
{
state: 'AZ',
creatorUsername: 'SG',
slug: 'will-a-republican-win-the-2022-ariz',
isWinRepublican: true,
},
{
state: 'WI',
creatorUsername: 'SG',
slug: 'will-a-democrat-win-the-2022-wiscon',
isWinRepublican: false,
},
{
state: 'NV',
creatorUsername: 'SG',
slug: 'will-a-democrat-win-the-2022-nevada',
isWinRepublican: false,
},
{
state: 'KS',
creatorUsername: 'SG',
slug: 'will-a-democrat-win-the-2022-kansas',
isWinRepublican: false,
},
{
state: 'NV',
creatorUsername: 'SG',
slug: 'will-a-democrat-win-the-2022-new-me',
isWinRepublican: false,
},
{
state: 'ME',
creatorUsername: 'SG',
slug: 'will-a-democrat-win-the-2022-maine',
isWinRepublican: false,
},
]

View File

@ -32,13 +32,8 @@ export function NumberInput(props: {
return ( return (
<Col className={className}> <Col className={className}>
<label className="input-group">
<Input <Input
className={clsx( className={clsx('max-w-[200px] !text-lg', inputClassName)}
'max-w-[200px] !text-lg',
error && 'input-error',
inputClassName
)}
ref={inputRef} ref={inputRef}
type="number" type="number"
pattern="[0-9]*" pattern="[0-9]*"
@ -46,10 +41,10 @@ export function NumberInput(props: {
placeholder={placeholder ?? '0'} placeholder={placeholder ?? '0'}
maxLength={9} maxLength={9}
value={numberString} value={numberString}
error={!!error}
disabled={disabled} disabled={disabled}
onChange={(e) => onChange(e.target.value.substring(0, 9))} onChange={(e) => onChange(e.target.value.substring(0, 9))}
/> />
</label>
<Spacer h={4} /> <Spacer h={4} />

View File

@ -4,18 +4,15 @@ import { getPseudoProbability } from 'common/pseudo-numeric'
import { BucketInput } from './bucket-input' import { BucketInput } from './bucket-input'
import { Input } from './input' import { Input } from './input'
import { Col } from './layout/col' import { Col } from './layout/col'
import { Spacer } from './layout/spacer'
export function ProbabilityInput(props: { function ProbabilityInput(props: {
prob: number | undefined prob: number | undefined
onChange: (newProb: number | undefined) => void onChange: (newProb: number | undefined) => void
disabled?: boolean disabled?: boolean
placeholder?: string placeholder?: string
className?: string
inputClassName?: string inputClassName?: string
}) { }) {
const { prob, onChange, disabled, placeholder, className, inputClassName } = const { prob, onChange, disabled, placeholder, inputClassName } = props
props
const onProbChange = (str: string) => { const onProbChange = (str: string) => {
let prob = parseInt(str.replace(/\D/g, '')) let prob = parseInt(str.replace(/\D/g, ''))
@ -29,10 +26,10 @@ export function ProbabilityInput(props: {
} }
return ( return (
<Col className={className}> <Col>
<label className="input-group"> <label className="relative w-fit">
<Input <Input
className={clsx('max-w-[200px] !text-lg', inputClassName)} className={clsx('pr-2 !text-lg', inputClassName)}
type="number" type="number"
max={99} max={99}
min={1} min={1}
@ -44,9 +41,10 @@ export function ProbabilityInput(props: {
disabled={disabled} disabled={disabled}
onChange={(e) => onProbChange(e.target.value)} onChange={(e) => onProbChange(e.target.value)}
/> />
<span className="bg-gray-200 text-sm">%</span> <span className="text-greyscale-4 absolute top-1/2 right-10 my-auto -translate-y-1/2">
%
</span>
</label> </label>
<Spacer h={4} />
</Col> </Col>
) )
} }
@ -82,7 +80,7 @@ export function ProbabilityOrNumericInput(props: {
/> />
) : ( ) : (
<ProbabilityInput <ProbabilityInput
inputClassName="w-full max-w-none" inputClassName="w-24"
prob={prob} prob={prob}
onChange={setProb} onChange={setProb}
disabled={isSubmitting} disabled={isSubmitting}

View File

@ -1,5 +1,4 @@
import { Input } from './input' import { Input } from './input'
import { Row } from './layout/row'
export function ProbabilitySelector(props: { export function ProbabilitySelector(props: {
probabilityInt: number probabilityInt: number
@ -9,8 +8,7 @@ export function ProbabilitySelector(props: {
const { probabilityInt, setProbabilityInt, isSubmitting } = props const { probabilityInt, setProbabilityInt, isSubmitting } = props
return ( return (
<Row className="items-center gap-2"> <label className="flex items-center text-lg">
<label className="input-group input-group-lg text-lg">
<Input <Input
type="number" type="number"
value={probabilityInt} value={probabilityInt}
@ -24,6 +22,5 @@ export function ProbabilitySelector(props: {
/> />
<span>%</span> <span>%</span>
</label> </label>
</Row>
) )
} }

View File

@ -7,7 +7,7 @@ import { useUserLikedContracts } from 'web/hooks/use-likes'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { XIcon } from '@heroicons/react/outline' import { XIcon } from '@heroicons/react/outline'
import { unLikeContract } from 'web/lib/firebase/likes' import { unLikeItem } from 'web/lib/firebase/likes'
import { contractPath } from 'web/lib/firebase/contracts' import { contractPath } from 'web/lib/firebase/contracts'
export function UserLikesButton(props: { user: User; className?: string }) { export function UserLikesButton(props: { user: User; className?: string }) {
@ -36,7 +36,7 @@ export function UserLikesButton(props: { user: User; className?: string }) {
</SiteLink> </SiteLink>
<XIcon <XIcon
className="ml-2 h-5 w-5 shrink-0 cursor-pointer" className="ml-2 h-5 w-5 shrink-0 cursor-pointer"
onClick={() => unLikeContract(user.id, likedContract.id)} onClick={() => unLikeItem(user.id, likedContract.id)}
/> />
</Row> </Row>
))} ))}

17
web/components/select.tsx Normal file
View File

@ -0,0 +1,17 @@
import clsx from 'clsx'
export const Select = (props: JSX.IntrinsicElements['select']) => {
const { className, children, ...rest } = props
return (
<select
className={clsx(
'h-12 cursor-pointer self-start overflow-hidden rounded-md border border-gray-300 pl-4 pr-10 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500',
className
)}
{...rest}
>
ß{children}
</select>
)
}

View File

@ -5,34 +5,22 @@ import clsx from 'clsx'
import { copyToClipboard } from 'web/lib/util/copy' import { copyToClipboard } from 'web/lib/util/copy'
import { ToastClipboard } from 'web/components/toast-clipboard' import { ToastClipboard } from 'web/components/toast-clipboard'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { contractDetailsButtonClassName } from 'web/components/contract/contract-info-dialog' import { IconButton } from './button'
export function ShareIconButton(props: { export function ShareIconButton(props: {
buttonClassName?: string
onCopyButtonClassName?: string
toastClassName?: string toastClassName?: string
children?: React.ReactNode children?: React.ReactNode
iconClassName?: string iconClassName?: string
copyPayload: string copyPayload: string
}) { }) {
const { const { toastClassName, children, iconClassName, copyPayload } = props
buttonClassName,
onCopyButtonClassName,
toastClassName,
children,
iconClassName,
copyPayload,
} = props
const [showToast, setShowToast] = useState(false) const [showToast, setShowToast] = useState(false)
return ( return (
<div className="relative z-10 flex-shrink-0"> <div className="relative z-10 flex-shrink-0">
<button <IconButton
className={clsx( size="2xs"
contractDetailsButtonClassName, className={clsx('mt-1', showToast ? 'text-indigo-600' : '')}
buttonClassName,
showToast ? onCopyButtonClassName : ''
)}
onClick={() => { onClick={() => {
copyToClipboard(copyPayload) copyToClipboard(copyPayload)
track('copy share link') track('copy share link')
@ -41,11 +29,11 @@ export function ShareIconButton(props: {
}} }}
> >
<LinkIcon <LinkIcon
className={clsx(iconClassName ? iconClassName : 'h-[24px] w-5')} className={clsx(iconClassName ? iconClassName : 'h-5 w-5')}
aria-hidden="true" aria-hidden="true"
/> />
{children} {children}
</button> </IconButton>
{showToast && <ToastClipboard className={toastClassName} />} {showToast && <ToastClipboard className={toastClassName} />}
</div> </div>

View File

@ -0,0 +1,20 @@
import { useState, useEffect } from 'react'
import { MidtermsMaps } from './midterms-maps'
export function StaticReactEmbed(props: { embedName: string }) {
const { embedName } = props
const [embed, setEmbed] = useState<JSX.Element | null>(null)
useEffect(() => {
const governorMidtermsMap = <MidtermsMaps mapType="governor" />
const senateMidtermsMap = <MidtermsMaps mapType="senate" />
if (embedName === 'governor-midterms-map') {
setEmbed(governorMidtermsMap)
} else if (embedName === 'senate-midterms-map') {
setEmbed(senateMidtermsMap)
}
}, [embedName, setEmbed])
return <div>{embed}</div>
}

21
web/components/table.tsx Normal file
View File

@ -0,0 +1,21 @@
import clsx from 'clsx'
/** `<table>` with styles. Expects table html (`<thead>`, `<td>` etc) */
export const Table = (props: {
zebra?: boolean
className?: string
children: React.ReactNode
}) => {
const { className, children } = props
return (
<table
className={clsx(
'w-full whitespace-nowrap text-left text-sm text-gray-500 [&_td]:p-2 [&_th]:p-2 [&>thead]:font-bold [&>tbody_tr:nth-child(odd)]:bg-white',
className
)}
>
{children}
</table>
)
}

View File

@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from 'react' import { useState } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { debounce } from 'lodash'
import { Comment } from 'common/comment' import { Comment } from 'common/comment'
import { User } from 'common/user' import { User } from 'common/user'
@ -9,8 +8,10 @@ import { transact } from 'web/lib/firebase/api'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { TipButton } from './contract/tip-button' import { TipButton } from './contract/tip-button'
import { Row } from './layout/row' import { Row } from './layout/row'
import { LIKE_TIP_AMOUNT } from 'common/like' import { LIKE_TIP_AMOUNT, TIP_UNDO_DURATION } from 'common/like'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { Button } from './button'
import clsx from 'clsx'
export function Tipper(prop: { export function Tipper(prop: {
comment: Comment comment: Comment
@ -19,24 +20,13 @@ export function Tipper(prop: {
}) { }) {
const { comment, myTip, totalTip } = prop const { comment, myTip, totalTip } = prop
// This is a temporary tipping amount before it actually gets confirmed. This is so tha we dont accidentally tip more than you have
const [tempTip, setTempTip] = useState(0)
const me = useUser() const me = useUser()
const [localTip, setLocalTip] = useState(myTip) const [saveTip] = useState(
() => async (user: User, comment: Comment, change: number) => {
// listen for user being set
const initialized = useRef(false)
useEffect(() => {
if (myTip && !initialized.current) {
setLocalTip(myTip)
initialized.current = true
}
}, [myTip])
const total = totalTip - myTip + localTip
// declare debounced function only on first render
const [saveTip] = useState(() =>
debounce(async (user: User, comment: Comment, change: number) => {
if (change === 0) { if (change === 0) {
return return
} }
@ -67,30 +57,88 @@ export function Tipper(prop: {
fromId: user.id, fromId: user.id,
toId: comment.userId, toId: comment.userId,
}) })
}, 1500) }
) )
// instant save on unrender
useEffect(() => () => void saveTip.flush(), [saveTip])
const addTip = (delta: number) => { const addTip = (delta: number) => {
setLocalTip(localTip + delta) setTempTip((tempTip) => tempTip + delta)
me && saveTip(me, comment, localTip - myTip + delta) const timeoutId = setTimeout(() => {
toast(`You tipped ${comment.userName} ${formatMoney(LIKE_TIP_AMOUNT)}!`) me &&
saveTip(me, comment, delta)
.then(() => setTempTip((tempTip) => tempTip - delta))
.catch((e) => console.error(e))
}, TIP_UNDO_DURATION + 1000)
toast.custom(
() => (
<TipToast
userName={comment.userName}
onUndoClick={() => {
clearTimeout(timeoutId)
setTempTip((tempTip) => tempTip - delta)
}}
/>
),
{ duration: TIP_UNDO_DURATION }
)
} }
const canUp = const canUp =
me && comment.userId !== me.id && me.balance >= localTip + LIKE_TIP_AMOUNT me && comment.userId !== me.id && me.balance - tempTip >= LIKE_TIP_AMOUNT
return ( return (
<Row className="items-center gap-0.5"> <Row className="items-center gap-0.5">
<TipButton <TipButton
tipAmount={LIKE_TIP_AMOUNT} tipAmount={LIKE_TIP_AMOUNT}
totalTipped={total} totalTipped={totalTip}
onClick={() => addTip(+LIKE_TIP_AMOUNT)} onClick={() => addTip(+LIKE_TIP_AMOUNT)}
userTipped={localTip > 0} userTipped={tempTip > 0 || myTip > 0}
disabled={!canUp} disabled={!canUp}
isCompact isCompact
/> />
</Row> </Row>
) )
} }
export function TipToast(props: { userName: string; onUndoClick: () => void }) {
const { userName, onUndoClick } = props
const [cancelled, setCancelled] = useState(false)
// There is a strange bug with toast where sometimes if you interact with one popup, the others will not dissappear at the right time, overriding it for now with this
const [timedOut, setTimedOut] = useState(false)
setTimeout(() => {
setTimedOut(true)
}, TIP_UNDO_DURATION)
if (timedOut) {
return <></>
}
return (
<div className="relative overflow-hidden rounded-lg bg-white drop-shadow-md">
<div
className={clsx(
'animate-progress-loading absolute bottom-0 z-10 h-1 w-full bg-indigo-600',
cancelled ? 'hidden' : ''
)}
/>
<Row className="text-greyscale-6 items-center gap-4 px-4 py-2 text-sm">
<div className={clsx(cancelled ? 'hidden' : 'inline')}>
Tipping {userName} {formatMoney(LIKE_TIP_AMOUNT)}...
</div>
<div className={clsx('py-1', cancelled ? 'inline' : 'hidden')}>
Cancelled tipping
</div>
<Button
className={clsx(cancelled ? 'hidden' : 'inline')}
size="xs"
color="gray-outline"
onClick={() => {
onUndoClick()
setCancelled(true)
}}
disabled={cancelled}
>
Cancel
</Button>
</Row>
</div>
)
}

View File

@ -8,7 +8,7 @@ export function ToastClipboard(props: { className?: string }) {
return ( return (
<Row <Row
className={clsx( className={clsx(
'border-base-300 absolute items-center' + 'border-greyscale-4 absolute items-center' +
'gap-2 divide-x divide-gray-200 rounded-md border-2 bg-white ' + 'gap-2 divide-x divide-gray-200 rounded-md border-2 bg-white ' +
'h-15 z-10 w-[15rem] p-2 pr-3 text-gray-500', 'h-15 z-10 w-[15rem] p-2 pr-3 text-gray-500',
className className

View File

@ -48,6 +48,7 @@ const BOT_USERNAMES = [
'MarketManagerBot', 'MarketManagerBot',
'Botlab', 'Botlab',
'JuniorBot', 'JuniorBot',
'ManifoldDream',
] ]
function BotBadge() { function BotBadge() {

View File

@ -291,7 +291,7 @@ export function ProfilePrivateStats(props: {
<Row className={'justify-between gap-4 sm:justify-end'}> <Row className={'justify-between gap-4 sm:justify-end'}>
<Col className={'text-greyscale-4 text-md sm:text-lg'}> <Col className={'text-greyscale-4 text-md sm:text-lg'}>
<span <span
className={clsx(profit >= 0 ? 'text-green-600' : 'text-red-400')} className={clsx(profit >= 0 ? 'text-teal-600' : 'text-red-400')}
> >
{formatMoney(profit)} {formatMoney(profit)}
</span> </span>

View File

@ -1,12 +1,17 @@
import { GlobalConfig } from 'common/globalConfig' import { GlobalConfig } from 'common/globalConfig'
import { useEffect, useState } from 'react' import { useEffect } from 'react'
import { listenForGlobalConfig } from 'web/lib/firebase/globalConfig' import { listenForGlobalConfig } from 'web/lib/firebase/globalConfig'
import { inMemoryStore, usePersistentState } from './use-persistent-state'
export const useGlobalConfig = () => { export const useGlobalConfig = () => {
const [globalConfig, setGlobalConfig] = useState<GlobalConfig | null>(null) const [globalConfig, setGlobalConfig] =
usePersistentState<GlobalConfig | null>(null, {
store: inMemoryStore(),
key: 'globalConfig',
})
useEffect(() => { useEffect(() => {
return listenForGlobalConfig(setGlobalConfig) return listenForGlobalConfig(setGlobalConfig)
}, []) }, [setGlobalConfig])
return globalConfig return globalConfig
} }

View File

@ -89,6 +89,17 @@ export const historyStore = <T>(prefix = '__manifold'): PersistentStore<T> => ({
}, },
}) })
const store: Record<string, any> = {}
export const inMemoryStore = <T>(): PersistentStore<T> => ({
get: (k: string) => {
return store[k]
},
set: (k: string, v: T | undefined) => {
store[k] = v
},
})
export const usePersistentState = <T>( export const usePersistentState = <T>(
initial: T, initial: T,
persist?: PersistenceOptions<T> persist?: PersistenceOptions<T>
@ -103,6 +114,7 @@ export const usePersistentState = <T>(
if (key != null && store != null) { if (key != null && store != null) {
store.set(key, state) store.set(key, state)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key, state]) }, [key, state])
if (store?.readsUrl) { if (store?.readsUrl) {
@ -116,6 +128,7 @@ export const usePersistentState = <T>(
const savedValue = key != null ? store.get(key) : undefined const savedValue = key != null ? store.get(key) : undefined
setState(savedValue ?? initial) setState(savedValue ?? initial)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router.isReady]) }, [router.isReady])
} }

View File

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
@ -12,7 +13,7 @@ type PropzProps = {
// This allows us to client-side render the page for authenticated users. // This allows us to client-side render the page for authenticated users.
// TODO: Could cache the result using stale-while-revalidate: https://swr.vercel.app/ // TODO: Could cache the result using stale-while-revalidate: https://swr.vercel.app/
export function usePropz( export function usePropz(
initialProps: Object, initialProps: Record<string, unknown>,
getStaticPropz: (props: PropzProps) => Promise<any> getStaticPropz: (props: PropzProps) => Promise<any>
) { ) {
// If props were successfully server-side generated, just use those // If props were successfully server-side generated, just use those
@ -29,6 +30,7 @@ export function usePropz(
if (router.isReady) { if (router.isReady) {
getStaticPropz({ params }).then((result) => setPropz(result.props)) getStaticPropz({ params }).then((result) => setPropz(result.props))
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params]) }, [params])
return propz return propz
} }

View File

@ -33,14 +33,14 @@ export function useTipTxns(on: {
}, [txns]) }, [txns])
} }
export function useMarketTipTxns(contractId: string): TipTxn[] { export function useItemTipTxns(itemId: string): TipTxn[] {
const [txns, setTxns] = useState<TipTxn[]>([]) const [txns, setTxns] = useState<TipTxn[]>([])
useEffect(() => { useEffect(() => {
return listenForTipTxns(contractId, (txns) => { return listenForTipTxns(itemId, (txns) => {
setTxns(txns.filter((txn) => !txn.data.commentId)) setTxns(txns.filter((txn) => !txn.data.commentId))
}) })
}, [contractId]) }, [itemId])
return txns return txns
} }

View File

@ -47,10 +47,7 @@ export const usePrefetchUsers = (userIds: string[]) => {
) )
} }
export const useUserContractMetricsByProfit = ( export const useUserContractMetricsByProfit = (userId: string, count = 50) => {
userId: string,
count: number
) => {
const positiveResult = useFirestoreQueryData<ContractMetrics>( const positiveResult = useFirestoreQueryData<ContractMetrics>(
['contract-metrics-descending', userId, count], ['contract-metrics-descending', userId, count],
getUserContractMetricsQuery(userId, count, 'desc') getUserContractMetricsQuery(userId, count, 'desc')
@ -71,10 +68,13 @@ export const useUserContractMetricsByProfit = (
if (!positiveResult.data || !negativeResult.data || !contracts) if (!positiveResult.data || !negativeResult.data || !contracts)
return undefined return undefined
const filteredContracts = filterDefined(contracts) as CPMMBinaryContract[] const filteredContracts = filterDefined(contracts).filter(
const filteredMetrics = metrics.filter( (c) => !c.isResolved
(m) => m.from && Math.abs(m.from.day.profit) >= 0.5 ) as CPMMBinaryContract[]
) const filteredMetrics = metrics
.filter((m) => m.from && Math.abs(m.from.day.profit) >= 0.5)
.filter((m) => filteredContracts.find((c) => c.id === m.contractId))
return { contracts: filteredContracts, metrics: filteredMetrics } return { contracts: filteredContracts, metrics: filteredMetrics }
} }

View File

@ -6,18 +6,23 @@ import { removeUndefinedProps } from 'common/util/object'
import { Like, LIKE_TIP_AMOUNT } from 'common/like' import { Like, LIKE_TIP_AMOUNT } from 'common/like'
import { track } from '@amplitude/analytics-browser' import { track } from '@amplitude/analytics-browser'
import { User } from 'common/user' import { User } from 'common/user'
import { Post } from 'common/post'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
function getLikesCollection(userId: string) { function getLikesCollection(userId: string) {
return collection(db, 'users', userId, 'likes') return collection(db, 'users', userId, 'likes')
} }
export const unLikeContract = async (userId: string, contractId: string) => { export const unLikeItem = async (userId: string, itemId: string) => {
const ref = await doc(getLikesCollection(userId), contractId) const ref = await doc(getLikesCollection(userId), itemId)
return await deleteDoc(ref) return await deleteDoc(ref)
} }
export const likeContract = async (user: User, contract: Contract) => { export const likeItem = async (
user: User,
item: Contract | Post,
itemType: string
) => {
if (user.balance < LIKE_TIP_AMOUNT) { if (user.balance < LIKE_TIP_AMOUNT) {
toast('You do not have enough M$ to like this market!') toast('You do not have enough M$ to like this market!')
return return
@ -28,27 +33,27 @@ export const likeContract = async (user: User, contract: Contract) => {
amount: LIKE_TIP_AMOUNT, amount: LIKE_TIP_AMOUNT,
fromId: user.id, fromId: user.id,
fromType: 'USER', fromType: 'USER',
toId: contract.creatorId, toId: item.creatorId,
toType: 'USER', toType: 'USER',
token: 'M$', token: 'M$',
category: 'TIP', category: 'TIP',
data: { contractId: contract.id }, data: { contractId: item.id },
description: `${user.name} liked contract ${contract.id} for M$ ${LIKE_TIP_AMOUNT} to ${contract.creatorId} `, description: `${user.name} liked ${itemType}${item.id} for M$ ${LIKE_TIP_AMOUNT} to ${item.creatorId} `,
}) })
console.log('result', result) console.log('result', result)
} }
// create new like in db under users collection // create new like in db under users collection
const ref = doc(getLikesCollection(user.id), contract.id) const ref = doc(getLikesCollection(user.id), item.id)
// contract slug and question are set via trigger // contract slug and question are set via trigger
const like = removeUndefinedProps({ const like = removeUndefinedProps({
id: ref.id, id: ref.id,
userId: user.id, userId: user.id,
createdTime: Date.now(), createdTime: Date.now(),
type: 'contract', type: itemType,
tipTxnId: result.txn.id, tipTxnId: result.txn.id,
} as Like) } as Like)
track('like', { track('like', {
contractId: contract.id, itemId: item.id,
}) })
await setDoc(ref, like) await setDoc(ref, like)
} }

View File

@ -26,14 +26,16 @@
"@hello-pangea/dnd": "16.0.0", "@hello-pangea/dnd": "16.0.0",
"@heroicons/react": "1.0.6", "@heroicons/react": "1.0.6",
"@react-query-firebase/firestore": "0.4.2", "@react-query-firebase/firestore": "0.4.2",
"@tiptap/core": "2.0.0-beta.182", "@tiptap/core": "2.0.0-beta.199",
"@tiptap/extension-character-count": "2.0.0-beta.31", "@tiptap/extension-character-count": "2.0.0-beta.199",
"@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-image": "2.0.0-beta.199",
"@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-link": "2.0.0-beta.199",
"@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/extension-mention": "2.0.0-beta.199",
"@tiptap/extension-placeholder": "2.0.0-beta.53", "@tiptap/extension-placeholder": "2.0.0-beta.199",
"@tiptap/react": "2.0.0-beta.114", "@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.191", "@tiptap/react": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"algoliasearch": "4.13.0", "algoliasearch": "4.13.0",
"browser-image-compression": "2.0.0", "browser-image-compression": "2.0.0",
"clsx": "1.1.1", "clsx": "1.1.1",
@ -44,7 +46,6 @@
"d3-scale": "4.0.2", "d3-scale": "4.0.2",
"d3-selection": "3.0.0", "d3-selection": "3.0.0",
"d3-shape": "3.1.0", "d3-shape": "3.1.0",
"daisyui": "1.16.4",
"dayjs": "1.10.7", "dayjs": "1.10.7",
"firebase": "9.9.3", "firebase": "9.9.3",
"gridjs": "5.0.2", "gridjs": "5.0.2",
@ -54,6 +55,7 @@
"next": "12.3.1", "next": "12.3.1",
"node-fetch": "3.2.4", "node-fetch": "3.2.4",
"prosemirror-state": "1.4.1", "prosemirror-state": "1.4.1",
"rc-slider": "10.0.1",
"react": "18.2.0", "react": "18.2.0",
"react-confetti": "6.0.1", "react-confetti": "6.0.1",
"react-dom": "18.2.0", "react-dom": "18.2.0",
@ -63,7 +65,7 @@
"react-masonry-css": "1.0.16", "react-masonry-css": "1.0.16",
"react-query": "3.39.0", "react-query": "3.39.0",
"react-twitter-embed": "4.0.4", "react-twitter-embed": "4.0.4",
"stability-client": "1.5.0", "stability-client": "1.6.1",
"string-similarity": "^4.0.4", "string-similarity": "^4.0.4",
"tippy.js": "6.3.7" "tippy.js": "6.3.7"
}, },

View File

@ -23,7 +23,7 @@ export default function Document() {
crossOrigin="anonymous" crossOrigin="anonymous"
/> />
</Head> </Head>
<body className="font-readex-pro bg-base-200 min-h-screen"> <body className="font-readex-pro bg-greyscale-1 min-h-screen">
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>

View File

@ -0,0 +1,23 @@
import { NextApiRequest, NextApiResponse } from 'next'
import {
CORS_ORIGIN_MANIFOLD,
CORS_ORIGIN_LOCALHOST,
} from 'common/envs/constants'
import { applyCorsHeaders } from 'web/lib/api/cors'
import { fetchBackend, forwardResponse } from 'web/lib/api/proxy'
export const config = { api: { bodyParser: true } }
export default async function route(req: NextApiRequest, res: NextApiResponse) {
await applyCorsHeaders(req, res, {
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
methods: 'POST',
})
try {
const backendRes = await fetchBackend(req, 'createcomment')
await forwardResponse(res, backendRes)
} catch (err) {
console.error('Error talking to cloud function: ', err)
res.status(500).json({ message: 'Error communicating with backend.' })
}
}

View File

@ -10,6 +10,7 @@ import {
} from 'web/hooks/use-persistent-state' } from 'web/hooks/use-persistent-state'
import { PAST_BETS } from 'common/user' import { PAST_BETS } from 'common/user'
import { Input } from 'web/components/input' import { Input } from 'web/components/input'
import { Select } from 'web/components/select'
const MAX_CONTRACTS_RENDERED = 100 const MAX_CONTRACTS_RENDERED = 100
@ -96,17 +97,13 @@ export default function ContractSearchFirestore(props: {
placeholder="Search markets" placeholder="Search markets"
className="w-full" className="w-full"
/> />
<select <Select value={sort} onChange={(e) => setSort(e.target.value)}>
className="select select-bordered"
value={sort}
onChange={(e) => setSort(e.target.value)}
>
<option value="score">Trending</option> <option value="score">Trending</option>
<option value="newest">Newest</option> <option value="newest">Newest</option>
<option value="most-traded">Most ${PAST_BETS}</option> <option value="most-traded">Most ${PAST_BETS}</option>
<option value="24-hour-vol">24h volume</option> <option value="24-hour-vol">24h volume</option>
<option value="close-date">Closing soon</option> <option value="close-date">Closing soon</option>
</select> </Select>
</div> </div>
<ContractsGrid contracts={matches} showTime={showTime} /> <ContractsGrid contracts={matches} showTime={showTime} />
</div> </div>

View File

@ -96,11 +96,9 @@ export default function Create(props: { auth: { user: User } }) {
<Title className="!mt-0" text="Create a market" /> <Title className="!mt-0" text="Create a market" />
<form> <form>
<div className="form-control w-full"> <div className="flex w-full flex-col">
<label className="label"> <label className="px-1 pt-2 pb-3">
<span className="mb-1">
Question<span className={'text-red-700'}>*</span> Question<span className={'text-red-700'}>*</span>
</span>
</label> </label>
<ExpandingInput <ExpandingInput
@ -229,7 +227,6 @@ export function NewContract(props: {
key: 'create market', key: 'create market',
max: MAX_DESCRIPTION_LENGTH, max: MAX_DESCRIPTION_LENGTH,
placeholder: descriptionPlaceholder, placeholder: descriptionPlaceholder,
disabled: isSubmitting,
defaultValue: params?.description defaultValue: params?.description
? JSON.parse(params.description) ? JSON.parse(params.description)
: undefined, : undefined,
@ -281,9 +278,7 @@ export function NewContract(props: {
return ( return (
<div> <div>
<label className="label"> <label className="px-1 pt-2 pb-3">Answer type</label>
<span className="mb-1">Answer type</span>
</label>
<Row> <Row>
<ChoicesToggleGroup <ChoicesToggleGroup
currentChoice={outcomeType} currentChoice={outcomeType}
@ -319,8 +314,8 @@ export function NewContract(props: {
{outcomeType === 'PSEUDO_NUMERIC' && ( {outcomeType === 'PSEUDO_NUMERIC' && (
<> <>
<div className="form-control mb-2 items-start"> <div className="mb-2 flex flex-col items-start">
<label className="label gap-2"> <label className="gap-2 px-1 py-2">
<span className="mb-1">Range</span> <span className="mb-1">Range</span>
<InfoTooltip text="The lower and higher bounds of the numeric range. Choose bounds the value could reasonably be expected to hit." /> <InfoTooltip text="The lower and higher bounds of the numeric range. Choose bounds the value could reasonably be expected to hit." />
</label> </label>
@ -364,8 +359,8 @@ export function NewContract(props: {
</div> </div>
)} )}
</div> </div>
<div className="form-control mb-2 items-start"> <div className="mb-2 flex flex-col items-start">
<label className="label gap-2"> <label className="gap-2 px-1 py-2">
<span className="mb-1">Initial value</span> <span className="mb-1">Initial value</span>
<InfoTooltip text="The starting value for this market. Should be in between min and max values." /> <InfoTooltip text="The starting value for this market. Should be in between min and max values." />
</label> </label>
@ -411,7 +406,7 @@ export function NewContract(props: {
)} )}
</Row> </Row>
<Row className="form-control my-2 items-center gap-2 text-sm"> <Row className="my-2 items-center gap-2 text-sm">
<span>Display this market on homepage</span> <span>Display this market on homepage</span>
<ShortToggle <ShortToggle
on={visibility === 'public'} on={visibility === 'public'}
@ -421,8 +416,8 @@ export function NewContract(props: {
<Spacer h={6} /> <Spacer h={6} />
<div className="form-control mb-1 items-start"> <div className="mb-1 flex flex-col items-start">
<label className="label mb-1 gap-2"> <label className="mb-1 gap-2 px-1 py-2">
<span>Question closes in</span> <span>Question closes in</span>
<InfoTooltip text="Predicting will be halted after this time (local timezone)." /> <InfoTooltip text="Predicting will be halted after this time (local timezone)." />
</label> </label>
@ -464,8 +459,8 @@ export function NewContract(props: {
<Spacer h={6} /> <Spacer h={6} />
<div className="form-control mb-1 items-start gap-1"> <div className="mb-1 flex flex-col items-start gap-1">
<label className="label gap-2"> <label className="gap-2 px-1 py-2">
<span className="mb-1">Description</span> <span className="mb-1">Description</span>
<InfoTooltip text="Optional. Describe how you will resolve this question." /> <InfoTooltip text="Optional. Describe how you will resolve this question." />
</label> </label>
@ -475,24 +470,24 @@ export function NewContract(props: {
<Spacer h={6} /> <Spacer h={6} />
<span className={'text-error'}>{errorText}</span> <span className={'text-error'}>{errorText}</span>
<Row className="items-end justify-between"> <Row className="items-end justify-between">
<div className="form-control mb-1 items-start"> <div className="mb-1 flex flex-col items-start">
<label className="label mb-1 gap-2"> <label className="mb-1 gap-2 px-1 py-2">
<span>Cost</span> <span>Cost</span>
<InfoTooltip <InfoTooltip
text={`Cost to create your question. This amount is used to subsidize predictions.`} text={`Cost to create your question. This amount is used to subsidize predictions.`}
/> />
</label> </label>
{!deservesFreeMarket ? ( {!deservesFreeMarket ? (
<div className="label-text text-neutral pl-1"> <div className="pl-1 text-sm text-gray-700">
{formatMoney(ante)} {formatMoney(ante)}
</div> </div>
) : ( ) : (
<Row> <Row className="text-sm">
<div className="label-text text-neutral pl-1 line-through"> <div className="pl-1 text-gray-700 line-through">
{formatMoney(ante)} {formatMoney(ante)}
</div> </div>
<div className="label-text text-primary pl-1">FREE </div> <div className="text-primary pl-1">FREE </div>
<div className="label-text pl-1 text-gray-500"> <div className="pl-1 text-gray-500">
(You have{' '} (You have{' '}
{FREE_MARKETS_PER_USER_MAX - (creator?.freeMarketsCreated ?? 0)}{' '} {FREE_MARKETS_PER_USER_MAX - (creator?.freeMarketsCreated ?? 0)}{' '}
free markets left) free markets left)

View File

@ -28,7 +28,7 @@ export default function DailyMovers() {
function ProbChangesWrapper(props: { userId: string }) { function ProbChangesWrapper(props: { userId: string }) {
const { userId } = props const { userId } = props
const data = useUserContractMetricsByProfit(userId, 50) const data = useUserContractMetricsByProfit(userId)
if (!data) return <LoadingIndicator /> if (!data) return <LoadingIndicator />

View File

@ -35,9 +35,7 @@ export default function CreateDateDocPage() {
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({})
disabled: isSubmitting,
})
const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined
const isValid = const isValid =

View File

@ -228,7 +228,7 @@ function GroupMembersList(props: { group: Group }) {
const { totalMembers } = group const { totalMembers } = group
if (totalMembers === 1) return <div /> if (totalMembers === 1) return <div />
return ( return (
<div className="text-neutral flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1 text-gray-700">
<span>{totalMembers} members</span> <span>{totalMembers} members</span>
</div> </div>
) )

View File

@ -1,4 +1,4 @@
import React, { ReactNode, useEffect, useState } from 'react' import React, { ReactNode, useEffect } from 'react'
import Router from 'next/router' import Router from 'next/router'
import { import {
AdjustmentsIcon, AdjustmentsIcon,
@ -63,6 +63,10 @@ import { useAllPosts } from 'web/hooks/use-post'
import { useGlobalConfig } from 'web/hooks/use-global-config' import { useGlobalConfig } from 'web/hooks/use-global-config'
import { useAdmin } from 'web/hooks/use-admin' import { useAdmin } from 'web/hooks/use-admin'
import { GlobalConfig } from 'common/globalConfig' import { GlobalConfig } from 'common/globalConfig'
import {
inMemoryStore,
usePersistentState,
} from 'web/hooks/use-persistent-state'
export default function Home() { export default function Home() {
const user = useUser() const user = useUser()
@ -91,8 +95,7 @@ export default function Home() {
}, [user, sections]) }, [user, sections])
const contractMetricsByProfit = useUserContractMetricsByProfit( const contractMetricsByProfit = useUserContractMetricsByProfit(
user?.id ?? '_', user?.id ?? '_'
3
) )
const trendingContracts = useTrendingContracts(6) const trendingContracts = useTrendingContracts(6)
@ -105,7 +108,10 @@ export default function Home() {
groups?.map((g) => g.slug) groups?.map((g) => g.slug)
) )
const [pinned, setPinned] = useState<JSX.Element[] | null>(null) const [pinned, setPinned] = usePersistentState<JSX.Element[] | null>(null, {
store: inMemoryStore(),
key: 'home-pinned',
})
useEffect(() => { useEffect(() => {
const pinnedItems = globalConfig?.pinnedItems const pinnedItems = globalConfig?.pinnedItems
@ -139,7 +145,7 @@ export default function Home() {
} }
} }
getPinned() getPinned()
}, [globalConfig]) }, [globalConfig, setPinned])
const isLoading = const isLoading =
!user || !user ||
@ -487,7 +493,7 @@ function DailyMoversSection(props: {
return ( return (
<Col className="gap-2"> <Col className="gap-2">
<SectionHeader label="Daily movers" href="/daily-movers" /> <SectionHeader label="Daily movers" href="/daily-movers" />
<ProfitChangeTable contracts={contracts} metrics={metrics} /> <ProfitChangeTable contracts={contracts} metrics={metrics} maxRows={3} />
</Col> </Col>
) )
} }
@ -523,8 +529,7 @@ export function DailyProfit(props: { user: User | null | undefined }) {
const { user } = props const { user } = props
const contractMetricsByProfit = useUserContractMetricsByProfit( const contractMetricsByProfit = useUserContractMetricsByProfit(
user?.id ?? '_', user?.id ?? '_'
100
) )
const profit = sum( const profit = sum(
contractMetricsByProfit?.metrics.map((m) => contractMetricsByProfit?.metrics.map((m) =>

View File

@ -26,6 +26,7 @@ import { useUser } from 'web/hooks/use-user'
import { usePost } from 'web/hooks/use-post' import { usePost } from 'web/hooks/use-post'
import { SEO } from 'web/components/SEO' import { SEO } from 'web/components/SEO'
import { Subtitle } from 'web/components/subtitle' import { Subtitle } from 'web/components/subtitle'
import { LikeItemButton } from 'web/components/contract/like-item-button'
export async function getStaticProps(props: { params: { slugs: string[] } }) { export async function getStaticProps(props: { params: { slugs: string[] } }) {
const { slugs } = props.params const { slugs } = props.params
@ -81,17 +82,20 @@ export default function PostPage(props: {
<br /> <br />
<Subtitle className="!mt-2 px-2 pb-4" text={post.subtitle} /> <Subtitle className="!mt-2 px-2 pb-4" text={post.subtitle} />
</div> </div>
<Row> <Row className="items-center">
<Col className="flex-1 px-2"> <Col className="flex-1 px-2">
<div className={'inline-flex'}> <div className={'inline-flex'}>
<div className="mr-1 text-gray-500">Created by</div> <div className="mr-1 text-gray-500">Created by</div>
<UserLink <UserLink
className="text-neutral" className="text-gray-700"
name={creator.name} name={creator.name}
username={creator.username} username={creator.username}
/> />
</div> </div>
</Col> </Col>
<Row className="items-center">
<LikeItemButton item={post} user={user} itemType={'post'} />
<Col className="px-2"> <Col className="px-2">
<Button <Button
size="lg" size="lg"
@ -114,10 +118,11 @@ export default function PostPage(props: {
</Button> </Button>
</Col> </Col>
</Row> </Row>
</Row>
<Spacer h={2} /> <Spacer h={2} />
<div className="rounded-lg bg-white px-6 py-4 sm:py-0"> <div className="rounded-lg bg-white px-6 py-4 sm:py-0">
<div className="form-control w-full py-2"> <div className="flex w-full flex-col py-2">
{user && user.id === post.creatorId ? ( {user && user.id === post.creatorId ? (
<RichEditPost post={post} /> <RichEditPost post={post} />
) : ( ) : (
@ -171,11 +176,9 @@ export function PostCommentsActivity(props: {
export function RichEditPost(props: { post: Post }) { export function RichEditPost(props: { post: Post }) {
const { post } = props const { post } = props
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
defaultValue: post.content, defaultValue: post.content,
disabled: isSubmitting,
}) })
async function savePost() { async function savePost() {
@ -193,10 +196,8 @@ export function RichEditPost(props: { post: Post }) {
<Row className="gap-2"> <Row className="gap-2">
<Button <Button
onClick={async () => { onClick={async () => {
setIsSubmitting(true)
await savePost() await savePost()
setEditing(false) setEditing(false)
setIsSubmitting(false)
}} }}
> >
Save Save

View File

@ -43,7 +43,7 @@ function EditUserField(props: {
return ( return (
<div> <div>
<label className="label">{label}</label> <label className="px-1 py-2">{label}</label>
{field === 'bio' ? ( {field === 'bio' ? (
<ExpandingInput <ExpandingInput
@ -156,7 +156,7 @@ export default function ProfilePage(props: {
</Row> </Row>
<div> <div>
<label className="label">Display name</label> <label className="px-1 py-2">Display name</label>
<Input <Input
type="text" type="text"
placeholder="Display name" placeholder="Display name"
@ -167,7 +167,7 @@ export default function ProfilePage(props: {
</div> </div>
<div> <div>
<label className="label">Username</label> <label className="px-1 py-2">Username</label>
<Input <Input
type="text" type="text"
placeholder="Username" placeholder="Username"
@ -193,15 +193,15 @@ export default function ProfilePage(props: {
))} ))}
<div> <div>
<label className="label">Email</label> <label className="px-1 py-2">Email</label>
<div className="ml-1 text-gray-500"> <div className="ml-1 text-gray-500">
{privateUser.email ?? '\u00a0'} {privateUser.email ?? '\u00a0'}
</div> </div>
</div> </div>
<div> <div>
<label className="label">API key</label> <label className="px-1 py-2">API key</label>
<div className="input-group w-full"> <div className="flex w-full items-stretch">
<Input <Input
type="text" type="text"
placeholder="Click refresh to generate key" placeholder="Click refresh to generate key"

View File

@ -25,6 +25,7 @@ import { Carousel } from 'web/components/carousel'
import { usePagination } from 'web/hooks/use-pagination' import { usePagination } from 'web/hooks/use-pagination'
import { LoadingIndicator } from 'web/components/loading-indicator' import { LoadingIndicator } from 'web/components/loading-indicator'
import { Title } from 'web/components/title' import { Title } from 'web/components/title'
import { useTracking } from 'web/hooks/use-tracking'
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
@ -170,6 +171,9 @@ export default function TournamentPage(props: { sections: SectionInfo[] }) {
const description = `Win real prizes (including cash!) by participating in forecasting const description = `Win real prizes (including cash!) by participating in forecasting
tournaments on current events, sports, science, and more.` tournaments on current events, sports, science, and more.`
useTracking('view tournaments')
return ( return (
<Page> <Page>
<SEO title="Tournaments" description={description} /> <SEO title="Tournaments" description={description} />

View File

@ -0,0 +1,29 @@
export default function Coin({
size = 18,
color = '#66667C',
strokeWidth = 1.5,
fill = 'none',
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 18 18"
width={size}
height={size}
fill={fill}
stroke={color}
strokeWidth={strokeWidth}
opacity={50}
transform="rotate(-30)"
>
<path
className="cls-2"
d="M15,9c0,.35-.07,.68-.2,1-.66,1.73-3.01,3-5.8,3s-5.14-1.27-5.8-3c-.13-.32-.2-.65-.2-1,0-2.21,2.69-4,6-4s6,1.79,6,4Z"
/>
<path
className="cls-1"
d="M15,9v2c0,2.21-2.69,4-6,4s-6-1.79-6-4v-2c0,.35,.07,.68,.2,1,.66,1.73,3.01,3,5.8,3s5.14-1.27,5.8-3c.13-.32,.2-.65,.2-1Z"
/>
</svg>
)
}

View File

@ -12,6 +12,7 @@ export default function Curve({
fill="none" fill="none"
stroke={color} stroke={color}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
transform="rotate(90)"
> >
<path d="M5.02,0V5.24c0,4.3,3.49,7.79,7.79,7.79h5.2" /> <path d="M5.02,0V5.24c0,4.3,3.49,7.79,7.79,7.79h5.2" />
</svg> </svg>

View File

@ -0,0 +1,23 @@
export default function TipJar({
size = 18,
color = '#66667C',
strokeWidth = 1.5,
fill = 'none',
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 18 18"
width={size}
height={size}
fill={fill}
stroke={color}
strokeWidth={strokeWidth}
opacity={50}
>
<path d="M15.5,8.1v5.8c0,1.43-1.16,2.6-2.6,2.6H5.1c-1.44,0-2.6-1.16-2.6-2.6v-5.8c0-1.04,.89-3.25,1.5-4.1h0v-2c0-.55,.45-1,1-1H13c.55,0,1,.45,1,1v2h0c.61,.85,1.5,3.06,1.5,4.1Z" />
<line x1="4" y1="4" x2="9" y2="4" />
<line x1="11.26" y1="4" x2="14" y2="4" />
</svg>
)
}

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const defaultTheme = require('tailwindcss/defaultTheme') const defaultTheme = require('tailwindcss/defaultTheme')
const plugin = require('tailwindcss/plugin') const plugin = require('tailwindcss/plugin')
@ -15,7 +16,20 @@ module.exports = {
} }
), ),
extend: { extend: {
keyframes: {
progress: {
'0%': { width: '0%' },
'100%': { width: '100%' },
},
},
animation: {
'progress-loading': 'progress 2s linear',
},
colors: { colors: {
primary: '#11b981',
'primary-focus': '#069668',
warning: '#F59E0B', // amber-500 TODO: change color
error: '#ff5724', // TODO: change color
'red-25': '#FDF7F6', 'red-25': '#FDF7F6',
'greyscale-1': '#FBFBFF', 'greyscale-1': '#FBFBFF',
'greyscale-1.5': '#F4F4FB', 'greyscale-1.5': '#F4F4FB',
@ -42,7 +56,6 @@ module.exports = {
require('@tailwindcss/forms'), require('@tailwindcss/forms'),
require('@tailwindcss/typography'), require('@tailwindcss/typography'),
require('@tailwindcss/line-clamp'), require('@tailwindcss/line-clamp'),
require('daisyui'),
plugin(function ({ addUtilities }) { plugin(function ({ addUtilities }) {
addUtilities({ addUtilities({
'.scrollbar-hide': { '.scrollbar-hide': {
@ -61,57 +74,7 @@ module.exports = {
'overflow-wrap': 'anywhere', 'overflow-wrap': 'anywhere',
'word-break': 'break-word', // for Safari 'word-break': 'break-word', // for Safari
}, },
'.only-thumb': {
'pointer-events': 'none',
'&::-webkit-slider-thumb': {
'pointer-events': 'auto !important',
},
'&::-moz-range-thumb': {
'pointer-events': 'auto !important',
},
'&::-ms-thumb': {
'pointer-events': 'auto !important',
},
},
}) })
}), }),
], ],
daisyui: {
themes: [
{
mantic: {
primary: '#11b981',
'primary-focus': '#069668',
// Foreground content color to use on primary color
'primary-content': '#ffffff',
secondary: '#a991f7',
'secondary-focus': '#8462f4',
// Foreground content color to use on secondary color
'secondary-content': '#ffffff',
accent: '#f6d860',
'accent-focus': '#f3cc30',
// Foreground content color to use on accent color
'accent-content': '#ffffff',
neutral: '#3d4451',
'neutral-focus': '#2a2e37',
// Foreground content color to use on neutral color
'neutral-content': '#ffffff',
'base-100': '#ffffff' /* Base page color, for blank backgrounds */,
'base-200': '#f9fafb' /* Base color, a little darker */,
'base-300': '#d1d5db' /* Base color, even more dark */,
// Foreground content color to use on base color
'base-content': '#1f2937',
info: '#2094f3',
success: '#009485',
warning: '#ff9900',
error: '#ff5724',
},
},
],
},
} }

468
yarn.lock
View File

@ -1303,6 +1303,13 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.1", "@babel/runtime@^7.18.3":
version "7.19.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78"
integrity sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.3", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.17.2", "@babel/runtime@^7.8.4": "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.17.2", "@babel/runtime@^7.8.4":
version "7.18.3" version "7.18.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
@ -2855,219 +2862,227 @@
lodash.isplainobject "^4.0.6" lodash.isplainobject "^4.0.6"
lodash.merge "^4.6.2" lodash.merge "^4.6.2"
"@tiptap/core@2.0.0-beta.182", "@tiptap/core@^2.0.0-beta.182": "@tiptap/core@2.0.0-beta.199", "@tiptap/core@^2.0.0-beta.199":
version "2.0.0-beta.182" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.182.tgz#d2001e9b765adda95e15d171479860a3349e2d04" resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.199.tgz#77c3d8df10a4594cf5860b8a73d5007b0a020d01"
integrity sha512-MZGkMGnVnWhBzjvpBNwQ9zBz38ndi3Irbf90uCTSArR0kaCVkW4vmyuPuOXd+0SO8Yv/l5oyDdOCpaG3rnQYfw== integrity sha512-34GaXcBEmNFjW1R7nf1LSmOHo3Q81YjKqvLAXjDLLG7MTx+YTrQ4yWwUvMsZtmi4o/FchUzrs1NVCfr571Zxzg==
dependencies: dependencies:
prosemirror-commands "1.3.0" prosemirror-commands "^1.3.1"
prosemirror-keymap "1.2.0" prosemirror-keymap "^1.2.0"
prosemirror-model "1.18.1" prosemirror-model "^1.18.1"
prosemirror-schema-list "1.2.0" prosemirror-schema-list "^1.2.2"
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
prosemirror-transform "1.6.0" prosemirror-transform "^1.7.0"
prosemirror-view "1.26.2" prosemirror-view "^1.28.2"
"@tiptap/extension-blockquote@^2.0.0-beta.29": "@tiptap/extension-blockquote@^2.0.0-beta.199":
version "2.0.0-beta.29" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.0.0-beta.29.tgz#6f1c4b17efa6457c7776f32d0807e96d848d4389" resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.0.0-beta.199.tgz#f3957a638d515f6e38cd91eacb59cbedef06af6c"
integrity sha512-zMYT5TtpKWav9VhTn4JLyMvXmhEdbD6on0MdhcTjRm0I5ugyR4ZbJwh2aelM7G9DZVYzB8jZU18OSDJmo7Af7w== integrity sha512-BbHKaIkVYgJCV5giJC3/bdXMZWxFylLKiAbOGSGwIsnnS5/oL+V4XN6hqcIDBxlcj3MQ/d9zG0+mvFyjRssAkg==
"@tiptap/extension-bold@^2.0.0-beta.28": "@tiptap/extension-bold@^2.0.0-beta.199":
version "2.0.0-beta.28" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.0.0-beta.28.tgz#cf67c264a80434ffb2368f3dd37cf357ae0c2064" resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.0.0-beta.199.tgz#edb3dca9ca49ec128611629fbb5d17eaca62c506"
integrity sha512-DY8GOzw9xjmTFrnvTbgHUNxTnDfKrkDgrhe0SUvdkT2udntWp8umPdhPiD3vczLgHOJw6tX68qMRjbsR1ZPcHQ== integrity sha512-l513jgGLmt8C69Yuh5Et7a46Tn8QpW4q1HhZK6ih0ajNT+L5Xk0CSxEK/K5EmHSACPhwqjsJztLpGjAdoOn0mA==
"@tiptap/extension-bubble-menu@^2.0.0-beta.61": "@tiptap/extension-bubble-menu@^2.0.0-beta.199":
version "2.0.0-beta.61" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.61.tgz#cc61ce8b094fdbcec58f44f0fa39172a726c024c" resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.199.tgz#23a01c4c57f5af2197299d836e18ffe498fd8d67"
integrity sha512-T3Yx+y1sUnXAJjK1CUfsQewSxOpDca9KzKqN2H9c9RZ9UlorR9XmZg6YYW7m9a7adeihj+o3cCO9jRd8dV+nnA== integrity sha512-T3K8xoDbX6J62lhIUpclQoW/1XFt7yfI5DCoxtVWUeKaF+pG6kdsB3CPG5C/+AQVlz2jSIJmQuPf8RQFpQs+yg==
dependencies: dependencies:
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
prosemirror-view "1.26.2" prosemirror-view "^1.28.2"
tippy.js "^6.3.7" tippy.js "^6.3.7"
"@tiptap/extension-bullet-list@^2.0.0-beta.29": "@tiptap/extension-bullet-list@^2.0.0-beta.199":
version "2.0.0-beta.29" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.0-beta.29.tgz#640883e4fffc1a86c7cbd78792688e7edee5ee41" resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.0-beta.199.tgz#72df0c94c6a9a5bce97ee1e0180657f1a63ffe18"
integrity sha512-R8VB2l1ZB6VeGWx/t/04nBS5Wg3qjIDEZCpPihj2fccJOw99Lu0Ub2UJg/SfdGmeNNpBh4ZYYFv1g/XjyzlXKg== integrity sha512-gGRQRqdQqCZQstB3ztSy8yzIdm5/5IIYxhCuFNb3Z9c9p/CzyRmaNqa7XkRLrXSajp4lS0OH8RkFUJqL6U+/9w==
"@tiptap/extension-character-count@2.0.0-beta.31": "@tiptap/extension-character-count@2.0.0-beta.199":
version "2.0.0-beta.31" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-character-count/-/extension-character-count-2.0.0-beta.31.tgz#fac9ba809ddc38cf67c8a05a42d94e062a1967d2" resolved "https://registry.yarnpkg.com/@tiptap/extension-character-count/-/extension-character-count-2.0.0-beta.199.tgz#cd6b008a8e242240c30dda7ee5fe515fc39a8d78"
integrity sha512-NNA9MN1IjZe+yYQLuYVAg9RNG/3RonYrHiM5mL6vsegd+PF4uMqyZLgsM0/9dMhxh9K/pDPaCRxhuDoZC8V1wA== integrity sha512-7QEyLZeTVHRi7XaI97n+yF/R9cs7Xo1pf/cpV+wn/QSkfr5YtYvy33LCN74Vlkw9FJ45KrROEXfawRE/UgM/Cw==
dependencies: dependencies:
prosemirror-model "1.18.1" prosemirror-model "^1.18.1"
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
"@tiptap/extension-code-block@^2.0.0-beta.42": "@tiptap/extension-code-block@^2.0.0-beta.199":
version "2.0.0-beta.42" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.0.0-beta.42.tgz#2abfd92eb22399fa542aafb3b76dddfb41d87ab5" resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.0.0-beta.199.tgz#145baa37276601bce75ddad55ab4926bc78b28fc"
integrity sha512-4wzLup4mI8w9ypIceekUV/8g41cQIPn31qs1iC9u1/JuTkjMj/tA+TFUyp6IMugLxoI/P2DlTztU6/6m7n9DyQ== integrity sha512-ZfftYE1kHA2pD46hXDkeYd1vuxp3bJLS854B2yHfw1cp3JVDjMXzm4Mzg7zLfr+YV1dT/N/fUfdCg38fqEUCyA==
dependencies: dependencies:
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
"@tiptap/extension-code@^2.0.0-beta.28": "@tiptap/extension-code@^2.0.0-beta.199":
version "2.0.0-beta.28" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.0.0-beta.28.tgz#a22c0e873497ac0bbcd77e4a855322f8591f954e" resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.0.0-beta.199.tgz#ab6e2355297b3faddf6ac6e9e02a52eca38c16f6"
integrity sha512-QPJ2Gwb1+3NgcC1ZIhvVcb+FsnWWDu5VZXTKXM4mz892i9V2x48uHg5anPiUV6pcolXsW1F5VNbXIHGTUUO6CQ== integrity sha512-P1U/xYD0MLT7JU2OHb3QoW7+JiPZXizFG/gTYmAHQV/gLH87cmflI7pPnloBdTkeIF0Q/cd6sSd75V9FxR4XJA==
"@tiptap/extension-document@^2.0.0-beta.17": "@tiptap/extension-document@^2.0.0-beta.199":
version "2.0.0-beta.17" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.0-beta.17.tgz#ded4182dd860762bcf41c588f712d83908c472a3" resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.0-beta.199.tgz#4a9432ab239cc951b4d9903a90d3a7d5597d3318"
integrity sha512-L6sg0FNchbtIpQkCSjMmItVGs3/vep8Fq56WRtDc1wBSGUSmtHaxQG7F2FZLnNIUMuvzVMRD81m2vYG73WkY6A== integrity sha512-l/3k9N2O4wIMQoN/SM3aIBwOhZ2KRxQoqGJfsbAUUwBURBDiT4N2VZaNiJC/w3xCVQXIxHSIlqtm9ZBcZeiH/Q==
"@tiptap/extension-dropcursor@^2.0.0-beta.29": "@tiptap/extension-dropcursor@^2.0.0-beta.199":
version "2.0.0-beta.29" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.0-beta.29.tgz#9ccc9d82cb9f8fa28a59ffc061c4c83ee059a12c" resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.0-beta.199.tgz#93d04db4ca56614b7b180632456cb9f2098c6156"
integrity sha512-I+joyoFB8pfdXUPLMqdNO08nlB5m2lbu0VQ5dpqdi/HzgVThMZPZA1cW0X8vAUvrALs5/JFRiFoR9hrLN5R5ng== integrity sha512-RhdYm0yBJxVLECaHWsZcBIwRJUoUqZ79jvs+kUVodxHW4+IxRAgEA+lImr0GD+kk8aX5Mrk8YhWuUUeu5nzpTg==
dependencies: dependencies:
prosemirror-dropcursor "1.5.0" prosemirror-dropcursor "1.5.0"
"@tiptap/extension-floating-menu@^2.0.0-beta.56": "@tiptap/extension-floating-menu@^2.0.0-beta.199":
version "2.0.0-beta.56" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.56.tgz#c7428d9109d215bdbd9033f69782c4aadb2aabec" resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.199.tgz#f0b0acb64e9b9bfe4935a0bd6b6d1863ea2d39cd"
integrity sha512-j/evHE/6UPGkIgXny9IGcAh0IrcnQmg0b2NBYebs2mqx9xYKYoe+0jVgNdLp/0M3MRgQCzyWTyatBDBFOUR2mw== integrity sha512-ELjqnNbxW66uqg54zlP2b4EVYUWvT2WvHmeOXALzoLlNzbqUopIl3XNRsvU2Dv1W88C1UjKgnRZIkHKFE1X3CA==
dependencies: dependencies:
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
prosemirror-view "1.26.2" prosemirror-view "^1.28.2"
tippy.js "^6.3.7" tippy.js "^6.3.7"
"@tiptap/extension-gapcursor@^2.0.0-beta.39": "@tiptap/extension-gapcursor@^2.0.0-beta.199":
version "2.0.0-beta.39" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.0-beta.39.tgz#b8585d2936df7ca90446758c3af90b46d552a1fb" resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.0-beta.199.tgz#ec801e30690d1b30183b9751d64615724311be97"
integrity sha512-oCyz5WEeQXrEIoa1WXaD52yf1EwMFCXaK1cVzFgUj8lkXJ+nJj+O/Zp0Mg+9/MVR0LYu/kifqVorKNXM4AFA/g== integrity sha512-0TDpDfDyay+IbD+wJMsBJ2c0Cq0NtllUOxbi0NPjjWW94Jrvs1yqUSzX4Qp9m5MW8qP24IV6krgZBM1JyQc6ng==
dependencies: dependencies:
prosemirror-gapcursor "1.3.0" prosemirror-gapcursor "^1.3.1"
"@tiptap/extension-hard-break@^2.0.0-beta.33": "@tiptap/extension-hard-break@^2.0.0-beta.199":
version "2.0.0-beta.33" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.0-beta.33.tgz#e2f355a22aaaec6e831cf2880c52aa5b0b860573" resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.0-beta.199.tgz#6ac23806c4e7e13f15e56cc2e1db7c12248e8091"
integrity sha512-41xf0vSV9hcyTFd01ItLq/CjhjgmOFLCrO3UWN/P2E/cIxuDTyXcvjTE/KXeqRCOV3OYd9fVr0wO91hc8Ij1Yg== integrity sha512-DF2wDo/+gSYRhzGowCvZJk3/j/zYJ22BHxZpkAEmLJ69mWSIqZv3S2/brujnNmnji9c3/+JN7ppPSeVykz0b9Q==
"@tiptap/extension-heading@^2.0.0-beta.29": "@tiptap/extension-heading@^2.0.0-beta.199":
version "2.0.0-beta.29" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.0.0-beta.29.tgz#d017d216c0fd1962c266f6f61a335093f9749862" resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.0.0-beta.199.tgz#b766de44fd09843364280a03d1e180db346f1ccd"
integrity sha512-q92jYcsT5bPhvuQaB0h44Z9r+Ii22tDYo082KMVnR4+tknHT/3xx+p4JC8KHjh+/5W8Quyafqy6mS8L8VX0zsQ== integrity sha512-WGQ7ET2TBpldrD8JX37OXHXq05LU3OWItIVBs9nKGh4otZTUwPtwfOyMlFfA+IMfQif+ilwLGvUC6EHOw/LwxQ==
"@tiptap/extension-history@^2.0.0-beta.26": "@tiptap/extension-history@^2.0.0-beta.199":
version "2.0.0-beta.26" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.0-beta.26.tgz#ae4c0ee8d19b3530e72d99cb5d0f69aefcf96d04" resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.0-beta.199.tgz#ff83184b6f7e0ed9a5c6362cfbc15319a451b960"
integrity sha512-ly19uwvdmXG8Fw1KcavXIHi3Qx6JBASOR7394zghOEpW3atpY8nd/8I373rZ8eDUcGOClfaF7bCx2xvIotAAnw== integrity sha512-oZMjKHFqqZuUuf0+IG5+OoKw9DIGilG+v8cm2JK9XnxF5CxF6HIXNDWl3552wRIA+Ro7fBRJEJ//hfJzp0Uhjw==
dependencies: dependencies:
prosemirror-history "1.3.0" prosemirror-history "^1.3.0"
"@tiptap/extension-horizontal-rule@^2.0.0-beta.36": "@tiptap/extension-horizontal-rule@^2.0.0-beta.199":
version "2.0.0-beta.36" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.0-beta.36.tgz#daf8e2d0f30b210a90fdb8f015646653661cfa04" resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.0-beta.199.tgz#6037b11f5a300ce0921a483d7fbbeea6a5b58f58"
integrity sha512-o+Zp7dcn3zAQhtlhZiFB69mTHuH3ZRbGEF7Cbf1D3uX1izotni5zIZbPaFFUT4r6OmVe/vDDt/nopfcGc10ktQ== integrity sha512-ISQndGiC6Y3+Ds3OJHKa2iB7s4FkRQxn8US/Hhj4yK7DOifoykLOrgDghwLu0H0dSM8KNb9caYEtmj64vDogNg==
dependencies: dependencies:
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
"@tiptap/extension-image@2.0.0-beta.30": "@tiptap/extension-image@2.0.0-beta.199":
version "2.0.0-beta.30" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.0-beta.30.tgz#60c6cfd09bfd017a3d8b1feaf0931462ffd71a60" resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.0-beta.199.tgz#48704c12e0cfc3da06a17cd44d2d8a8b9fd33de7"
integrity sha512-VhEmgiKkZMiKR7hbpJgIlIUS/QNjSGI5ER7mKDAbuV1IB5yb6nGjZ6o3Exrr2/CaTaW5hQarBC1z2Xgdu05EGg== integrity sha512-prmFRXdJucUeWVhSprrPfcn9pAtYkzm57N8WOUlVad5ocdeR9aMz9+0u0akb5v6XhaVAQgWqwqNb0Kx8lSuqhA==
"@tiptap/extension-italic@^2.0.0-beta.28": "@tiptap/extension-italic@^2.0.0-beta.199":
version "2.0.0-beta.28" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.28.tgz#bf88ecae64c8f2f69f1f508b802c1efd7454a84e" resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.199.tgz#db24dbdd0d47fdfaa22dde8ba35e2c08b7162e82"
integrity sha512-/pKRiCfewh7nqiXRD3N4hQHfGrGNOiWPFYZfY35bSpvTms7PDb/MF7xT1CWW23hSpY31BBS+R/a66vlR/gqu7Q== integrity sha512-jaYJr5ZMxU2swK6h1XJr6Wb1LlWOWbvsX/wo59iZ9KVv1AHiKZlCMcWGThy4aoAs/CUT11pB8qbzyOO163LHZg==
"@tiptap/extension-link@2.0.0-beta.43": "@tiptap/extension-link@2.0.0-beta.199":
version "2.0.0-beta.43" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.0-beta.43.tgz#c123a2170dd50d075b9fe7fb91d86d23f778ffb0" resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.0-beta.199.tgz#57865aa0211d7d9972ed5fee9a92fedbf0083b37"
integrity sha512-AYueqfTW713KGVfWSWhVbj4ObeWudgawikm3m0uYcKSdsAz/CfEvOD2/NA0uyQzlxmYLA6Pf8HMxoKGN+O4Cmg== integrity sha512-zwXDg+zsHhn2a4rJkFd/pND4zUfJ2RCgyrkBqAL+nimSiknaSsIw4NpnZTZFWze9i3NDcc2BNngDNovoEIEukg==
dependencies: dependencies:
linkifyjs "^3.0.5" linkifyjs "^3.0.5"
prosemirror-model "1.18.1" prosemirror-model "^1.18.1"
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
"@tiptap/extension-list-item@^2.0.0-beta.23": "@tiptap/extension-list-item@^2.0.0-beta.199":
version "2.0.0-beta.23" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.0-beta.23.tgz#6d1ac7235462b0bcee196f42bb1871669480b843" resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.0-beta.199.tgz#2e667f0ea5d9314307427625345e915edf91b989"
integrity sha512-AkzvdELz3ZnrlZM0r9+ritBDOnAjXHR/8zCZhW0ZlWx4zyKPMsNG5ygivY+xr4QT65NEGRT8P8b2zOhXrMjjMQ== integrity sha512-rzcz5MJgoX1M9M9e1iruyRxcwYyYmdCXsl9gB8hhJYh4R+AW1peRmHJ3vVX5oPZXg/tXOMTv/or2x8v30c9tJw==
"@tiptap/extension-mention@2.0.0-beta.102": "@tiptap/extension-mention@2.0.0-beta.199":
version "2.0.0-beta.102" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.0.0-beta.102.tgz#a80036b0a4481efc4f69b788af3f5c76428624cc" resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.0.0-beta.199.tgz#15f021411d48809ffc4c908204e6e262b0b9160b"
integrity sha512-QTBBpWnRnoV7/ZW31HwhPvZL3HiwnlehlHSLeMioVxAQPF5WrRtlOpxK/SRu7+KuwdCb7ZA1eWW/yjbXI3oktg== integrity sha512-zNCZbU03GNPaO9Aga/3AKUQBv0EridVdIFxUY3GVb5uixoxMeXDf/OI0GqnQ2KyW7ufmD5VdlZS0mP/9QCo+DA==
dependencies: dependencies:
"@tiptap/suggestion" "^2.0.0-beta.97" prosemirror-model "^1.18.1"
prosemirror-model "1.18.1" prosemirror-state "^1.4.1"
prosemirror-state "1.4.1"
"@tiptap/extension-ordered-list@^2.0.0-beta.30": "@tiptap/extension-ordered-list@^2.0.0-beta.199":
version "2.0.0-beta.30" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.0-beta.30.tgz#1f656b664302d90272c244b2e478d7056203f2a8" resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.0-beta.199.tgz#cdc6eb58e94d095013b5182683ce1e31b8733083"
integrity sha512-GRxGQdq1u0Rp5N8TjthCqoZ//460m343A0HCN7UwfQOnX7Ipv0UJemwNkSHWrl7Pexym9vy3yPWgrn7oRRmgEw== integrity sha512-ciQhBRtNUudQyCgvQKRZ1WbV7Q9IZP82GHEsk+wScZgI0SsrGY8pnfJT7CyF8aPIjkQkccozKVTbyMrjBOqWSw==
"@tiptap/extension-paragraph@^2.0.0-beta.26": "@tiptap/extension-paragraph@^2.0.0-beta.199":
version "2.0.0-beta.26" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.0-beta.26.tgz#5199c8cedb9c076347a2e15cc67442ef7c3c3fbb" resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.0-beta.199.tgz#34213e6594a1183a77bb33ced49502bafb0a3d1c"
integrity sha512-WcYsuUa7LLfk0vi7I1dVjdMRu53B52FMMqd+UL1qPdDKVkU3DBsZVwPj+yyfQyqN8Mc/xyg9VacGaiKFLmWNDg== integrity sha512-+BoMCaxlsHqw065zTUNd+ywkvFJzNKbTY461/AlKX2dgHeaO8doXHDQK+9icOpibQvrKaMhOJmuBTgGlJlUUgw==
"@tiptap/extension-placeholder@2.0.0-beta.53": "@tiptap/extension-placeholder@2.0.0-beta.199":
version "2.0.0-beta.53" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.0.0-beta.53.tgz#df29d813044da9a0e30bf8409335e77f6857c2b2" resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.0.0-beta.199.tgz#0208c42f8b92a88e66b726353d07b652f09fd823"
integrity sha512-NGU/a+GvcJVBjFqb2vI45+rNa3Cjsq/M+R/2xg9olb1w/HBr17NKf/5WSoqcc1S2cdnmMH6rB0/mVhG7Ciur+Q== integrity sha512-Tdq0r9XQ6hcu4ASvw2Xko6h8uS/xONmMmOFiTkK/54REB3RRQpkdCtXrhFn/T4DunJVBf6FUOLTjYN3SONhuew==
dependencies: dependencies:
prosemirror-model "1.18.1" prosemirror-model "^1.18.1"
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
prosemirror-view "1.26.2" prosemirror-view "^1.28.2"
"@tiptap/extension-strike@^2.0.0-beta.29": "@tiptap/extension-strike@^2.0.0-beta.199":
version "2.0.0-beta.29" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.0-beta.29.tgz#7004d0c5d126b0517fa78efc5a333a4b8e3334bf" resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.0-beta.199.tgz#5fc6e067728009d92027e58a042f18449f2fa264"
integrity sha512-zqFuY7GfNmZ/KClt6kxQ+msGo3syqucP/Xnlihxi+/h/G+oTvEwyOIXCtDOltvxcsWH/TUsdr5vzLp0j+Mdc6Q== integrity sha512-KyN5+d9o9FGvrSiSuh81oo4+XjMDsZVY4UHc9lBY0nAzaGAkJOwkCjk40RfyO5ZJ2GdEEQ6Nh/3YqVMcJTY+rA==
"@tiptap/extension-text@^2.0.0-beta.17": "@tiptap/extension-text@^2.0.0-beta.199":
version "2.0.0-beta.17" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.0-beta.17.tgz#4fdd1bdf62c82c1af6feef91c689906a8f5b171e" resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.0-beta.199.tgz#0204f3e50622e39b6fd08b5ef72fe7d0199117f5"
integrity sha512-OyKL+pqWJEtjyd9/mrsuY1kZh2b3LWpOQDWKtd4aWR4EA0efmQG+7FPwcIeAVEh7ZoqM+/ABCnPjN6IjzIrSfg== integrity sha512-ntOqEhkBjDHrdzxvpPe4U1JB5GgE9/yyWqWdgzSL9lpSndRTJN1xQLOmyuv0qsLqOgBHn1YITHvaxPb3t8FrFw==
"@tiptap/react@2.0.0-beta.114": "@tiptap/html@2.0.0-beta.199":
version "2.0.0-beta.114" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.0.0-beta.114.tgz#fa2b3fcdf379bf7ee25388c0eddbda49249977d5" resolved "https://registry.yarnpkg.com/@tiptap/html/-/html-2.0.0-beta.199.tgz#cb33319ad00b31ebf68dcc9cbb758c0aff78403c"
integrity sha512-9JbRE+16WM6RxbBxzY74SrJtLodvjeRBnEbWxuhxVgGKxMunRj6r8oED87ODJgqLmkpofwE0KFHTPGdEXfdcKA== integrity sha512-/Ow/LOCXZrNXR6cYJMN8O4ZMm7Jl/t6L0K6MCJ1p+pKu11xtf1mI73t8WYFA5J8t7cL7eWUgDPtqlOE+DJTvHg==
dependencies: dependencies:
"@tiptap/extension-bubble-menu" "^2.0.0-beta.61" "@tiptap/core" "^2.0.0-beta.199"
"@tiptap/extension-floating-menu" "^2.0.0-beta.56" prosemirror-model "^1.18.1"
prosemirror-view "1.26.2" zeed-dom "^0.9.19"
"@tiptap/starter-kit@2.0.0-beta.191": "@tiptap/react@2.0.0-beta.199":
version "2.0.0-beta.191" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.0.0-beta.191.tgz#3f549367f6dbb8cf83f63aa0941722d91d0fd8e7" resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.0.0-beta.199.tgz#98f9fb2134fdc385648ed06ea1ec78902c0e99cf"
integrity sha512-YRrBCi9W4jiH/xLTJJOCdD7pL4Wb98Ip8qCJ94RElShDj0O1i5tT9wWlgVWoGIU+CRAds5XENRwZ97sJ+YfYyg== integrity sha512-AjBtoavcJ7WOoEXdJlrVEdEv6xuI5UFnqB88w8NlORSkWbfQ3uuOm3A0LUZ92/SsBz6NISZbsFahMy0DYgGbIA==
dependencies: dependencies:
"@tiptap/core" "^2.0.0-beta.182" "@tiptap/extension-bubble-menu" "^2.0.0-beta.199"
"@tiptap/extension-blockquote" "^2.0.0-beta.29" "@tiptap/extension-floating-menu" "^2.0.0-beta.199"
"@tiptap/extension-bold" "^2.0.0-beta.28" prosemirror-view "^1.28.2"
"@tiptap/extension-bullet-list" "^2.0.0-beta.29"
"@tiptap/extension-code" "^2.0.0-beta.28"
"@tiptap/extension-code-block" "^2.0.0-beta.42"
"@tiptap/extension-document" "^2.0.0-beta.17"
"@tiptap/extension-dropcursor" "^2.0.0-beta.29"
"@tiptap/extension-gapcursor" "^2.0.0-beta.39"
"@tiptap/extension-hard-break" "^2.0.0-beta.33"
"@tiptap/extension-heading" "^2.0.0-beta.29"
"@tiptap/extension-history" "^2.0.0-beta.26"
"@tiptap/extension-horizontal-rule" "^2.0.0-beta.36"
"@tiptap/extension-italic" "^2.0.0-beta.28"
"@tiptap/extension-list-item" "^2.0.0-beta.23"
"@tiptap/extension-ordered-list" "^2.0.0-beta.30"
"@tiptap/extension-paragraph" "^2.0.0-beta.26"
"@tiptap/extension-strike" "^2.0.0-beta.29"
"@tiptap/extension-text" "^2.0.0-beta.17"
"@tiptap/suggestion@^2.0.0-beta.97": "@tiptap/starter-kit@2.0.0-beta.199":
version "2.0.0-beta.97" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.0-beta.97.tgz#2e3dc20deebc2c37c5d39c848e61e9c837e7188a" resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.0.0-beta.199.tgz#b0d48784abe711afc2973592467f213f86794c82"
integrity sha512-3NWG+HE7v2w97Ek6z1tUosoZKpCDH+oAtIG9XoNkK1PmlaVV/H4d6HT9uPX+Y6SeN7fSAqlcXFUGLXcDi9d+Zw== integrity sha512-ToPhccdSAixNhAujBa5VEOg+G6Nkc+JJGaUVEEGH2EV2ICzbhFhcdbAHqI6lNtCFleaX9NULJBGHXEwLR5T83Q==
dependencies: dependencies:
prosemirror-model "1.18.1" "@tiptap/core" "^2.0.0-beta.199"
prosemirror-state "1.4.1" "@tiptap/extension-blockquote" "^2.0.0-beta.199"
prosemirror-view "1.26.2" "@tiptap/extension-bold" "^2.0.0-beta.199"
"@tiptap/extension-bullet-list" "^2.0.0-beta.199"
"@tiptap/extension-code" "^2.0.0-beta.199"
"@tiptap/extension-code-block" "^2.0.0-beta.199"
"@tiptap/extension-document" "^2.0.0-beta.199"
"@tiptap/extension-dropcursor" "^2.0.0-beta.199"
"@tiptap/extension-gapcursor" "^2.0.0-beta.199"
"@tiptap/extension-hard-break" "^2.0.0-beta.199"
"@tiptap/extension-heading" "^2.0.0-beta.199"
"@tiptap/extension-history" "^2.0.0-beta.199"
"@tiptap/extension-horizontal-rule" "^2.0.0-beta.199"
"@tiptap/extension-italic" "^2.0.0-beta.199"
"@tiptap/extension-list-item" "^2.0.0-beta.199"
"@tiptap/extension-ordered-list" "^2.0.0-beta.199"
"@tiptap/extension-paragraph" "^2.0.0-beta.199"
"@tiptap/extension-strike" "^2.0.0-beta.199"
"@tiptap/extension-text" "^2.0.0-beta.199"
"@tiptap/suggestion@2.0.0-beta.199":
version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.0-beta.199.tgz#8bf8029a25826bc41266bb9fffe8b33195bf9a40"
integrity sha512-FwkaMW0fX1Xlsv4n5GPlVkHwwMSMvIXgZ6LZnwy4Mxo+Njz7YgHw8X8YQo927lMbYsVHYhNcsk726HaxudDMEw==
dependencies:
prosemirror-model "^1.18.1"
prosemirror-state "^1.4.1"
prosemirror-view "^1.28.2"
"@tootallnate/once@2": "@tootallnate/once@2":
version "2.0.0" version "2.0.0"
@ -3483,6 +3498,11 @@
"@types/node" "*" "@types/node" "*"
form-data "^2.5.0" form-data "^2.5.0"
"@types/marked@4.0.7":
version "4.0.7"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.7.tgz#400a76809fd08c2bbd9e25f3be06ea38c8e0a1d3"
integrity sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw==
"@types/mdast@^3.0.0": "@types/mdast@^3.0.0":
version "3.0.10" version "3.0.10"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
@ -4961,9 +4981,9 @@ commander@^8.0.0, commander@^8.3.0:
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
commander@^9.4.0: commander@^9.4.0:
version "9.4.1" version "9.4.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.1.tgz#d1dd8f2ce6faf93147295c0df13c7c21141cfbdd" resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.0.tgz#bc4a40918fefe52e22450c111ecd6b7acce6f11c"
integrity sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw== integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==
commondir@^1.0.1: commondir@^1.0.1:
version "1.0.1" version "1.0.1"
@ -5462,11 +5482,6 @@ d3-transition@3:
d3-interpolate "1 - 3" d3-interpolate "1 - 3"
d3-timer "1 - 3" d3-timer "1 - 3"
daisyui@1.16.4:
version "1.16.4"
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-1.16.4.tgz#52773401c0962e37ef40507d29f0e513c7f2856f"
integrity sha512-bpPUlIR6PJdnaM+Vj+Rd0ljMwbdwjvFAW9E/bhxCRDEU48OnmpKQxnkwNGAtXbwWWATS6I1dEIlLdM8zCbV3uQ==
damerau-levenshtein@^1.0.7: damerau-levenshtein@^1.0.7:
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@ -5844,9 +5859,9 @@ dot-prop@^5.2.0:
is-obj "^2.0.0" is-obj "^2.0.0"
dotenv@^16.0.2: dotenv@^16.0.2:
version "16.0.3" version "16.0.2"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.2.tgz#0b0f8652c016a3858ef795024508cddc4bffc5bf"
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== integrity sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==
duplexer3@^0.1.4: duplexer3@^0.1.4:
version "0.1.4" version "0.1.4"
@ -7079,9 +7094,9 @@ google-p12-pem@^3.1.3:
node-forge "^1.3.1" node-forge "^1.3.1"
google-protobuf@^3.21.0: google-protobuf@^3.21.0:
version "3.21.2" version "3.21.0"
resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.2.tgz#4580a2bea8bbb291ee579d1fefb14d6fa3070ea4" resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.0.tgz#8dfa3fca16218618d373d414d3c1139e28034d6e"
integrity sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA== integrity sha512-byR7MBTK4tZ5PZEb+u5ZTzpt4SfrTxv5682MjPlHN16XeqgZE2/8HOIWeiXe8JKnT9OVbtBGhbq8mtvkK8cd5g==
got@^9.6.0: got@^9.6.0:
version "9.6.0" version "9.6.0"
@ -8581,6 +8596,11 @@ markdown-escapes@^1.0.0:
resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
marked@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/marked/-/marked-4.1.1.tgz#2f709a4462abf65a283f2453dc1c42ab177d302e"
integrity sha512-0cNMnTcUJPxbA6uWmCmjWz4NJRe/0Xfk2NhXCUHjew9qJzFN20krFnsUe7QynwqOwa5m1fZ4UDg0ycKFVC0ccw==
match-sorter@^6.0.2: match-sorter@^6.0.2:
version "6.3.1" version "6.3.1"
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda" resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda"
@ -9893,10 +9913,10 @@ property-information@^5.0.0, property-information@^5.3.0:
dependencies: dependencies:
xtend "^4.0.0" xtend "^4.0.0"
prosemirror-commands@1.3.0: prosemirror-commands@^1.3.1:
version "1.3.0" version "1.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz#361b2e2b2a347ce7453386459f97c3f549a1113b" resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.3.1.tgz#926c88801eebaa50363d4658850b41406d375a31"
integrity sha512-BwBbZ5OAScPcm0x7H8SPbqjuEJnCU2RJT9LDyOiiIl/3NbL1nJZI4SFNHwU2e/tRr2Xe7JsptpzseqvZvToLBQ== integrity sha512-XTporPgoECkOQACVw0JTe3RZGi+fls3/byqt+tXwGTkD7qLuB4KdVrJamDMJf4kfKga3uB8hZ+kUUyZ5oWpnfg==
dependencies: dependencies:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
@ -9911,17 +9931,17 @@ prosemirror-dropcursor@1.5.0:
prosemirror-transform "^1.1.0" prosemirror-transform "^1.1.0"
prosemirror-view "^1.1.0" prosemirror-view "^1.1.0"
prosemirror-gapcursor@1.3.0: prosemirror-gapcursor@^1.3.1:
version "1.3.0" version "1.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.0.tgz#e07c22ad959b86ec0c4cfc590cc5f484dd984d56" resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.1.tgz#8cfd874592e4504d63720e14ed680c7866e64554"
integrity sha512-9Tdx83xB2W4Oqchm12FtCkSizbqvi64cjs1I9TRPblqdA5TUWoVZ4ZI+t71Jh6HSEh4cDMPzx3UwfryJtKlb/w== integrity sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA==
dependencies: dependencies:
prosemirror-keymap "^1.0.0" prosemirror-keymap "^1.0.0"
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
prosemirror-view "^1.0.0" prosemirror-view "^1.0.0"
prosemirror-history@1.3.0: prosemirror-history@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.0.tgz#bf5a1ff7759aca759ddf0c722c2fa5b14fb0ddc1" resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.0.tgz#bf5a1ff7759aca759ddf0c722c2fa5b14fb0ddc1"
integrity sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA== integrity sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA==
@ -9930,7 +9950,7 @@ prosemirror-history@1.3.0:
prosemirror-transform "^1.0.0" prosemirror-transform "^1.0.0"
rope-sequence "^1.3.0" rope-sequence "^1.3.0"
prosemirror-keymap@1.2.0, prosemirror-keymap@^1.0.0: prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz#d5cc9da9b712020690a994b50b92a0e448a60bf5" resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz#d5cc9da9b712020690a994b50b92a0e448a60bf5"
integrity sha512-TdSfu+YyLDd54ufN/ZeD1VtBRYpgZnTPnnbY+4R08DDgs84KrIPEPbJL8t1Lm2dkljFx6xeBE26YWH3aIzkPKg== integrity sha512-TdSfu+YyLDd54ufN/ZeD1VtBRYpgZnTPnnbY+4R08DDgs84KrIPEPbJL8t1Lm2dkljFx6xeBE26YWH3aIzkPKg==
@ -9938,23 +9958,23 @@ prosemirror-keymap@1.2.0, prosemirror-keymap@^1.0.0:
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
w3c-keyname "^2.2.0" w3c-keyname "^2.2.0"
prosemirror-model@1.18.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0: prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1:
version "1.18.1" version "1.18.1"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd"
integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw== integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==
dependencies: dependencies:
orderedmap "^2.0.0" orderedmap "^2.0.0"
prosemirror-schema-list@1.2.0: prosemirror-schema-list@^1.2.2:
version "1.2.0" version "1.2.2"
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.2.0.tgz#1932268593a7396c0ac168cbe31f28187406ce24" resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.2.2.tgz#bafda37b72367d39accdcaf6ddf8fb654a16e8e5"
integrity sha512-8PT/9xOx1HHdC7fDNNfhQ50Z8Mzu7nKyA1KCDltSpcZVZIbB0k7KtsHrnXyuIhbLlScoymBiLZ00c5MH6wdFsA== integrity sha512-rd0pqSDp86p0MUMKG903g3I9VmElFkQpkZ2iOd3EOVg1vo5Cst51rAsoE+5IPy0LPXq64eGcCYlW1+JPNxOj2w==
dependencies: dependencies:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0" prosemirror-transform "^1.0.0"
prosemirror-state@1.4.1, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2: prosemirror-state@1.4.1, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.1.tgz#f6e26c7b6a7e11206176689eb6ebbf91870953e1" resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.1.tgz#f6e26c7b6a7e11206176689eb6ebbf91870953e1"
integrity sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg== integrity sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg==
@ -9962,14 +9982,21 @@ prosemirror-state@1.4.1, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0" prosemirror-transform "^1.0.0"
prosemirror-transform@1.6.0, prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0: prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.6.0.tgz#8162dbfaf124f9253a7ab28605a9460411a96a53" resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.6.0.tgz#8162dbfaf124f9253a7ab28605a9460411a96a53"
integrity sha512-MAp7AjsjEGEqQY0sSMufNIUuEyB1ZR9Fqlm8dTwwWwpEJRv/plsKjWXBbx52q3Ml8MtaMcd7ic14zAHVB3WaMw== integrity sha512-MAp7AjsjEGEqQY0sSMufNIUuEyB1ZR9Fqlm8dTwwWwpEJRv/plsKjWXBbx52q3Ml8MtaMcd7ic14zAHVB3WaMw==
dependencies: dependencies:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-view@1.26.2, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0: prosemirror-transform@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.7.0.tgz#a8a0768f3ee6418d26ebef435beda9d43c65e472"
integrity sha512-O4T697Cqilw06Zvc3Wm+e237R6eZtJL/xGMliCi+Uo8VL6qHk6afz1qq0zNjT3eZMuYwnP8ZS0+YxX/tfcE9TQ==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0:
version "1.26.2" version "1.26.2"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.26.2.tgz#e673894ecf26aea330b727622d561c51b41d31eb" resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.26.2.tgz#e673894ecf26aea330b727622d561c51b41d31eb"
integrity sha512-CGKw+GadkfSBEwRAJTHCEKJ4DlV6/3IhAdjpwGyZHUHtbP7jX4Ol4zmi7xa2c6GOabDlIJLYXJydoNYLX7lNeQ== integrity sha512-CGKw+GadkfSBEwRAJTHCEKJ4DlV6/3IhAdjpwGyZHUHtbP7jX4Ol4zmi7xa2c6GOabDlIJLYXJydoNYLX7lNeQ==
@ -9978,6 +10005,15 @@ prosemirror-view@1.26.2, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0:
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0" prosemirror-transform "^1.1.0"
prosemirror-view@^1.28.2:
version "1.28.3"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.28.3.tgz#51131ede4d3852380be9a5e0e4ba7662725dac1f"
integrity sha512-YnJxLRzIaCNEt3VKiy+PBxtpwsCbjrfiBKIgHJeqbKhdeP8bU2qL4ngdGmxp9K4+06cZG5bE9vipuhP+KUl+BQ==
dependencies:
prosemirror-model "^1.16.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
proto3-json-serializer@^0.1.8: proto3-json-serializer@^0.1.8:
version "0.1.9" version "0.1.9"
resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-0.1.9.tgz#705ddb41b009dd3e6fcd8123edd72926abf65a34" resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-0.1.9.tgz#705ddb41b009dd3e6fcd8123edd72926abf65a34"
@ -10182,6 +10218,25 @@ raw-body@2.5.1, raw-body@^2.2.0:
iconv-lite "0.4.24" iconv-lite "0.4.24"
unpipe "1.0.0" unpipe "1.0.0"
rc-slider@10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-10.0.1.tgz#7058c68ff1e1aa4e7c3536e5e10128bdbccb87f9"
integrity sha512-igTKF3zBet7oS/3yNiIlmU8KnZ45npmrmHlUUio8PNbIhzMcsh+oE/r2UD42Y6YD2D/s+kzCQkzQrPD6RY435Q==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.5"
rc-util "^5.18.1"
shallowequal "^1.1.0"
rc-util@^5.18.1:
version "5.24.4"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.24.4.tgz#a4126f01358c86f17c1bf380a1d83d6c9155ae65"
integrity sha512-2a4RQnycV9eV7lVZPEJ7QwJRPlZNc06J7CwcwZo4vIHr3PfUqtYgl1EkUV9ETAc6VRRi8XZOMFhYG63whlIC9Q==
dependencies:
"@babel/runtime" "^7.18.3"
react-is "^16.12.0"
shallowequal "^1.1.0"
rc@^1.2.8: rc@^1.2.8:
version "1.2.8" version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@ -10312,7 +10367,7 @@ react-instantsearch-hooks@6.24.1:
dequal "^2.0.0" dequal "^2.0.0"
instantsearch.js "^4.40.1" instantsearch.js "^4.40.1"
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -11316,10 +11371,10 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
stability-client@1.5.0: stability-client@1.6.1:
version "1.5.0" version "1.6.1"
resolved "https://registry.yarnpkg.com/stability-client/-/stability-client-1.5.0.tgz#f221420a297c808f209c469a0df8fa2401f8f6ae" resolved "https://registry.yarnpkg.com/stability-client/-/stability-client-1.6.1.tgz#960250c8083119227d25972db662c873333f9a26"
integrity sha512-hXuDK6QW/msf50pu8M4L1hTCYG4w5ZrhLgxirigUrSZEARupIPT88O7qz4YSmUbbp9nKlzS8UJ6NjYUH7qy45w== integrity sha512-N5igY8YH1vT6T0p++GWbr1KHqeAyYO+/WGgrgsTeKk7bCTXLeQng8B9OWYOOAczb95iyvFXDdUCRj5Cry00iaA==
dependencies: dependencies:
"@improbable-eng/grpc-web" "^0.15.0" "@improbable-eng/grpc-web" "^0.15.0"
"@improbable-eng/grpc-web-node-http-transport" "^0.15.0" "@improbable-eng/grpc-web-node-http-transport" "^0.15.0"
@ -12627,6 +12682,13 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zeed-dom@^0.9.19:
version "0.9.26"
resolved "https://registry.yarnpkg.com/zeed-dom/-/zeed-dom-0.9.26.tgz#f0127d1024b34a1233a321bd6d0275b3ba998b30"
integrity sha512-HWjX8rA3Y/RI32zby3KIN1D+mgskce+She4K7kRyyx62OiVxJ5FnYm8vWq0YVAja3Tf2S1M0XAc6O2lRFcMgcQ==
dependencies:
css-what "^6.1.0"
zod@3.17.2: zod@3.17.2:
version "3.17.2" version "3.17.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.2.tgz#d20b32146a3b5068f8f71768b4f9a4bfe52cddb0" resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.2.tgz#d20b32146a3b5068f8f71768b4f9a4bfe52cddb0"