Merge branch 'main' into austin/dc-hackathon
This commit is contained in:
commit
86b489bd26
|
@ -210,7 +210,6 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
}
|
||||
}
|
||||
|
||||
const netPayout = payout - loan
|
||||
const profit = payout + saleValue + redeemed - totalInvested
|
||||
const profitPercent = (profit / totalInvested) * 100
|
||||
|
||||
|
@ -221,8 +220,8 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
|
||||
return {
|
||||
invested,
|
||||
loan,
|
||||
payout,
|
||||
netPayout,
|
||||
profit,
|
||||
profitPercent,
|
||||
totalShares,
|
||||
|
@ -233,8 +232,8 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
export function getContractBetNullMetrics() {
|
||||
return {
|
||||
invested: 0,
|
||||
loan: 0,
|
||||
payout: 0,
|
||||
netPayout: 0,
|
||||
profit: 0,
|
||||
profitPercent: 0,
|
||||
totalShares: {} as { [outcome: string]: number },
|
||||
|
|
|
@ -576,7 +576,7 @@ Work towards sustainable, systemic change.`,
|
|||
|
||||
If you would like to support our work, you can do so by getting involved or by donating.`,
|
||||
},
|
||||
{
|
||||
{
|
||||
name: 'CaRLA',
|
||||
website: 'https://carlaef.org/',
|
||||
photo: 'https://i.imgur.com/IsNVTOY.png',
|
||||
|
@ -589,6 +589,14 @@ CaRLA uses legal advocacy and education to ensure all cities comply with their o
|
|||
|
||||
In addition to housing impact litigation, we provide free legal aid, education and workshops, counseling and advocacy to advocates, homeowners, small developers, and city and state government officials.`,
|
||||
},
|
||||
{
|
||||
name: 'Mriya',
|
||||
website: 'https://mriya-ua.org/',
|
||||
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',
|
||||
preview: 'Donate supplies to soldiers in Ukraine',
|
||||
description: 'Donate supplies to soldiers in Ukraine, including tourniquets and plate carriers.',
|
||||
},
|
||||
].map((charity) => {
|
||||
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
|
||||
return {
|
||||
|
|
|
@ -30,7 +30,7 @@ export function contractTextDetails(contract: Contract) {
|
|||
const { closeTime, groupLinks } = contract
|
||||
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
|
||||
|
||||
const groupHashtags = groupLinks?.slice(0, 5).map((g) => `#${g.name}`)
|
||||
const groupHashtags = groupLinks?.map((g) => `#${g.name.replace(/ /g, '')}`)
|
||||
|
||||
return (
|
||||
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
|
||||
|
|
|
@ -3,6 +3,7 @@ import { JSONContent } from '@tiptap/core'
|
|||
export type Post = {
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
content: JSONContent
|
||||
creatorId: string // User id
|
||||
createdTime: number
|
||||
|
@ -17,3 +18,4 @@ export type DateDoc = Post & {
|
|||
}
|
||||
|
||||
export const MAX_POST_TITLE_LENGTH = 480
|
||||
export const MAX_POST_SUBTITLE_LENGTH = 480
|
||||
|
|
24
common/util/color.ts
Normal file
24
common/util/color.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
export const interpolateColor = (color1: string, color2: string, p: number) => {
|
||||
const rgb1 = parseInt(color1.replace('#', ''), 16)
|
||||
const rgb2 = parseInt(color2.replace('#', ''), 16)
|
||||
|
||||
const [r1, g1, b1] = toArray(rgb1)
|
||||
const [r2, g2, b2] = toArray(rgb2)
|
||||
|
||||
const q = 1 - p
|
||||
const rr = Math.round(r1 * q + r2 * p)
|
||||
const rg = Math.round(g1 * q + g2 * p)
|
||||
const rb = Math.round(b1 * q + b2 * p)
|
||||
|
||||
const hexString = Number((rr << 16) + (rg << 8) + rb).toString(16)
|
||||
const hex = `#${'0'.repeat(6 - hexString.length)}${hexString}`
|
||||
return hex
|
||||
}
|
||||
|
||||
function toArray(rgb: number) {
|
||||
const r = rgb >> 16
|
||||
const g = (rgb >> 8) % 256
|
||||
const b = rgb % 256
|
||||
|
||||
return [r, g, b]
|
||||
}
|
|
@ -86,6 +86,15 @@ export function richTextToString(text?: JSONContent) {
|
|||
dfs(newText, (current) => {
|
||||
if (current.marks?.some((m) => m.type === TiptapSpoiler.name)) {
|
||||
current.text = '[spoiler]'
|
||||
} else if (current.type === 'image') {
|
||||
current.text = '[Image]'
|
||||
// This is a hack, I've no idea how to change a tiptap extenstion's schema
|
||||
current.type = 'text'
|
||||
} else if (current.type === 'iframe') {
|
||||
const src = current.attrs?.['src'] ? current.attrs['src'] : ''
|
||||
current.text = '[Iframe]' + (src ? ` url:${src}` : '')
|
||||
// This is a hack, I've no idea how to change a tiptap extenstion's schema
|
||||
current.type = 'text'
|
||||
}
|
||||
})
|
||||
return generateText(newText, exhibitExts)
|
||||
|
|
|
@ -3,7 +3,11 @@ import * as admin from 'firebase-admin'
|
|||
import { getUser } from './utils'
|
||||
import { slugify } from '../../common/util/slugify'
|
||||
import { randomString } from '../../common/util/random'
|
||||
import { Post, MAX_POST_TITLE_LENGTH } from '../../common/post'
|
||||
import {
|
||||
Post,
|
||||
MAX_POST_TITLE_LENGTH,
|
||||
MAX_POST_SUBTITLE_LENGTH,
|
||||
} from '../../common/post'
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
import { z } from 'zod'
|
||||
|
@ -36,6 +40,7 @@ const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
|
|||
|
||||
const postSchema = z.object({
|
||||
title: z.string().min(1).max(MAX_POST_TITLE_LENGTH),
|
||||
subtitle: z.string().min(1).max(MAX_POST_SUBTITLE_LENGTH),
|
||||
content: contentSchema,
|
||||
groupId: z.string().optional(),
|
||||
|
||||
|
@ -48,10 +53,8 @@ const postSchema = z.object({
|
|||
|
||||
export const createpost = newEndpoint({}, async (req, auth) => {
|
||||
const firestore = admin.firestore()
|
||||
const { title, content, groupId, question, ...otherProps } = validate(
|
||||
postSchema,
|
||||
req.body
|
||||
)
|
||||
const { title, subtitle, content, groupId, question, ...otherProps } =
|
||||
validate(postSchema, req.body)
|
||||
|
||||
const creator = await getUser(auth.uid)
|
||||
if (!creator)
|
||||
|
@ -89,6 +92,7 @@ export const createpost = newEndpoint({}, async (req, auth) => {
|
|||
creatorId: creator.id,
|
||||
slug,
|
||||
title,
|
||||
subtitle,
|
||||
createdTime: Date.now(),
|
||||
content: content,
|
||||
contractSlug,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { APIError, newEndpoint } from './api'
|
||||
import { sendPortfolioUpdateEmailsToAllUsers } from './weekly-portfolio-emails'
|
||||
import { isProd } from './utils'
|
||||
import { sendTrendingMarketsEmailsToAllUsers } from 'functions/src/weekly-markets-emails'
|
||||
|
||||
// Function for testing scheduled functions locally
|
||||
export const testscheduledfunction = newEndpoint(
|
||||
|
@ -10,7 +10,7 @@ export const testscheduledfunction = newEndpoint(
|
|||
throw new APIError(400, 'This function is only available in dev mode')
|
||||
|
||||
// Replace your function here
|
||||
await sendPortfolioUpdateEmailsToAllUsers()
|
||||
await sendTrendingMarketsEmailsToAllUsers()
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
|
|
@ -34,17 +34,20 @@ export const unsubscribe: EndpointDefinition = {
|
|||
const previousDestinations =
|
||||
user.notificationPreferences[notificationSubscriptionType]
|
||||
|
||||
let newDestinations = previousDestinations
|
||||
if (wantsToOptOutAll) newDestinations.push('email')
|
||||
else
|
||||
newDestinations = previousDestinations.filter(
|
||||
(destination) => destination !== 'email'
|
||||
)
|
||||
|
||||
console.log(previousDestinations)
|
||||
const { email } = user
|
||||
|
||||
const update: Partial<PrivateUser> = {
|
||||
notificationPreferences: {
|
||||
...user.notificationPreferences,
|
||||
[notificationSubscriptionType]: wantsToOptOutAll
|
||||
? previousDestinations.push('email')
|
||||
: previousDestinations.filter(
|
||||
(destination) => destination !== 'email'
|
||||
),
|
||||
[notificationSubscriptionType]: newDestinations,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -60,7 +63,7 @@ export const unsubscribe: EndpointDefinition = {
|
|||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>Manifold Markets 7th Day Anniversary Gift!</title>
|
||||
<title>Unsubscribe from Manifold Markets emails</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
|
@ -213,7 +216,7 @@ export const unsubscribe: EndpointDefinition = {
|
|||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>Manifold Markets 7th Day Anniversary Gift!</title>
|
||||
<title>Unsubscribe from Manifold Markets emails</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
|
|
|
@ -4,21 +4,24 @@ import * as admin from 'firebase-admin'
|
|||
import { Contract } from '../../common/contract'
|
||||
import {
|
||||
getAllPrivateUsers,
|
||||
getGroup,
|
||||
getPrivateUser,
|
||||
getUser,
|
||||
getValues,
|
||||
isProd,
|
||||
log,
|
||||
} from './utils'
|
||||
import { sendInterestingMarketsEmail } from './emails'
|
||||
import { createRNG, shuffle } from '../../common/util/random'
|
||||
import { DAY_MS } from '../../common/util/time'
|
||||
import { DAY_MS, HOUR_MS } from '../../common/util/time'
|
||||
import { filterDefined } from '../../common/util/array'
|
||||
import { Follow } from '../../common/follow'
|
||||
import { countBy, uniq, uniqBy } from 'lodash'
|
||||
import { sendInterestingMarketsEmail } from './emails'
|
||||
|
||||
export const weeklyMarketsEmails = functions
|
||||
.runWith({ secrets: ['MAILGUN_KEY'], memory: '4GB' })
|
||||
// every minute on Monday for an hour at 12pm PT (UTC -07:00)
|
||||
.pubsub.schedule('* 19 * * 1')
|
||||
// every minute on Monday for 2 hours starting at 12pm PT (UTC -07:00)
|
||||
.pubsub.schedule('* 19-20 * * 1')
|
||||
.timeZone('Etc/UTC')
|
||||
.onRun(async () => {
|
||||
await sendTrendingMarketsEmailsToAllUsers()
|
||||
|
@ -40,20 +43,30 @@ export async function getTrendingContracts() {
|
|||
)
|
||||
}
|
||||
|
||||
async function sendTrendingMarketsEmailsToAllUsers() {
|
||||
export async function sendTrendingMarketsEmailsToAllUsers() {
|
||||
const numContractsToSend = 6
|
||||
const privateUsers = isProd()
|
||||
? await getAllPrivateUsers()
|
||||
: filterDefined([await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2')])
|
||||
// get all users that haven't unsubscribed from weekly emails
|
||||
: filterDefined([
|
||||
await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2'), // dev Ian
|
||||
])
|
||||
const privateUsersToSendEmailsTo = privateUsers
|
||||
.filter((user) => {
|
||||
return (
|
||||
// Get all users that haven't unsubscribed from weekly emails
|
||||
.filter(
|
||||
(user) =>
|
||||
user.notificationPreferences.trending_markets.includes('email') &&
|
||||
!user.weeklyTrendingEmailSent
|
||||
)
|
||||
})
|
||||
.slice(150) // Send the emails out in batches
|
||||
)
|
||||
.slice(0, 90) // Send the emails out in batches
|
||||
|
||||
// For testing different users on prod: (only send ian an email though)
|
||||
// const privateUsersToSendEmailsTo = filterDefined([
|
||||
// await getPrivateUser('AJwLWoo3xue32XIiAVrL5SyR1WB2'), // prod Ian
|
||||
// // isProd()
|
||||
// await getPrivateUser('FptiiMZZ6dQivihLI8MYFQ6ypSw1'), // prod Mik
|
||||
// // : await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2'), // dev Ian
|
||||
// ])
|
||||
|
||||
log(
|
||||
'Sending weekly trending emails to',
|
||||
privateUsersToSendEmailsTo.length,
|
||||
|
@ -70,42 +83,358 @@ async function sendTrendingMarketsEmailsToAllUsers() {
|
|||
!contract.groupSlugs?.includes('manifold-features') &&
|
||||
!contract.groupSlugs?.includes('manifold-6748e065087e')
|
||||
)
|
||||
.slice(0, 20)
|
||||
log(
|
||||
`Found ${trendingContracts.length} trending contracts:\n`,
|
||||
trendingContracts.map((c) => c.question).join('\n ')
|
||||
)
|
||||
.slice(0, 50)
|
||||
|
||||
// TODO: convert to Promise.all
|
||||
for (const privateUser of privateUsersToSendEmailsTo) {
|
||||
if (!privateUser.email) {
|
||||
log(`No email for ${privateUser.username}`)
|
||||
continue
|
||||
}
|
||||
const contractsAvailableToSend = trendingContracts.filter((contract) => {
|
||||
return !contract.uniqueBettorIds?.includes(privateUser.id)
|
||||
})
|
||||
if (contractsAvailableToSend.length < numContractsToSend) {
|
||||
log('not enough new, unbet-on contracts to send to user', privateUser.id)
|
||||
await firestore.collection('private-users').doc(privateUser.id).update({
|
||||
const uniqueTrendingContracts = removeSimilarQuestions(
|
||||
trendingContracts,
|
||||
trendingContracts,
|
||||
true
|
||||
).slice(0, 20)
|
||||
|
||||
await Promise.all(
|
||||
privateUsersToSendEmailsTo.map(async (privateUser) => {
|
||||
if (!privateUser.email) {
|
||||
log(`No email for ${privateUser.username}`)
|
||||
return
|
||||
}
|
||||
|
||||
const unbetOnFollowedMarkets = await getUserUnBetOnFollowsMarkets(
|
||||
privateUser.id
|
||||
)
|
||||
const unBetOnGroupMarkets = await getUserUnBetOnGroupsMarkets(
|
||||
privateUser.id,
|
||||
unbetOnFollowedMarkets
|
||||
)
|
||||
const similarBettorsMarkets = await getSimilarBettorsMarkets(
|
||||
privateUser.id,
|
||||
unBetOnGroupMarkets
|
||||
)
|
||||
|
||||
const marketsAvailableToSend = uniqBy(
|
||||
[
|
||||
...chooseRandomSubset(unbetOnFollowedMarkets, 2),
|
||||
// // Most people will belong to groups but may not follow other users,
|
||||
// so choose more from the other subsets if the followed markets is sparse
|
||||
...chooseRandomSubset(
|
||||
unBetOnGroupMarkets,
|
||||
unbetOnFollowedMarkets.length < 2 ? 3 : 2
|
||||
),
|
||||
...chooseRandomSubset(
|
||||
similarBettorsMarkets,
|
||||
unbetOnFollowedMarkets.length < 2 ? 3 : 2
|
||||
),
|
||||
],
|
||||
(contract) => contract.id
|
||||
)
|
||||
// // at least send them trending contracts if nothing else
|
||||
if (marketsAvailableToSend.length < numContractsToSend) {
|
||||
const trendingMarketsToSend =
|
||||
numContractsToSend - marketsAvailableToSend.length
|
||||
log(
|
||||
`not enough personalized markets, sending ${trendingMarketsToSend} trending`
|
||||
)
|
||||
marketsAvailableToSend.push(
|
||||
...removeSimilarQuestions(
|
||||
uniqueTrendingContracts,
|
||||
marketsAvailableToSend,
|
||||
false
|
||||
)
|
||||
.filter(
|
||||
(contract) => !contract.uniqueBettorIds?.includes(privateUser.id)
|
||||
)
|
||||
.slice(0, trendingMarketsToSend)
|
||||
)
|
||||
}
|
||||
|
||||
if (marketsAvailableToSend.length < numContractsToSend) {
|
||||
log(
|
||||
'not enough new, unbet-on contracts to send to user',
|
||||
privateUser.id
|
||||
)
|
||||
await firestore.collection('private-users').doc(privateUser.id).update({
|
||||
weeklyTrendingEmailSent: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
// choose random subset of contracts to send to user
|
||||
const contractsToSend = chooseRandomSubset(
|
||||
marketsAvailableToSend,
|
||||
numContractsToSend
|
||||
)
|
||||
|
||||
const user = await getUser(privateUser.id)
|
||||
if (!user) return
|
||||
|
||||
log(
|
||||
'sending contracts:',
|
||||
contractsToSend.map((c) => c.question + ' ' + c.popularityScore)
|
||||
)
|
||||
// if they don't have enough markets, find user bets and get the other bettor ids who most overlap on those markets, then do the same thing as above for them
|
||||
await sendInterestingMarketsEmail(user, privateUser, contractsToSend)
|
||||
await firestore.collection('private-users').doc(user.id).update({
|
||||
weeklyTrendingEmailSent: true,
|
||||
})
|
||||
continue
|
||||
}
|
||||
// choose random subset of contracts to send to user
|
||||
const contractsToSend = chooseRandomSubset(
|
||||
contractsAvailableToSend,
|
||||
numContractsToSend
|
||||
)
|
||||
|
||||
const user = await getUser(privateUser.id)
|
||||
if (!user) continue
|
||||
|
||||
await sendInterestingMarketsEmail(user, privateUser, contractsToSend)
|
||||
await firestore.collection('private-users').doc(user.id).update({
|
||||
weeklyTrendingEmailSent: true,
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const MINIMUM_POPULARITY_SCORE = 10
|
||||
|
||||
const getUserUnBetOnFollowsMarkets = async (userId: string) => {
|
||||
const follows = await getValues<Follow>(
|
||||
firestore.collection('users').doc(userId).collection('follows')
|
||||
)
|
||||
|
||||
const unBetOnContractsFromFollows = await Promise.all(
|
||||
follows.map(async (follow) => {
|
||||
const unresolvedContracts = await getValues<Contract>(
|
||||
firestore
|
||||
.collection('contracts')
|
||||
.where('isResolved', '==', false)
|
||||
.where('visibility', '==', 'public')
|
||||
.where('creatorId', '==', follow.userId)
|
||||
// can't use multiple inequality (/orderBy) operators on different fields,
|
||||
// so have to filter for closed contracts separately
|
||||
.orderBy('popularityScore', 'desc')
|
||||
.limit(50)
|
||||
)
|
||||
// filter out contracts that have close times less than 6 hours from now
|
||||
const openContracts = unresolvedContracts.filter(
|
||||
(contract) => (contract?.closeTime ?? 0) > Date.now() + 6 * HOUR_MS
|
||||
)
|
||||
|
||||
return openContracts.filter(
|
||||
(contract) => !contract.uniqueBettorIds?.includes(userId)
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const sortedMarkets = uniqBy(
|
||||
unBetOnContractsFromFollows.flat(),
|
||||
(contract) => contract.id
|
||||
)
|
||||
.filter(
|
||||
(contract) =>
|
||||
contract.popularityScore !== undefined &&
|
||||
contract.popularityScore > MINIMUM_POPULARITY_SCORE
|
||||
)
|
||||
.sort((a, b) => (b.popularityScore ?? 0) - (a.popularityScore ?? 0))
|
||||
|
||||
const uniqueSortedMarkets = removeSimilarQuestions(
|
||||
sortedMarkets,
|
||||
sortedMarkets,
|
||||
true
|
||||
)
|
||||
|
||||
const topSortedMarkets = uniqueSortedMarkets.slice(0, 10)
|
||||
// log(
|
||||
// 'top 10 sorted markets by followed users',
|
||||
// topSortedMarkets.map((c) => c.question + ' ' + c.popularityScore)
|
||||
// )
|
||||
return topSortedMarkets
|
||||
}
|
||||
|
||||
const getUserUnBetOnGroupsMarkets = async (
|
||||
userId: string,
|
||||
differentThanTheseContracts: Contract[]
|
||||
) => {
|
||||
const snap = await firestore
|
||||
.collectionGroup('groupMembers')
|
||||
.where('userId', '==', userId)
|
||||
.get()
|
||||
|
||||
const groupIds = filterDefined(
|
||||
snap.docs.map((doc) => doc.ref.parent.parent?.id)
|
||||
)
|
||||
const groups = filterDefined(
|
||||
await Promise.all(groupIds.map(async (groupId) => await getGroup(groupId)))
|
||||
)
|
||||
if (groups.length === 0) return []
|
||||
|
||||
const unBetOnContractsFromGroups = await Promise.all(
|
||||
groups.map(async (group) => {
|
||||
const unresolvedContracts = await getValues<Contract>(
|
||||
firestore
|
||||
.collection('contracts')
|
||||
.where('isResolved', '==', false)
|
||||
.where('visibility', '==', 'public')
|
||||
.where('groupSlugs', 'array-contains', group.slug)
|
||||
// can't use multiple inequality (/orderBy) operators on different fields,
|
||||
// so have to filter for closed contracts separately
|
||||
.orderBy('popularityScore', 'desc')
|
||||
.limit(50)
|
||||
)
|
||||
// filter out contracts that have close times less than 6 hours from now
|
||||
const openContracts = unresolvedContracts.filter(
|
||||
(contract) => (contract?.closeTime ?? 0) > Date.now() + 6 * HOUR_MS
|
||||
)
|
||||
|
||||
return openContracts.filter(
|
||||
(contract) => !contract.uniqueBettorIds?.includes(userId)
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const sortedMarkets = uniqBy(
|
||||
unBetOnContractsFromGroups.flat(),
|
||||
(contract) => contract.id
|
||||
)
|
||||
.filter(
|
||||
(contract) =>
|
||||
contract.popularityScore !== undefined &&
|
||||
contract.popularityScore > MINIMUM_POPULARITY_SCORE
|
||||
)
|
||||
.sort((a, b) => (b.popularityScore ?? 0) - (a.popularityScore ?? 0))
|
||||
|
||||
const uniqueSortedMarkets = removeSimilarQuestions(
|
||||
sortedMarkets,
|
||||
sortedMarkets,
|
||||
true
|
||||
)
|
||||
const topSortedMarkets = removeSimilarQuestions(
|
||||
uniqueSortedMarkets,
|
||||
differentThanTheseContracts,
|
||||
false
|
||||
).slice(0, 10)
|
||||
|
||||
// log(
|
||||
// 'top 10 sorted group markets',
|
||||
// topSortedMarkets.map((c) => c.question + ' ' + c.popularityScore)
|
||||
// )
|
||||
return topSortedMarkets
|
||||
}
|
||||
|
||||
// Gets markets followed by similar bettors and bet on by similar bettors
|
||||
const getSimilarBettorsMarkets = async (
|
||||
userId: string,
|
||||
differentThanTheseContracts: Contract[]
|
||||
) => {
|
||||
// get contracts with unique bettor ids with this user
|
||||
const contractsUserHasBetOn = await getValues<Contract>(
|
||||
firestore
|
||||
.collection('contracts')
|
||||
.where('uniqueBettorIds', 'array-contains', userId)
|
||||
)
|
||||
if (contractsUserHasBetOn.length === 0) return []
|
||||
// count the number of times each unique bettor id appears on those contracts
|
||||
const bettorIdsToCounts = countBy(
|
||||
contractsUserHasBetOn.map((contract) => contract.uniqueBettorIds).flat(),
|
||||
(bettorId) => bettorId
|
||||
)
|
||||
|
||||
// sort by number of times they appear with at least 2 appearances
|
||||
const sortedBettorIds = Object.entries(bettorIdsToCounts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.filter((bettorId) => bettorId[1] > 2)
|
||||
.map((entry) => entry[0])
|
||||
.filter((bettorId) => bettorId !== userId)
|
||||
|
||||
// get the top 10 most similar bettors (excluding this user)
|
||||
const similarBettorIds = sortedBettorIds.slice(0, 10)
|
||||
if (similarBettorIds.length === 0) return []
|
||||
|
||||
// get contracts with unique bettor ids with this user
|
||||
const contractsSimilarBettorsHaveBetOn = uniqBy(
|
||||
(
|
||||
await getValues<Contract>(
|
||||
firestore
|
||||
.collection('contracts')
|
||||
.where(
|
||||
'uniqueBettorIds',
|
||||
'array-contains-any',
|
||||
similarBettorIds.slice(0, 10)
|
||||
)
|
||||
.orderBy('popularityScore', 'desc')
|
||||
.limit(200)
|
||||
)
|
||||
).filter(
|
||||
(contract) =>
|
||||
!contract.uniqueBettorIds?.includes(userId) &&
|
||||
(contract.popularityScore ?? 0) > MINIMUM_POPULARITY_SCORE
|
||||
),
|
||||
(contract) => contract.id
|
||||
)
|
||||
|
||||
// sort the contracts by how many times similar bettor ids are in their unique bettor ids array
|
||||
const sortedContractsInSimilarBettorsBets = contractsSimilarBettorsHaveBetOn
|
||||
.map((contract) => {
|
||||
const appearances = contract.uniqueBettorIds?.filter((bettorId) =>
|
||||
similarBettorIds.includes(bettorId)
|
||||
).length
|
||||
return [contract, appearances] as [Contract, number]
|
||||
})
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map((entry) => entry[0])
|
||||
|
||||
const uniqueSortedContractsInSimilarBettorsBets = removeSimilarQuestions(
|
||||
sortedContractsInSimilarBettorsBets,
|
||||
sortedContractsInSimilarBettorsBets,
|
||||
true
|
||||
)
|
||||
|
||||
const topMostSimilarContracts = removeSimilarQuestions(
|
||||
uniqueSortedContractsInSimilarBettorsBets,
|
||||
differentThanTheseContracts,
|
||||
false
|
||||
).slice(0, 10)
|
||||
|
||||
// log(
|
||||
// 'top 10 sorted contracts other similar bettors have bet on',
|
||||
// topMostSimilarContracts.map((c) => c.question)
|
||||
// )
|
||||
|
||||
return topMostSimilarContracts
|
||||
}
|
||||
|
||||
// search contract array by question and remove contracts with 3 matching words in the question
|
||||
const removeSimilarQuestions = (
|
||||
contractsToFilter: Contract[],
|
||||
byContracts: Contract[],
|
||||
allowExactSameContracts: boolean
|
||||
) => {
|
||||
// log(
|
||||
// 'contracts to filter by',
|
||||
// byContracts.map((c) => c.question + ' ' + c.popularityScore)
|
||||
// )
|
||||
let contractsToRemove: Contract[] = []
|
||||
byContracts.length > 0 &&
|
||||
byContracts.forEach((contract) => {
|
||||
const contractQuestion = stripNonAlphaChars(
|
||||
contract.question.toLowerCase()
|
||||
)
|
||||
const contractQuestionWords = uniq(contractQuestion.split(' ')).filter(
|
||||
(w) => !IGNORE_WORDS.includes(w)
|
||||
)
|
||||
contractsToRemove = contractsToRemove.concat(
|
||||
contractsToFilter.filter(
|
||||
// Remove contracts with more than 2 matching (uncommon) words and a lower popularity score
|
||||
(c2) => {
|
||||
const significantOverlap =
|
||||
// TODO: we should probably use a library for comparing strings/sentiments
|
||||
uniq(
|
||||
stripNonAlphaChars(c2.question.toLowerCase()).split(' ')
|
||||
).filter((word) => contractQuestionWords.includes(word)).length >
|
||||
2
|
||||
const lessPopular =
|
||||
(c2.popularityScore ?? 0) < (contract.popularityScore ?? 0)
|
||||
return (
|
||||
(significantOverlap && lessPopular) ||
|
||||
(allowExactSameContracts ? false : c2.id === contract.id)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
// log(
|
||||
// 'contracts to filter out',
|
||||
// contractsToRemove.map((c) => c.question)
|
||||
// )
|
||||
|
||||
const returnContracts = contractsToFilter.filter(
|
||||
(cf) => !contractsToRemove.map((c) => c.id).includes(cf.id)
|
||||
)
|
||||
|
||||
return returnContracts
|
||||
}
|
||||
|
||||
const fiveMinutes = 5 * 60 * 1000
|
||||
|
@ -116,3 +445,40 @@ function chooseRandomSubset(contracts: Contract[], count: number) {
|
|||
shuffle(contracts, rng)
|
||||
return contracts.slice(0, count)
|
||||
}
|
||||
|
||||
function stripNonAlphaChars(str: string) {
|
||||
return str.replace(/[^\w\s']|_/g, '').replace(/\s+/g, ' ')
|
||||
}
|
||||
|
||||
const IGNORE_WORDS = [
|
||||
'the',
|
||||
'a',
|
||||
'an',
|
||||
'and',
|
||||
'or',
|
||||
'of',
|
||||
'to',
|
||||
'in',
|
||||
'on',
|
||||
'will',
|
||||
'be',
|
||||
'is',
|
||||
'are',
|
||||
'for',
|
||||
'by',
|
||||
'at',
|
||||
'from',
|
||||
'what',
|
||||
'when',
|
||||
'which',
|
||||
'that',
|
||||
'it',
|
||||
'as',
|
||||
'if',
|
||||
'then',
|
||||
'than',
|
||||
'but',
|
||||
'have',
|
||||
'has',
|
||||
'had',
|
||||
]
|
||||
|
|
|
@ -14,11 +14,6 @@ export function getHtml(parsedReq: ParsedRequest) {
|
|||
numericValue,
|
||||
resolution,
|
||||
} = parsedReq
|
||||
const MAX_QUESTION_CHARS = 100
|
||||
const truncatedQuestion =
|
||||
question.length > MAX_QUESTION_CHARS
|
||||
? question.slice(0, MAX_QUESTION_CHARS) + '...'
|
||||
: question
|
||||
const hideAvatar = creatorAvatarUrl ? '' : 'hidden'
|
||||
|
||||
let resolutionColor = 'text-primary'
|
||||
|
@ -69,7 +64,7 @@ export function getHtml(parsedReq: ParsedRequest) {
|
|||
<meta charset="utf-8">
|
||||
<title>Generated Image</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=line-clamp"></script>
|
||||
</head>
|
||||
<style>
|
||||
${getTemplateCss(theme, fontSize)}
|
||||
|
@ -109,8 +104,8 @@ export function getHtml(parsedReq: ParsedRequest) {
|
|||
</div>
|
||||
|
||||
<div class="flex flex-row justify-between gap-12 pt-36">
|
||||
<div class="text-indigo-700 text-6xl leading-tight">
|
||||
${truncatedQuestion}
|
||||
<div class="text-indigo-700 text-6xl leading-tight line-clamp-4">
|
||||
${question}
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
${
|
||||
|
@ -127,7 +122,7 @@ export function getHtml(parsedReq: ParsedRequest) {
|
|||
|
||||
<!-- Metadata -->
|
||||
<div class="absolute bottom-16">
|
||||
<div class="text-gray-500 text-3xl max-w-[80vw]">
|
||||
<div class="text-gray-500 text-3xl max-w-[80vw] line-clamp-2">
|
||||
${metadata}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,8 +24,5 @@
|
|||
"prettier": "2.7.1",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.8.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.43"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export function SEO(props: {
|
|||
|
||||
return (
|
||||
<Head>
|
||||
<title>{title} | Manifold Markets</title>
|
||||
<title>{`${title} | Manifold Markets`}</title>
|
||||
|
||||
<meta
|
||||
property="og:title"
|
||||
|
|
|
@ -192,6 +192,7 @@ export function AnswerBetPanel(props: {
|
|||
isSubmitting={isSubmitting}
|
||||
disabled={!!betDisabled}
|
||||
color={'indigo'}
|
||||
actionLabel="Buy"
|
||||
/>
|
||||
) : (
|
||||
<BetSignUpPrompt />
|
||||
|
|
|
@ -20,11 +20,11 @@ import { AnswerBetPanel } from 'web/components/answers/answer-bet-panel'
|
|||
import { Row } from 'web/components/layout/row'
|
||||
import { Avatar } from 'web/components/avatar'
|
||||
import { Linkify } from 'web/components/linkify'
|
||||
import { BuyButton } from 'web/components/yes-no-selector'
|
||||
import { UserLink } from 'web/components/user-link'
|
||||
import { Button } from 'web/components/button'
|
||||
import { useAdmin } from 'web/hooks/use-admin'
|
||||
import { needsAdminToResolve } from 'web/pages/[username]/[contractSlug]'
|
||||
import { CATEGORY_COLORS } from '../charts/contract/choice'
|
||||
import { useChartAnswers } from '../charts/contract/choice'
|
||||
|
||||
export function AnswersPanel(props: {
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
|
@ -38,6 +38,7 @@ export function AnswersPanel(props: {
|
|||
const answers = (useAnswers(contract.id) ?? contract.answers).filter(
|
||||
(a) => a.number != 0 || contract.outcomeType === 'MULTIPLE_CHOICE'
|
||||
)
|
||||
|
||||
const hasZeroBetAnswers = answers.some((answer) => totalBets[answer.id] < 1)
|
||||
|
||||
const [winningAnswers, losingAnswers] = partition(
|
||||
|
@ -104,6 +105,10 @@ export function AnswersPanel(props: {
|
|||
? 'checkbox'
|
||||
: undefined
|
||||
|
||||
const colorSortedAnswer = useChartAnswers(contract).map(
|
||||
(value, _index) => value.text
|
||||
)
|
||||
|
||||
return (
|
||||
<Col className="gap-3">
|
||||
{(resolveOption || resolution) &&
|
||||
|
@ -128,7 +133,12 @@ export function AnswersPanel(props: {
|
|||
)}
|
||||
>
|
||||
{answerItems.map((item) => (
|
||||
<OpenAnswer key={item.id} answer={item} contract={contract} />
|
||||
<OpenAnswer
|
||||
key={item.id}
|
||||
answer={item}
|
||||
contract={contract}
|
||||
colorIndex={colorSortedAnswer.indexOf(item.text)}
|
||||
/>
|
||||
))}
|
||||
{hasZeroBetAnswers && !showAllAnswers && (
|
||||
<Button
|
||||
|
@ -174,15 +184,18 @@ export function AnswersPanel(props: {
|
|||
function OpenAnswer(props: {
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
answer: Answer
|
||||
colorIndex: number | undefined
|
||||
}) {
|
||||
const { answer, contract } = props
|
||||
const { username, avatarUrl, name, text } = answer
|
||||
const { answer, contract, colorIndex } = props
|
||||
const { username, avatarUrl, text } = answer
|
||||
const prob = getDpmOutcomeProbability(contract.totalShares, answer.id)
|
||||
const probPercent = formatPercent(prob)
|
||||
const [open, setOpen] = useState(false)
|
||||
const color =
|
||||
colorIndex != undefined ? CATEGORY_COLORS[colorIndex] : '#B1B1C7'
|
||||
|
||||
return (
|
||||
<Col className="border-base-200 bg-base-200 relative flex-1 rounded-md px-2">
|
||||
<Col className="my-1 px-2">
|
||||
<Modal open={open} setOpen={setOpen} position="center">
|
||||
<AnswerBetPanel
|
||||
answer={answer}
|
||||
|
@ -193,40 +206,44 @@ function OpenAnswer(props: {
|
|||
/>
|
||||
</Modal>
|
||||
|
||||
<div
|
||||
className="pointer-events-none absolute -mx-2 h-full rounded-tl-md bg-green-600 bg-opacity-10"
|
||||
style={{ width: `${100 * Math.max(prob, 0.01)}%` }}
|
||||
/>
|
||||
|
||||
<Row className="my-4 gap-3">
|
||||
<Avatar className="mx-1" username={username} avatarUrl={avatarUrl} />
|
||||
<Col className="min-w-0 flex-1 lg:gap-1">
|
||||
<div className="text-sm text-gray-500">
|
||||
<UserLink username={username} name={name} /> answered
|
||||
</div>
|
||||
|
||||
<Col className="align-items justify-between gap-4 sm:flex-row">
|
||||
<Linkify className="whitespace-pre-line text-lg" text={text} />
|
||||
<Row className="align-items items-center justify-end gap-4">
|
||||
<span
|
||||
className={clsx(
|
||||
'text-2xl',
|
||||
tradingAllowed(contract) ? 'text-primary' : 'text-gray-500'
|
||||
)}
|
||||
>
|
||||
{probPercent}
|
||||
</span>
|
||||
<BuyButton
|
||||
className={clsx(
|
||||
'btn-sm flex-initial !px-6 sm:flex',
|
||||
tradingAllowed(contract) ? '' : '!hidden'
|
||||
)}
|
||||
<Col
|
||||
className={clsx(
|
||||
'bg-greyscale-1 relative w-full rounded-lg transition-all',
|
||||
tradingAllowed(contract) ? 'text-greyscale-7' : 'text-greyscale-5'
|
||||
)}
|
||||
>
|
||||
<Row className="z-20 -mb-1 justify-between gap-2 py-2 px-3">
|
||||
<Row>
|
||||
<Avatar
|
||||
className="mt-0.5 mr-2 inline h-5 w-5 border border-transparent transition-transform hover:border-none"
|
||||
username={username}
|
||||
avatarUrl={avatarUrl}
|
||||
/>
|
||||
<Linkify
|
||||
className="text-md cursor-pointer whitespace-pre-line"
|
||||
text={text}
|
||||
/>
|
||||
</Row>
|
||||
<Row className="gap-2">
|
||||
<div className="my-auto text-xl">{probPercent}</div>
|
||||
{tradingAllowed(contract) && (
|
||||
<Button
|
||||
size="2xs"
|
||||
color="gray-outline"
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
</Row>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
className="my-auto"
|
||||
>
|
||||
BUY
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
</Row>
|
||||
<hr
|
||||
color={color}
|
||||
className="absolute z-0 h-full w-full rounded-l-lg border-none opacity-30"
|
||||
style={{ width: `${100 * Math.max(prob, 0.01)}%` }}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import clsx from 'clsx'
|
||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'
|
||||
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
|
||||
import { MenuIcon } from '@heroicons/react/solid'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
|
|
|
@ -68,11 +68,11 @@ export function AuthProvider(props: {
|
|||
}, [setAuthUser, serverUser])
|
||||
|
||||
useEffect(() => {
|
||||
if (authUser != null) {
|
||||
if (authUser) {
|
||||
// Persist to local storage, to reduce login blink next time.
|
||||
// Note: Cap on localStorage size is ~5mb
|
||||
localStorage.setItem(CACHED_USER_KEY, JSON.stringify(authUser))
|
||||
} else {
|
||||
} else if (authUser === null) {
|
||||
localStorage.removeItem(CACHED_USER_KEY)
|
||||
}
|
||||
}, [authUser])
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
NoLabel,
|
||||
YesLabel,
|
||||
} from './outcome-label'
|
||||
import { getProbability } from 'common/calculate'
|
||||
import { getContractBetMetrics, getProbability } from 'common/calculate'
|
||||
import { useFocus } from 'web/hooks/use-focus'
|
||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
||||
|
@ -395,6 +395,7 @@ export function BuyPanel(props: {
|
|||
disabled={!!betDisabled || outcome === undefined}
|
||||
size="xl"
|
||||
color={outcome === 'NO' ? 'red' : 'green'}
|
||||
actionLabel="Wager"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
|
@ -831,13 +832,21 @@ export function SellPanel(props: {
|
|||
|
||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||
|
||||
const betDisabled = isSubmitting || !amount || error
|
||||
const betDisabled = isSubmitting || !amount || error !== undefined
|
||||
|
||||
// Sell all shares if remaining shares would be < 1
|
||||
const isSellingAllShares = amount === Math.floor(shares)
|
||||
|
||||
const sellQuantity = isSellingAllShares ? shares : amount
|
||||
|
||||
const loanAmount = sumBy(userBets, (bet) => bet.loanAmount ?? 0)
|
||||
const soldShares = Math.min(sellQuantity ?? 0, shares)
|
||||
const saleFrac = soldShares / shares
|
||||
const loanPaid = saleFrac * loanAmount
|
||||
|
||||
const { invested } = getContractBetMetrics(contract, userBets)
|
||||
const costBasis = invested * saleFrac
|
||||
|
||||
async function submitSell() {
|
||||
if (!user || !amount) return
|
||||
|
||||
|
@ -882,8 +891,23 @@ export function SellPanel(props: {
|
|||
sharesOutcome,
|
||||
unfilledBets
|
||||
)
|
||||
const netProceeds = saleValue - loanPaid
|
||||
const profit = saleValue - costBasis
|
||||
const resultProb = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
|
||||
const getValue = getMappedValue(contract)
|
||||
const rawDifference = Math.abs(getValue(resultProb) - getValue(initialProb))
|
||||
const displayedDifference =
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||
? formatLargeNumber(rawDifference)
|
||||
: formatPercent(rawDifference)
|
||||
const probChange = Math.abs(resultProb - initialProb)
|
||||
|
||||
const warning =
|
||||
probChange >= 0.3
|
||||
? `Are you sure you want to move the market by ${displayedDifference}?`
|
||||
: undefined
|
||||
|
||||
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||
const [yesBets, noBets] = partition(
|
||||
openUserBets,
|
||||
|
@ -923,14 +947,18 @@ export function SellPanel(props: {
|
|||
label="Qty"
|
||||
error={error}
|
||||
disabled={isSubmitting}
|
||||
inputClassName="w-full"
|
||||
inputClassName="w-full ml-1"
|
||||
/>
|
||||
|
||||
<Col className="mt-3 w-full gap-3 text-sm">
|
||||
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||
Sale proceeds
|
||||
Sale amount
|
||||
<span className="text-neutral">{formatMoney(saleValue)}</span>
|
||||
</Row>
|
||||
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||
Profit
|
||||
<span className="text-neutral">{formatMoney(profit)}</span>
|
||||
</Row>
|
||||
<Row className="items-center justify-between">
|
||||
<div className="text-gray-500">
|
||||
{isPseudoNumeric ? 'Estimated value' : 'Probability'}
|
||||
|
@ -941,24 +969,33 @@ export function SellPanel(props: {
|
|||
{format(resultProb)}
|
||||
</div>
|
||||
</Row>
|
||||
{loanPaid !== 0 && (
|
||||
<>
|
||||
<Row className="mt-6 items-center justify-between gap-2 text-gray-500">
|
||||
Loan payment
|
||||
<span className="text-neutral">{formatMoney(-loanPaid)}</span>
|
||||
</Row>
|
||||
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||
Net proceeds
|
||||
<span className="text-neutral">{formatMoney(netProceeds)}</span>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
<Spacer h={8} />
|
||||
|
||||
<button
|
||||
className={clsx(
|
||||
'btn flex-1',
|
||||
betDisabled
|
||||
? 'btn-disabled'
|
||||
: sharesOutcome === 'YES'
|
||||
? 'btn-primary'
|
||||
: 'border-none bg-red-400 hover:bg-red-500',
|
||||
isSubmitting ? 'loading' : ''
|
||||
)}
|
||||
onClick={betDisabled ? undefined : submitSell}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit sell'}
|
||||
</button>
|
||||
<WarningConfirmationButton
|
||||
marketType="binary"
|
||||
amount={undefined}
|
||||
warning={warning}
|
||||
isSubmitting={isSubmitting}
|
||||
onSubmit={betDisabled ? undefined : submitSell}
|
||||
disabled={!!betDisabled}
|
||||
size="xl"
|
||||
color="blue"
|
||||
actionLabel={`Sell ${Math.floor(soldShares)} shares`}
|
||||
/>
|
||||
|
||||
{wasSubmitted && <div className="mt-4">Sell submitted!</div>}
|
||||
</>
|
||||
|
|
|
@ -160,26 +160,31 @@ export function BetsList(props: { user: User }) {
|
|||
unsettled,
|
||||
(c) => contractsMetrics[c.id].payout
|
||||
)
|
||||
const currentNetInvestment = sumBy(
|
||||
unsettled,
|
||||
(c) => contractsMetrics[c.id].netPayout
|
||||
)
|
||||
const currentLoan = sumBy(unsettled, (c) => contractsMetrics[c.id].loan)
|
||||
|
||||
const investedProfitPercent =
|
||||
((currentBetsValue - currentInvested) / (currentInvested + 0.1)) * 100
|
||||
|
||||
return (
|
||||
<Col>
|
||||
<Row className="justify-between gap-4 sm:flex-row">
|
||||
<Col>
|
||||
<div className="text-greyscale-6 text-xs sm:text-sm">
|
||||
Investment value
|
||||
</div>
|
||||
<div className="text-lg">
|
||||
{formatMoney(currentNetInvestment)}{' '}
|
||||
<ProfitBadge profitPercent={investedProfitPercent} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col className="justify-between gap-4 sm:flex-row">
|
||||
<Row className="gap-4">
|
||||
<Col>
|
||||
<div className="text-greyscale-6 text-xs sm:text-sm">
|
||||
Investment value
|
||||
</div>
|
||||
<div className="text-lg">
|
||||
{formatMoney(currentBetsValue)}{' '}
|
||||
<ProfitBadge profitPercent={investedProfitPercent} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div className="text-greyscale-6 text-xs sm:text-sm">
|
||||
Total loans
|
||||
</div>
|
||||
<div className="text-lg">{formatMoney(currentLoan)}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="gap-2">
|
||||
<select
|
||||
|
@ -206,7 +211,7 @@ export function BetsList(props: { user: User }) {
|
|||
<option value="closeTime">Close date</option>
|
||||
</select>
|
||||
</Row>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<Col className="mt-6 divide-y">
|
||||
{displayedContracts.length === 0 ? (
|
||||
|
@ -612,7 +617,7 @@ function SellButton(props: {
|
|||
label: 'Sell',
|
||||
disabled: isSubmitting,
|
||||
}}
|
||||
submitBtn={{ className: 'btn-primary', label: 'Sell' }}
|
||||
submitBtn={{ label: 'Sell', color: 'green' }}
|
||||
onSubmit={async () => {
|
||||
setIsSubmitting(true)
|
||||
await sellBet({ contractId: contract.id, betId: bet.id })
|
||||
|
|
|
@ -10,16 +10,54 @@ export type ColorType =
|
|||
| 'indigo'
|
||||
| 'yellow'
|
||||
| 'gray'
|
||||
| 'gray-outline'
|
||||
| 'gradient'
|
||||
| 'gray-white'
|
||||
| 'highlight-blue'
|
||||
|
||||
const sizeClasses = {
|
||||
'2xs': 'px-2 py-1 text-xs',
|
||||
xs: 'px-2.5 py-1.5 text-sm',
|
||||
sm: 'px-3 py-2 text-sm',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-4 py-2 text-base',
|
||||
xl: 'px-6 py-2.5 text-base font-semibold',
|
||||
'2xl': 'px-6 py-3 text-xl font-semibold',
|
||||
}
|
||||
|
||||
export function buttonClass(size: SizeType, color: ColorType | 'override') {
|
||||
return clsx(
|
||||
'font-md inline-flex items-center justify-center rounded-md border border-transparent shadow-sm transition-colors disabled:cursor-not-allowed',
|
||||
sizeClasses[size],
|
||||
color === 'green' &&
|
||||
'disabled:bg-greyscale-2 bg-teal-500 text-white hover:bg-teal-600',
|
||||
color === 'red' &&
|
||||
'disabled:bg-greyscale-2 bg-red-400 text-white hover:bg-red-500',
|
||||
color === 'yellow' &&
|
||||
'disabled:bg-greyscale-2 bg-yellow-400 text-white hover:bg-yellow-500',
|
||||
color === 'blue' &&
|
||||
'disabled:bg-greyscale-2 bg-blue-400 text-white hover:bg-blue-500',
|
||||
color === 'indigo' &&
|
||||
'disabled:bg-greyscale-2 bg-indigo-500 text-white hover:bg-indigo-600',
|
||||
color === 'gray' &&
|
||||
'bg-greyscale-1 text-greyscale-6 hover:bg-greyscale-2 disabled:opacity-50',
|
||||
color === 'gray-outline' &&
|
||||
'border-greyscale-4 text-greyscale-4 hover:bg-greyscale-4 border-2 hover:text-white disabled:opacity-50',
|
||||
color === 'gradient' &&
|
||||
'disabled:bg-greyscale-2 border-none bg-gradient-to-r from-indigo-500 to-blue-500 text-white hover:from-indigo-700 hover:to-blue-700',
|
||||
color === 'gray-white' &&
|
||||
'text-greyscale-6 hover:bg-greyscale-2 border-none shadow-none disabled:opacity-50',
|
||||
color === 'highlight-blue' &&
|
||||
'text-highlight-blue disabled:bg-greyscale-2 border-none shadow-none'
|
||||
)
|
||||
}
|
||||
|
||||
export function Button(props: {
|
||||
className?: string
|
||||
onClick?: MouseEventHandler<any> | undefined
|
||||
children?: ReactNode
|
||||
size?: SizeType
|
||||
color?: ColorType
|
||||
color?: ColorType | 'override'
|
||||
type?: 'button' | 'reset' | 'submit'
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
|
@ -35,42 +73,10 @@ export function Button(props: {
|
|||
loading,
|
||||
} = props
|
||||
|
||||
const sizeClasses = {
|
||||
'2xs': 'px-2 py-1 text-xs',
|
||||
xs: 'px-2.5 py-1.5 text-sm',
|
||||
sm: 'px-3 py-2 text-sm',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-4 py-2 text-base',
|
||||
xl: 'px-6 py-2.5 text-base font-semibold',
|
||||
'2xl': 'px-6 py-3 text-xl font-semibold',
|
||||
}[size]
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
className={clsx(
|
||||
'font-md items-center justify-center rounded-md border border-transparent shadow-sm transition-colors disabled:cursor-not-allowed',
|
||||
sizeClasses,
|
||||
color === 'green' &&
|
||||
'disabled:bg-greyscale-2 bg-teal-500 text-white hover:bg-teal-600',
|
||||
color === 'red' &&
|
||||
'disabled:bg-greyscale-2 bg-red-400 text-white hover:bg-red-500',
|
||||
color === 'yellow' &&
|
||||
'disabled:bg-greyscale-2 bg-yellow-400 text-white hover:bg-yellow-500',
|
||||
color === 'blue' &&
|
||||
'disabled:bg-greyscale-2 bg-blue-400 text-white hover:bg-blue-500',
|
||||
color === 'indigo' &&
|
||||
'disabled:bg-greyscale-2 bg-indigo-500 text-white hover:bg-indigo-600',
|
||||
color === 'gray' &&
|
||||
'bg-greyscale-1 text-greyscale-6 hover:bg-greyscale-2 disabled:opacity-50',
|
||||
color === 'gradient' &&
|
||||
'disabled:bg-greyscale-2 border-none bg-gradient-to-r from-indigo-500 to-blue-500 text-white hover:from-indigo-700 hover:to-blue-700',
|
||||
color === 'gray-white' &&
|
||||
'text-greyscale-6 hover:bg-greyscale-2 border-none shadow-none disabled:opacity-50',
|
||||
color === 'highlight-blue' &&
|
||||
'text-highlight-blue disabled:bg-greyscale-2 border-none shadow-none',
|
||||
className
|
||||
)}
|
||||
className={clsx(buttonClass(size, color), className)}
|
||||
disabled={disabled || loading}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
|
16
web/components/card.tsx
Normal file
16
web/components/card.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import clsx from 'clsx'
|
||||
|
||||
export function Card(props: JSX.IntrinsicElements['div']) {
|
||||
const { children, className, ...rest } = props
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'cursor-pointer rounded-lg border-2 bg-white transition-shadow hover:shadow-md focus:shadow-md',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { LinkIcon, SwitchVerticalIcon } from '@heroicons/react/outline'
|
||||
import toast from 'react-hot-toast'
|
||||
import { SwitchVerticalIcon } from '@heroicons/react/outline'
|
||||
|
||||
import { Col } from '../layout/col'
|
||||
import { Row } from '../layout/row'
|
||||
|
@ -16,7 +15,6 @@ import { SiteLink } from 'web/components/site-link'
|
|||
import { formatMoney } from 'common/util/format'
|
||||
import { NoLabel, YesLabel } from '../outcome-label'
|
||||
import { QRCode } from '../qr-code'
|
||||
import { copyToClipboard } from 'web/lib/util/copy'
|
||||
import { AmountInput } from '../amount-input'
|
||||
import { getProbability } from 'common/calculate'
|
||||
import { createMarket } from 'web/lib/firebase/api'
|
||||
|
@ -26,6 +24,7 @@ import Textarea from 'react-expanding-textarea'
|
|||
import { useTextEditor } from 'web/components/editor'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { CopyLinkButton } from '../copy-link-button'
|
||||
|
||||
type challengeInfo = {
|
||||
amount: number
|
||||
|
@ -302,16 +301,7 @@ function CreateChallengeForm(props: {
|
|||
<Title className="!my-0" text="Challenge Created!" />
|
||||
|
||||
<div>Share the challenge using the link.</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
copyToClipboard(challengeSlug)
|
||||
toast('Link copied to clipboard!')
|
||||
}}
|
||||
className={'btn btn-outline mb-4 whitespace-nowrap normal-case'}
|
||||
>
|
||||
<LinkIcon className={'mr-2 h-5 w-5'} />
|
||||
Copy link
|
||||
</button>
|
||||
<CopyLinkButton url={challengeSlug} />
|
||||
|
||||
<QRCode url={challengeSlug} className="self-center" />
|
||||
<Row className={'gap-1 text-gray-500'}>
|
||||
|
|
|
@ -6,6 +6,8 @@ import { Charity } from 'common/charity'
|
|||
import { useCharityTxns } from 'web/hooks/use-charity-txns'
|
||||
import { manaToUSD } from '../../../common/util/format'
|
||||
import { Row } from '../layout/row'
|
||||
import { Col } from '../layout/col'
|
||||
import { Card } from '../card'
|
||||
|
||||
export function CharityCard(props: { charity: Charity; match?: number }) {
|
||||
const { charity } = props
|
||||
|
@ -15,43 +17,44 @@ export function CharityCard(props: { charity: Charity; match?: number }) {
|
|||
const raised = sumBy(txns, (txn) => txn.amount)
|
||||
|
||||
return (
|
||||
<Link href={`/charity/${slug}`} passHref>
|
||||
<div className="card card-compact transition:shadow flex-1 cursor-pointer border-2 bg-white hover:shadow-md">
|
||||
<Row className="mt-6 mb-2">
|
||||
{tags?.includes('Featured') && <FeaturedBadge />}
|
||||
</Row>
|
||||
<div className="px-8">
|
||||
<figure className="relative h-32">
|
||||
{photo ? (
|
||||
<Image src={photo} alt="" layout="fill" objectFit="contain" />
|
||||
) : (
|
||||
<div className="h-full w-full bg-gradient-to-r from-slate-300 to-indigo-200" />
|
||||
)}
|
||||
</figure>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{/* <h3 className="card-title line-clamp-3">{name}</h3> */}
|
||||
<div className="line-clamp-4 text-sm">{preview}</div>
|
||||
{raised > 0 && (
|
||||
<>
|
||||
<Row className="mt-4 flex-1 items-end justify-center gap-6 text-gray-900">
|
||||
<Row className="items-baseline gap-1">
|
||||
<span className="text-3xl font-semibold">
|
||||
{formatUsd(raised)}
|
||||
</span>
|
||||
raised
|
||||
</Row>
|
||||
{/* {match && (
|
||||
<Link href={`/charity/${slug}`}>
|
||||
<a className="flex-1">
|
||||
<Card className="!rounded-2xl">
|
||||
<Row className="mt-6 mb-2">
|
||||
{tags?.includes('Featured') && <FeaturedBadge />}
|
||||
</Row>
|
||||
<div className="px-8">
|
||||
<figure className="relative h-32">
|
||||
{photo ? (
|
||||
<Image src={photo} alt="" layout="fill" objectFit="contain" />
|
||||
) : (
|
||||
<div className="h-full w-full bg-gradient-to-r from-slate-300 to-indigo-200" />
|
||||
)}
|
||||
</figure>
|
||||
</div>
|
||||
<Col className="p-8">
|
||||
<div className="line-clamp-4 text-sm">{preview}</div>
|
||||
{raised > 0 && (
|
||||
<>
|
||||
<Row className="mt-4 flex-1 items-end justify-center gap-6 text-gray-900">
|
||||
<Row className="items-baseline gap-1">
|
||||
<span className="text-3xl font-semibold">
|
||||
{formatUsd(raised)}
|
||||
</span>
|
||||
raised
|
||||
</Row>
|
||||
{/* {match && (
|
||||
<Col className="text-gray-500">
|
||||
<span className="text-xl">+{formatUsd(match)}</span>
|
||||
<span className="">match</span>
|
||||
</Col>
|
||||
)} */}
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Card>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -31,9 +31,9 @@ const getBetPoints = (bets: Bet[]) => {
|
|||
}
|
||||
|
||||
const BinaryChartTooltip = (props: TooltipProps<Date, HistoryPoint<Bet>>) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const { data, x, xScale } = props
|
||||
const [start, end] = xScale.domain()
|
||||
const d = xScale.invert(mouseX)
|
||||
const d = xScale.invert(x)
|
||||
return (
|
||||
<Row className="items-center gap-2">
|
||||
{data.obj && <Avatar size="xs" avatarUrl={data.obj.userAvatarUrl} />}
|
||||
|
|
|
@ -19,62 +19,59 @@ import { MultiPoint, MultiValueHistoryChart } from '../generic-charts'
|
|||
import { Row } from 'web/components/layout/row'
|
||||
import { Avatar } from 'web/components/avatar'
|
||||
|
||||
// thanks to https://observablehq.com/@jonhelfman/optimal-orders-for-choosing-categorical-colors
|
||||
const CATEGORY_COLORS = [
|
||||
'#00b8dd',
|
||||
'#eecafe',
|
||||
'#874c62',
|
||||
'#6457ca',
|
||||
'#f773ba',
|
||||
'#9c6bbc',
|
||||
'#a87744',
|
||||
'#af8a04',
|
||||
'#bff9aa',
|
||||
'#f3d89d',
|
||||
'#c9a0f5',
|
||||
'#ff00e5',
|
||||
'#9dc6f7',
|
||||
'#824475',
|
||||
'#d973cc',
|
||||
'#bc6808',
|
||||
'#056e70',
|
||||
'#677932',
|
||||
'#00b287',
|
||||
'#c8ab6c',
|
||||
'#a2fb7a',
|
||||
'#f8db68',
|
||||
'#14675a',
|
||||
'#8288f4',
|
||||
'#fe1ca0',
|
||||
'#ad6aff',
|
||||
'#786306',
|
||||
'#9bfbaf',
|
||||
'#b00cf7',
|
||||
'#2f7ec5',
|
||||
'#4b998b',
|
||||
'#42fa0e',
|
||||
'#5b80a1',
|
||||
'#962d9d',
|
||||
'#3385ff',
|
||||
'#48c5ab',
|
||||
'#b2c873',
|
||||
'#4cf9a4',
|
||||
'#00ffff',
|
||||
'#3cca73',
|
||||
'#99ae17',
|
||||
'#7af5cf',
|
||||
'#52af45',
|
||||
'#fbb80f',
|
||||
'#29971b',
|
||||
'#187c9a',
|
||||
'#00d539',
|
||||
'#bbfa1a',
|
||||
'#61f55c',
|
||||
'#cabc03',
|
||||
'#ff9000',
|
||||
'#779100',
|
||||
'#bcfd6f',
|
||||
'#70a560',
|
||||
export const CATEGORY_COLORS = [
|
||||
'#7eb0d5',
|
||||
'#fd7f6f',
|
||||
'#b2e061',
|
||||
'#bd7ebe',
|
||||
'#ffb55a',
|
||||
'#ffee65',
|
||||
'#beb9db',
|
||||
'#fdcce5',
|
||||
'#8bd3c7',
|
||||
'#bddfb7',
|
||||
'#e2e3f3',
|
||||
'#fafafa',
|
||||
'#9fcdeb',
|
||||
'#d3d3d3',
|
||||
'#b1a296',
|
||||
'#e1bdb6',
|
||||
'#f2dbc0',
|
||||
'#fae5d3',
|
||||
'#c5e0ec',
|
||||
'#e0f0ff',
|
||||
'#ffddcd',
|
||||
'#fbd5e2',
|
||||
'#f2e7e5',
|
||||
'#ffe7ba',
|
||||
'#eed9c4',
|
||||
'#ea9999',
|
||||
'#f9cb9c',
|
||||
'#ffe599',
|
||||
'#b6d7a8',
|
||||
'#a2c4c9',
|
||||
'#9fc5e8',
|
||||
'#b4a7d6',
|
||||
'#d5a6bd',
|
||||
'#e06666',
|
||||
'#f6b26b',
|
||||
'#ffd966',
|
||||
'#93c47d',
|
||||
'#76a5af',
|
||||
'#6fa8dc',
|
||||
'#8e7cc3',
|
||||
'#c27ba0',
|
||||
'#cc0000',
|
||||
'#e69138',
|
||||
'#f1c232',
|
||||
'#6aa84f',
|
||||
'#45818e',
|
||||
'#3d85c6',
|
||||
'#674ea7',
|
||||
'#a64d79',
|
||||
'#990000',
|
||||
'#b45f06',
|
||||
'#bf9000',
|
||||
]
|
||||
|
||||
const MARGIN = { top: 20, right: 10, bottom: 20, left: 40 }
|
||||
|
@ -144,6 +141,15 @@ const Legend = (props: { className?: string; items: LegendItem[] }) => {
|
|||
)
|
||||
}
|
||||
|
||||
export function useChartAnswers(
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
) {
|
||||
return useMemo(
|
||||
() => getTrackedAnswers(contract, CATEGORY_COLORS.length),
|
||||
[contract]
|
||||
)
|
||||
}
|
||||
|
||||
export const ChoiceContractChart = (props: {
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
bets: Bet[]
|
||||
|
@ -153,10 +159,7 @@ export const ChoiceContractChart = (props: {
|
|||
}) => {
|
||||
const { contract, bets, width, height, onMouseOver } = props
|
||||
const [start, end] = getDateRange(contract)
|
||||
const answers = useMemo(
|
||||
() => getTrackedAnswers(contract, CATEGORY_COLORS.length),
|
||||
[contract]
|
||||
)
|
||||
const answers = useChartAnswers(contract)
|
||||
const betPoints = useMemo(() => getBetPoints(answers, bets), [answers, bets])
|
||||
const data = useMemo(
|
||||
() => [
|
||||
|
@ -180,9 +183,9 @@ export const ChoiceContractChart = (props: {
|
|||
|
||||
const ChoiceTooltip = useMemo(
|
||||
() => (props: TooltipProps<Date, MultiPoint<Bet>>) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const { data, x, xScale } = props
|
||||
const [start, end] = xScale.domain()
|
||||
const d = xScale.invert(mouseX)
|
||||
const d = xScale.invert(x)
|
||||
const legendItems = sortBy(
|
||||
data.y.map((p, i) => ({
|
||||
color: CATEGORY_COLORS[i],
|
||||
|
|
|
@ -26,11 +26,11 @@ const getNumericChartData = (contract: NumericContract) => {
|
|||
const NumericChartTooltip = (
|
||||
props: TooltipProps<number, DistributionPoint>
|
||||
) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const x = xScale.invert(mouseX)
|
||||
const { data, x, xScale } = props
|
||||
const amount = xScale.invert(x)
|
||||
return (
|
||||
<>
|
||||
<span className="text-semibold">{formatLargeNumber(x)}</span>
|
||||
<span className="text-semibold">{formatLargeNumber(amount)}</span>
|
||||
<span className="text-greyscale-6">{formatPct(data.y, 2)}</span>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -45,9 +45,9 @@ const getBetPoints = (bets: Bet[], scaleP: (p: number) => number) => {
|
|||
const PseudoNumericChartTooltip = (
|
||||
props: TooltipProps<Date, HistoryPoint<Bet>>
|
||||
) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const { data, x, xScale } = props
|
||||
const [start, end] = xScale.domain()
|
||||
const d = xScale.invert(mouseX)
|
||||
const d = xScale.invert(x)
|
||||
return (
|
||||
<Row className="items-center gap-2">
|
||||
{data.obj && <Avatar size="xs" avatarUrl={data.obj.userAvatarUrl} />}
|
||||
|
|
|
@ -7,6 +7,8 @@ import {
|
|||
CurveFactory,
|
||||
SeriesPoint,
|
||||
curveLinear,
|
||||
curveStepBefore,
|
||||
curveStepAfter,
|
||||
stack,
|
||||
stackOrderReverse,
|
||||
} from 'd3-shape'
|
||||
|
@ -19,6 +21,8 @@ import {
|
|||
AreaPath,
|
||||
AreaWithTopStroke,
|
||||
Point,
|
||||
SliceMarker,
|
||||
TooltipParams,
|
||||
TooltipComponent,
|
||||
computeColorStops,
|
||||
formatPct,
|
||||
|
@ -32,21 +36,42 @@ export type HistoryPoint<T = unknown> = Point<Date, number, T>
|
|||
export type DistributionPoint<T = unknown> = Point<number, number, T>
|
||||
export type ValueKind = 'm$' | 'percent' | 'amount'
|
||||
|
||||
type SliceExtent = { y0: number; y1: number }
|
||||
|
||||
const interpolateY = (
|
||||
curve: CurveFactory,
|
||||
x: number,
|
||||
x0: number,
|
||||
x1: number,
|
||||
y0: number,
|
||||
y1: number
|
||||
) => {
|
||||
if (curve === curveLinear) {
|
||||
const p = (x - x0) / (x1 - x0)
|
||||
return y0 * (1 - p) + y1 * p
|
||||
} else if (curve === curveStepAfter) {
|
||||
return y0
|
||||
} else if (curve === curveStepBefore) {
|
||||
return y1
|
||||
}
|
||||
}
|
||||
|
||||
const getTickValues = (min: number, max: number, n: number) => {
|
||||
const step = (max - min) / (n - 1)
|
||||
return [min, ...range(1, n - 1).map((i) => min + step * i), max]
|
||||
}
|
||||
|
||||
const betAtPointSelector = <X, Y, P extends Point<X, Y>>(
|
||||
const dataAtPointSelector = <X, Y, P extends Point<X, Y>>(
|
||||
data: P[],
|
||||
xScale: ContinuousScale<X>
|
||||
) => {
|
||||
const bisect = bisector((p: P) => p.x)
|
||||
return (posX: number) => {
|
||||
const x = xScale.invert(posX)
|
||||
const item = data[bisect.left(data, x) - 1]
|
||||
const result = item ? { ...item, x: posX } : undefined
|
||||
return result
|
||||
const i = bisect.left(data, x)
|
||||
const prev = data[i - 1] as P | undefined
|
||||
const next = data[i] as P | undefined
|
||||
return { prev, next, x: posX }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,6 +89,7 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
|||
}) => {
|
||||
const { data, w, h, color, margin, yScale, curve, Tooltip } = props
|
||||
|
||||
const [ttParams, setTTParams] = useState<TooltipParams<P>>()
|
||||
const [viewXScale, setViewXScale] =
|
||||
useState<ScaleContinuousNumeric<number, number>>()
|
||||
const xScale = viewXScale ?? props.xScale
|
||||
|
@ -78,13 +104,19 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
|||
return { xAxis, yAxis }
|
||||
}, [w, xScale, yScale])
|
||||
|
||||
const selector = betAtPointSelector(data, xScale)
|
||||
const onMouseOver = useEvent((mouseX: number) => {
|
||||
const selector = dataAtPointSelector(data, xScale)
|
||||
const onMouseOver = useEvent((mouseX: number, mouseY: number) => {
|
||||
const p = selector(mouseX)
|
||||
props.onMouseOver?.(p)
|
||||
return p
|
||||
props.onMouseOver?.(p.prev)
|
||||
if (p.prev) {
|
||||
setTTParams({ x: mouseX, y: mouseY, data: p.prev })
|
||||
} else {
|
||||
setTTParams(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
const onMouseLeave = useEvent(() => setTTParams(undefined))
|
||||
|
||||
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||
if (ev.selection) {
|
||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||
|
@ -103,8 +135,10 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
|||
margin={margin}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
ttParams={ttParams}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
Tooltip={Tooltip}
|
||||
>
|
||||
<AreaWithTopStroke
|
||||
|
@ -134,6 +168,7 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|||
}) => {
|
||||
const { data, w, h, colors, margin, yScale, yKind, curve, Tooltip } = props
|
||||
|
||||
const [ttParams, setTTParams] = useState<TooltipParams<P>>()
|
||||
const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
|
||||
const xScale = viewXScale ?? props.xScale
|
||||
|
||||
|
@ -168,16 +203,23 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|||
return d3Stack(data)
|
||||
}, [data])
|
||||
|
||||
const selector = betAtPointSelector(data, xScale)
|
||||
const onMouseOver = useEvent((mouseX: number) => {
|
||||
const selector = dataAtPointSelector(data, xScale)
|
||||
const onMouseOver = useEvent((mouseX: number, mouseY: number) => {
|
||||
const p = selector(mouseX)
|
||||
props.onMouseOver?.(p)
|
||||
return p
|
||||
props.onMouseOver?.(p.prev)
|
||||
if (p.prev) {
|
||||
setTTParams({ x: mouseX, y: mouseY, data: p.prev })
|
||||
} else {
|
||||
setTTParams(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
const onMouseLeave = useEvent(() => setTTParams(undefined))
|
||||
|
||||
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||
if (ev.selection) {
|
||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||
|
||||
setViewXScale(() =>
|
||||
xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
||||
)
|
||||
|
@ -193,8 +235,10 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|||
margin={margin}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
ttParams={ttParams}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
Tooltip={Tooltip}
|
||||
>
|
||||
{series.map((s, i) => (
|
||||
|
@ -226,13 +270,15 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
Tooltip?: TooltipComponent<Date, P>
|
||||
pct?: boolean
|
||||
}) => {
|
||||
const { data, w, h, color, margin, yScale, yKind, curve, Tooltip } = props
|
||||
const { data, w, h, color, margin, yScale, yKind, Tooltip } = props
|
||||
const curve = props.curve ?? curveLinear
|
||||
|
||||
const [mouse, setMouse] = useState<TooltipParams<P> & SliceExtent>()
|
||||
const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
|
||||
const xScale = viewXScale ?? props.xScale
|
||||
|
||||
const px = useCallback((p: P) => xScale(p.x), [xScale])
|
||||
const py0 = yScale(yScale.domain()[0])
|
||||
const py0 = yScale(0)
|
||||
const py1 = useCallback((p: P) => yScale(p.y), [yScale])
|
||||
|
||||
const { xAxis, yAxis } = useMemo(() => {
|
||||
|
@ -253,21 +299,53 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
return { xAxis, yAxis }
|
||||
}, [w, h, yKind, xScale, yScale])
|
||||
|
||||
const selector = betAtPointSelector(data, xScale)
|
||||
const selector = dataAtPointSelector(data, xScale)
|
||||
const onMouseOver = useEvent((mouseX: number) => {
|
||||
const p = selector(mouseX)
|
||||
props.onMouseOver?.(p)
|
||||
return p
|
||||
props.onMouseOver?.(p.prev)
|
||||
const x0 = p.prev ? xScale(p.prev.x) : xScale.range()[0]
|
||||
const x1 = p.next ? xScale(p.next.x) : xScale.range()[1]
|
||||
const y0 = p.prev ? yScale(p.prev.y) : yScale.range()[0]
|
||||
const y1 = p.next ? yScale(p.next.y) : yScale.range()[1]
|
||||
const markerY = interpolateY(curve, mouseX, x0, x1, y0, y1)
|
||||
if (p.prev && markerY) {
|
||||
setMouse({
|
||||
x: mouseX,
|
||||
y: markerY,
|
||||
y0: py0,
|
||||
y1: markerY,
|
||||
data: p.prev,
|
||||
})
|
||||
} else {
|
||||
setMouse(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
const onMouseLeave = useEvent(() => setMouse(undefined))
|
||||
|
||||
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||
if (ev.selection) {
|
||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||
setViewXScale(() =>
|
||||
xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
||||
)
|
||||
const newViewXScale = xScale
|
||||
.copy()
|
||||
.domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
||||
setViewXScale(() => newViewXScale)
|
||||
|
||||
const dataInView = data.filter((p) => {
|
||||
const x = newViewXScale(p.x)
|
||||
return x >= 0 && x <= w
|
||||
})
|
||||
const yMin = Math.min(...dataInView.map((p) => p.y))
|
||||
const yMax = Math.max(...dataInView.map((p) => p.y))
|
||||
|
||||
// Prevents very small selections from being too zoomed in
|
||||
if (yMax - yMin > 0.05) {
|
||||
// adds a little padding to the top and bottom of the selection
|
||||
yScale.domain([yMin - (yMax - yMin) * 0.1, yMax + (yMax - yMin) * 0.1])
|
||||
}
|
||||
} else {
|
||||
setViewXScale(undefined)
|
||||
yScale.domain([0, 1])
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -285,8 +363,12 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
margin={margin}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
ttParams={
|
||||
mouse ? { x: mouse.x, y: mouse.y, data: mouse.data } : undefined
|
||||
}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
Tooltip={Tooltip}
|
||||
>
|
||||
{stops && (
|
||||
|
@ -306,6 +388,9 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
py1={py1}
|
||||
curve={curve ?? curveLinear}
|
||||
/>
|
||||
{mouse && (
|
||||
<SliceMarker color="#5BCEFF" x={mouse.x} y0={mouse.y0} y1={mouse.y1} />
|
||||
)}
|
||||
</SVGChart>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
ReactNode,
|
||||
SVGProps,
|
||||
memo,
|
||||
useRef,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { ReactNode, SVGProps, memo, useRef, useEffect, useMemo } from 'react'
|
||||
import { pointer, select } from 'd3-selection'
|
||||
import { Axis, AxisScale } from 'd3-axis'
|
||||
import { brushX, D3BrushEvent } from 'd3-brush'
|
||||
|
@ -17,6 +9,7 @@ import clsx from 'clsx'
|
|||
|
||||
import { Contract } from 'common/contract'
|
||||
import { useMeasureSize } from 'web/hooks/use-measure-size'
|
||||
import { useIsMobile } from 'web/hooks/use-is-mobile'
|
||||
|
||||
export type Point<X, Y, T = unknown> = { x: X; y: Y; obj?: T }
|
||||
|
||||
|
@ -123,6 +116,28 @@ export const AreaWithTopStroke = <P,>(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export const SliceMarker = (props: {
|
||||
color: string
|
||||
x: number
|
||||
y0: number
|
||||
y1: number
|
||||
}) => {
|
||||
const { color, x, y0, y1 } = props
|
||||
return (
|
||||
<g>
|
||||
<line stroke="white" strokeWidth={1} x1={x} x2={x} y1={y0} y2={y1} />
|
||||
<circle
|
||||
stroke="white"
|
||||
strokeWidth={1}
|
||||
fill={color}
|
||||
cx={x}
|
||||
cy={y1}
|
||||
r={5}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
export const SVGChart = <X, TT>(props: {
|
||||
children: ReactNode
|
||||
w: number
|
||||
|
@ -130,8 +145,10 @@ export const SVGChart = <X, TT>(props: {
|
|||
margin: Margin
|
||||
xAxis: Axis<X>
|
||||
yAxis: Axis<number>
|
||||
ttParams: TooltipParams<TT> | undefined
|
||||
onSelect?: (ev: D3BrushEvent<any>) => void
|
||||
onMouseOver?: (mouseX: number, mouseY: number) => TT | undefined
|
||||
onMouseOver?: (mouseX: number, mouseY: number) => void
|
||||
onMouseLeave?: () => void
|
||||
Tooltip?: TooltipComponent<X, TT>
|
||||
}) => {
|
||||
const {
|
||||
|
@ -141,16 +158,18 @@ export const SVGChart = <X, TT>(props: {
|
|||
margin,
|
||||
xAxis,
|
||||
yAxis,
|
||||
onMouseOver,
|
||||
ttParams,
|
||||
onSelect,
|
||||
onMouseOver,
|
||||
onMouseLeave,
|
||||
Tooltip,
|
||||
} = props
|
||||
const [mouse, setMouse] = useState<{ x: number; y: number; data: TT }>()
|
||||
const tooltipMeasure = useMeasureSize()
|
||||
const overlayRef = useRef<SVGGElement>(null)
|
||||
const innerW = w - (margin.left + margin.right)
|
||||
const innerH = h - (margin.top + margin.bottom)
|
||||
const clipPathId = useMemo(() => nanoid(), [])
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const justSelected = useRef(false)
|
||||
useEffect(() => {
|
||||
|
@ -165,7 +184,7 @@ export const SVGChart = <X, TT>(props: {
|
|||
if (!justSelected.current) {
|
||||
justSelected.current = true
|
||||
onSelect(ev)
|
||||
setMouse(undefined)
|
||||
onMouseLeave?.()
|
||||
if (overlayRef.current) {
|
||||
select(overlayRef.current).call(brush.clear)
|
||||
}
|
||||
|
@ -181,44 +200,49 @@ export const SVGChart = <X, TT>(props: {
|
|||
.select('.selection')
|
||||
.attr('shape-rendering', 'null')
|
||||
}
|
||||
}, [innerW, innerH, onSelect])
|
||||
}, [innerW, innerH, onSelect, onMouseLeave])
|
||||
|
||||
const onPointerMove = (ev: React.PointerEvent) => {
|
||||
if (ev.pointerType === 'mouse' && onMouseOver) {
|
||||
const [x, y] = pointer(ev)
|
||||
const data = onMouseOver(x, y)
|
||||
if (data !== undefined) {
|
||||
setMouse({ x, y, data })
|
||||
} else {
|
||||
setMouse(undefined)
|
||||
}
|
||||
onMouseOver(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
const onTouchMove = (ev: React.TouchEvent) => {
|
||||
if (onMouseOver) {
|
||||
const touch = ev.touches[0]
|
||||
const x = touch.pageX - ev.currentTarget.getBoundingClientRect().left
|
||||
const y = touch.pageY - ev.currentTarget.getBoundingClientRect().top
|
||||
onMouseOver(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerLeave = () => {
|
||||
setMouse(undefined)
|
||||
onMouseLeave?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden">
|
||||
{mouse && Tooltip && (
|
||||
{ttParams && Tooltip && (
|
||||
<TooltipContainer
|
||||
setElem={tooltipMeasure.setElem}
|
||||
margin={margin}
|
||||
pos={getTooltipPosition(
|
||||
mouse.x,
|
||||
mouse.y,
|
||||
ttParams.x,
|
||||
ttParams.y,
|
||||
innerW,
|
||||
innerH,
|
||||
tooltipMeasure.width,
|
||||
tooltipMeasure.height
|
||||
tooltipMeasure.width ?? 140,
|
||||
tooltipMeasure.height ?? 35,
|
||||
isMobile ?? false
|
||||
)}
|
||||
>
|
||||
<Tooltip
|
||||
xScale={xAxis.scale()}
|
||||
mouseX={mouse.x}
|
||||
mouseY={mouse.y}
|
||||
data={mouse.data}
|
||||
x={ttParams.x}
|
||||
y={ttParams.y}
|
||||
data={ttParams.data}
|
||||
/>
|
||||
</TooltipContainer>
|
||||
)}
|
||||
|
@ -230,18 +254,30 @@ export const SVGChart = <X, TT>(props: {
|
|||
<XAxis axis={xAxis} w={innerW} h={innerH} />
|
||||
<YAxis axis={yAxis} w={innerW} h={innerH} />
|
||||
<g clipPath={`url(#${clipPathId})`}>{children}</g>
|
||||
<g
|
||||
ref={overlayRef}
|
||||
x="0"
|
||||
y="0"
|
||||
width={innerW}
|
||||
height={innerH}
|
||||
fill="none"
|
||||
pointerEvents="all"
|
||||
onPointerEnter={onPointerMove}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerLeave={onPointerLeave}
|
||||
/>
|
||||
{!isMobile ? (
|
||||
<g
|
||||
ref={overlayRef}
|
||||
x="0"
|
||||
y="0"
|
||||
width={innerW}
|
||||
height={innerH}
|
||||
fill="none"
|
||||
pointerEvents="all"
|
||||
onPointerEnter={onPointerMove}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerLeave={onPointerLeave}
|
||||
/>
|
||||
) : (
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width={innerW}
|
||||
height={innerH}
|
||||
fill="transparent"
|
||||
onTouchMove={onTouchMove}
|
||||
onTouchEnd={onPointerLeave}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -255,31 +291,34 @@ export const getTooltipPosition = (
|
|||
mouseY: number,
|
||||
containerWidth: number,
|
||||
containerHeight: number,
|
||||
tooltipWidth?: number,
|
||||
tooltipHeight?: number
|
||||
tooltipWidth: number,
|
||||
tooltipHeight: number,
|
||||
isMobile: boolean
|
||||
) => {
|
||||
let left = mouseX + 12
|
||||
let bottom = containerHeight - mouseY + 12
|
||||
let bottom = !isMobile
|
||||
? containerHeight - mouseY + 12
|
||||
: containerHeight - tooltipHeight + 12
|
||||
if (tooltipWidth != null) {
|
||||
const overflow = left + tooltipWidth - containerWidth
|
||||
if (overflow > 0) {
|
||||
left -= overflow
|
||||
}
|
||||
}
|
||||
|
||||
if (tooltipHeight != null) {
|
||||
const overflow = tooltipHeight - mouseY
|
||||
if (overflow > 0) {
|
||||
bottom -= overflow
|
||||
}
|
||||
}
|
||||
|
||||
return { left, bottom }
|
||||
}
|
||||
|
||||
export type TooltipProps<X, T> = {
|
||||
mouseX: number
|
||||
mouseY: number
|
||||
export type TooltipParams<T> = { x: number; y: number; data: T }
|
||||
export type TooltipProps<X, T> = TooltipParams<T> & {
|
||||
xScale: ContinuousScale<X>
|
||||
data: T
|
||||
}
|
||||
|
||||
export type TooltipComponent<X, T> = React.ComponentType<TooltipProps<X, T>>
|
||||
|
|
|
@ -22,8 +22,8 @@ const getPoints = (startDate: number, dailyValues: number[]) => {
|
|||
}
|
||||
|
||||
const DailyCountTooltip = (props: TooltipProps<Date, HistoryPoint>) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const d = xScale.invert(mouseX)
|
||||
const { data, x, xScale } = props
|
||||
const d = xScale.invert(x)
|
||||
return (
|
||||
<Row className="items-center gap-2">
|
||||
<span className="font-semibold">{dayjs(d).format('MMM DD')}</span>
|
||||
|
@ -33,8 +33,8 @@ const DailyCountTooltip = (props: TooltipProps<Date, HistoryPoint>) => {
|
|||
}
|
||||
|
||||
const DailyPercentTooltip = (props: TooltipProps<Date, HistoryPoint>) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const d = xScale.invert(mouseX)
|
||||
const { data, x, xScale } = props
|
||||
const d = xScale.invert(x)
|
||||
return (
|
||||
<Row className="items-center gap-2">
|
||||
<span className="font-semibold">{dayjs(d).format('MMM DD')}</span>
|
||||
|
|
|
@ -138,16 +138,6 @@ export function CommentInputTextArea(props: {
|
|||
<LoadingIndicator spinnerClassName="border-gray-500" />
|
||||
)}
|
||||
</TextEditor>
|
||||
<Row>
|
||||
{!user && (
|
||||
<button
|
||||
className="btn btn-outline btn-sm mt-2 normal-case"
|
||||
onClick={submitComment}
|
||||
>
|
||||
Add my comment
|
||||
</button>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,11 +16,11 @@ export function ConfirmationButton(props: {
|
|||
}
|
||||
cancelBtn?: {
|
||||
label?: string
|
||||
className?: string
|
||||
color?: ColorType
|
||||
}
|
||||
submitBtn?: {
|
||||
label?: string
|
||||
className?: string
|
||||
color?: ColorType
|
||||
isSubmitting?: boolean
|
||||
}
|
||||
children: ReactNode
|
||||
|
@ -53,14 +53,14 @@ export function ConfirmationButton(props: {
|
|||
<Col className="gap-4 rounded-md bg-white px-8 py-6">
|
||||
{children}
|
||||
<Row className="gap-4">
|
||||
<div
|
||||
className={clsx('btn', cancelBtn?.className)}
|
||||
<Button
|
||||
color={cancelBtn?.color ?? 'gray-white'}
|
||||
onClick={() => updateOpen(false)}
|
||||
>
|
||||
{cancelBtn?.label ?? 'Cancel'}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
className={clsx('btn', submitBtn?.className)}
|
||||
color={submitBtn?.color ?? 'blue'}
|
||||
onClick={
|
||||
onSubmitWithSuccess
|
||||
? () =>
|
||||
|
@ -100,18 +100,11 @@ export function ResolveConfirmationButton(props: {
|
|||
onResolve: () => void
|
||||
isSubmitting: boolean
|
||||
openModalButtonClass?: string
|
||||
submitButtonClass?: string
|
||||
color?: ColorType
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const {
|
||||
onResolve,
|
||||
isSubmitting,
|
||||
openModalButtonClass,
|
||||
submitButtonClass,
|
||||
color,
|
||||
disabled,
|
||||
} = props
|
||||
const { onResolve, isSubmitting, openModalButtonClass, color, disabled } =
|
||||
props
|
||||
return (
|
||||
<ConfirmationButton
|
||||
openModalBtn={{
|
||||
|
@ -126,7 +119,7 @@ export function ResolveConfirmationButton(props: {
|
|||
}}
|
||||
submitBtn={{
|
||||
label: 'Resolve',
|
||||
className: clsx('border-none', submitButtonClass),
|
||||
color: color,
|
||||
isSubmitting,
|
||||
}}
|
||||
onSubmit={onResolve}
|
||||
|
|
|
@ -446,7 +446,7 @@ function ContractSearchControls(props: {
|
|||
className="input input-bordered w-full"
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
{!isMobile && (
|
||||
{!isMobile && !query && (
|
||||
<SearchFilters
|
||||
filter={filter}
|
||||
selectFilter={selectFilter}
|
||||
|
@ -457,7 +457,7 @@ function ContractSearchControls(props: {
|
|||
includeProbSorts={includeProbSorts}
|
||||
/>
|
||||
)}
|
||||
{isMobile && (
|
||||
{isMobile && !query && (
|
||||
<>
|
||||
<MobileSearchBar
|
||||
children={
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
BinaryContract,
|
||||
Contract,
|
||||
CPMMBinaryContract,
|
||||
CPMMContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
NumericContract,
|
||||
|
@ -35,6 +36,7 @@ import { getMappedValue } from 'common/pseudo-numeric'
|
|||
import { Tooltip } from '../tooltip'
|
||||
import { SiteLink } from '../site-link'
|
||||
import { ProbChange } from './prob-change-table'
|
||||
import { Card } from '../card'
|
||||
|
||||
export function ContractCard(props: {
|
||||
contract: Contract
|
||||
|
@ -75,12 +77,7 @@ export function ContractCard(props: {
|
|||
!hideQuickBet
|
||||
|
||||
return (
|
||||
<Row
|
||||
className={clsx(
|
||||
'group relative gap-3 rounded-lg bg-white shadow-md hover:cursor-pointer hover:bg-gray-100',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Card className={clsx('group relative flex gap-3', className)}>
|
||||
<Col className="relative flex-1 gap-3 py-4 pb-12 pl-6">
|
||||
<AvatarDetails
|
||||
contract={contract}
|
||||
|
@ -195,7 +192,7 @@ export function ContractCard(props: {
|
|||
/>
|
||||
</Link>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -391,7 +388,7 @@ export function PseudoNumericResolutionOrExpectation(props: {
|
|||
}
|
||||
|
||||
export function ContractCardProbChange(props: {
|
||||
contract: CPMMBinaryContract
|
||||
contract: CPMMContract
|
||||
noLinkAvatar?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
|
@ -399,12 +396,7 @@ export function ContractCardProbChange(props: {
|
|||
const contract = useContractWithPreload(props.contract) as CPMMBinaryContract
|
||||
|
||||
return (
|
||||
<Col
|
||||
className={clsx(
|
||||
className,
|
||||
'mb-4 rounded-lg bg-white shadow hover:bg-gray-100 hover:shadow-lg'
|
||||
)}
|
||||
>
|
||||
<Card className={clsx(className, 'mb-4')}>
|
||||
<AvatarDetails
|
||||
contract={contract}
|
||||
className={'px-6 pt-4'}
|
||||
|
@ -419,6 +411,6 @@ export function ContractCardProbChange(props: {
|
|||
</SiteLink>
|
||||
<ProbChange className="py-2 pr-4" contract={contract} />
|
||||
</Row>
|
||||
</Col>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -357,7 +357,7 @@ export function GroupDisplay(props: {
|
|||
const groupSection = (
|
||||
<a
|
||||
className={clsx(
|
||||
'bg-greyscale-4 max-w-[140px] truncate whitespace-nowrap rounded-full py-0.5 px-2 text-xs text-white sm:max-w-[250px]',
|
||||
'bg-greyscale-4 max-w-[200px] truncate whitespace-nowrap rounded-full py-0.5 px-2 text-xs text-white sm:max-w-[250px]',
|
||||
!disabled && 'hover:bg-greyscale-3 cursor-pointer'
|
||||
)}
|
||||
>
|
||||
|
@ -437,14 +437,14 @@ function EditableCloseDate(props: {
|
|||
return (
|
||||
<>
|
||||
<Modal
|
||||
size="sm"
|
||||
size="md"
|
||||
open={isEditingCloseTime}
|
||||
setOpen={setIsEditingCloseTime}
|
||||
position="top"
|
||||
>
|
||||
<Col className="rounded bg-white px-8 pb-8">
|
||||
<Subtitle text="Edit Close Date" />
|
||||
<Row className="z-10 mr-2 w-full shrink-0 flex-wrap items-center gap-2">
|
||||
<Subtitle text="Edit market close time" />
|
||||
<Row className="z-10 mr-2 mt-4 w-full shrink-0 flex-wrap items-center gap-2">
|
||||
<input
|
||||
type="date"
|
||||
className="input input-bordered w-full shrink-0 sm:w-fit"
|
||||
|
@ -461,22 +461,18 @@ function EditableCloseDate(props: {
|
|||
min="00:00"
|
||||
value={closeHoursMinutes}
|
||||
/>
|
||||
<Button size={'xs'} color={'indigo'} onClick={() => onSave()}>
|
||||
Set
|
||||
</Button>
|
||||
</Row>
|
||||
|
||||
<Button
|
||||
className="mt-4"
|
||||
className="mt-8"
|
||||
size={'xs'}
|
||||
color={'indigo'}
|
||||
onClick={() => onSave()}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
<Button
|
||||
className="mt-4"
|
||||
size={'xs'}
|
||||
color={'gray-white'}
|
||||
color="red"
|
||||
onClick={() => onSave(Date.now())}
|
||||
>
|
||||
Close Now
|
||||
Close market now
|
||||
</Button>
|
||||
</Col>
|
||||
</Modal>
|
||||
|
|
|
@ -17,7 +17,7 @@ import { SiteLink } from '../site-link'
|
|||
import { firestoreConsolePath } from 'common/envs/constants'
|
||||
import { deleteField } from 'firebase/firestore'
|
||||
import ShortToggle from '../widgets/short-toggle'
|
||||
import { DuplicateContractButton } from '../copy-contract-button'
|
||||
import { DuplicateContractButton } from '../duplicate-contract-button'
|
||||
import { Row } from '../layout/row'
|
||||
import { BETTORS, User } from 'common/user'
|
||||
import { Button } from '../button'
|
||||
|
|
54
web/components/contract/contract-mention.tsx
Normal file
54
web/components/contract/contract-mention.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import clsx from 'clsx'
|
||||
import { Contract } from 'common/contract'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import Link from 'next/link'
|
||||
import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
||||
import { fromNow } from 'web/lib/util/time'
|
||||
import { BinaryContractOutcomeLabel } from '../outcome-label'
|
||||
import { getColor } from './quick-bet'
|
||||
|
||||
export function ContractMention(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
const { outcomeType, resolution } = contract
|
||||
const probTextColor = `text-${getColor(contract)}`
|
||||
|
||||
return (
|
||||
<Link href={contractPath(contract)}>
|
||||
<a
|
||||
className="group inline whitespace-nowrap rounded-sm hover:bg-indigo-50 focus:bg-indigo-50"
|
||||
title={tooltipLabel(contract)}
|
||||
>
|
||||
<span className="break-anywhere mr-0.5 whitespace-normal font-normal text-indigo-700">
|
||||
{contract.question}
|
||||
</span>
|
||||
{outcomeType === 'BINARY' && (
|
||||
<span
|
||||
className={clsx(
|
||||
probTextColor,
|
||||
'rounded-full px-2 font-semibold ring-1 ring-inset ring-indigo-100 group-hover:ring-indigo-200'
|
||||
)}
|
||||
>
|
||||
{resolution ? (
|
||||
<BinaryContractOutcomeLabel
|
||||
contract={contract}
|
||||
resolution={resolution}
|
||||
/>
|
||||
) : (
|
||||
getBinaryProbPercent(contract)
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{/* TODO: numeric? */}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function tooltipLabel(contract: Contract) {
|
||||
const { resolutionTime, creatorName, volume, closeTime = 0 } = contract
|
||||
const dateFormat = resolutionTime
|
||||
? `Resolved ${fromNow(resolutionTime)}`
|
||||
: `${closeTime < Date.now() ? 'Closed' : 'Closes'} ${fromNow(closeTime)}`
|
||||
|
||||
return `By ${creatorName}. ${formatMoney(volume)} bet. ${dateFormat}`
|
||||
}
|
|
@ -48,15 +48,8 @@ export function ContractReportResolution(props: { contract: Contract }) {
|
|||
openModalBtn={{
|
||||
label: '',
|
||||
icon: <FlagIcon className="h-5 w-5" />,
|
||||
className: clsx(flagClass, reporting && 'btn-disabled loading'),
|
||||
}}
|
||||
cancelBtn={{
|
||||
label: 'Cancel',
|
||||
className: 'border-none btn-sm btn-ghost self-center',
|
||||
}}
|
||||
submitBtn={{
|
||||
label: 'Submit',
|
||||
className: 'btn-secondary',
|
||||
disabled: reporting,
|
||||
className: clsx(flagClass),
|
||||
}}
|
||||
onSubmitWithSuccess={onSubmit}
|
||||
disabled={userReported}
|
||||
|
|
|
@ -45,7 +45,7 @@ export function ContractsGrid(props: {
|
|||
cardUIOptions || {}
|
||||
const { itemIds: contractIds, highlightClassName } = highlightOptions || {}
|
||||
const onVisibilityUpdated = useCallback(
|
||||
(visible) => {
|
||||
(visible: boolean) => {
|
||||
if (visible && loadMore) {
|
||||
loadMore()
|
||||
}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { sortBy } from 'lodash'
|
||||
import clsx from 'clsx'
|
||||
import { contractPath } from 'web/lib/firebase/contracts'
|
||||
import { CPMMContract } from 'common/contract'
|
||||
import { formatPercent } from 'common/util/format'
|
||||
import { SiteLink } from '../site-link'
|
||||
import { sortBy } from 'lodash'
|
||||
import { Col } from '../layout/col'
|
||||
import { Row } from '../layout/row'
|
||||
import { LoadingIndicator } from '../loading-indicator'
|
||||
import { useContractWithPreload } from 'web/hooks/use-contract'
|
||||
import { ContractCardProbChange } from './contract-card'
|
||||
|
||||
export function ProbChangeTable(props: {
|
||||
changes: CPMMContract[] | undefined
|
||||
|
@ -39,46 +36,21 @@ export function ProbChangeTable(props: {
|
|||
if (rows === 0) return <div className="px-4 text-gray-500">None</div>
|
||||
|
||||
return (
|
||||
<Col className="mb-4 w-full divide-x-2 divide-y rounded-lg bg-white shadow-md md:flex-row md:divide-y-0">
|
||||
<Col className="flex-1 divide-y">
|
||||
<Col className="mb-4 w-full gap-4 rounded-lg md:flex-row">
|
||||
<Col className="flex-1 gap-4">
|
||||
{filteredPositiveChanges.map((contract) => (
|
||||
<ProbChangeRow key={contract.id} contract={contract} />
|
||||
<ContractCardProbChange key={contract.id} contract={contract} />
|
||||
))}
|
||||
</Col>
|
||||
<Col className="flex-1 divide-y">
|
||||
<Col className="flex-1 gap-4">
|
||||
{filteredNegativeChanges.map((contract) => (
|
||||
<ProbChangeRow key={contract.id} contract={contract} />
|
||||
<ContractCardProbChange key={contract.id} contract={contract} />
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProbChangeRow(props: {
|
||||
contract: CPMMContract
|
||||
className?: string
|
||||
}) {
|
||||
const { className } = props
|
||||
const contract =
|
||||
(useContractWithPreload(props.contract) as CPMMContract) ?? props.contract
|
||||
return (
|
||||
<Row
|
||||
className={clsx(
|
||||
'items-center justify-between gap-4 hover:bg-gray-100',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<SiteLink
|
||||
className="p-4 pr-0 font-semibold text-indigo-700"
|
||||
href={contractPath(contract)}
|
||||
>
|
||||
<span className="line-clamp-2">{contract.question}</span>
|
||||
</SiteLink>
|
||||
<ProbChange className="py-2 pr-4 text-xl" contract={contract} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProbChange(props: {
|
||||
contract: CPMMContract
|
||||
className?: string
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import { LinkIcon } from '@heroicons/react/outline'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { Contract } from 'common/contract'
|
||||
import { contractPath } from 'web/lib/firebase/contracts'
|
||||
import { Col } from '../layout/col'
|
||||
|
@ -9,9 +6,7 @@ import { Row } from '../layout/row'
|
|||
import { ShareEmbedButton } from '../share-embed-button'
|
||||
import { Title } from '../title'
|
||||
import { TweetButton } from '../tweet-button'
|
||||
import { Button } from '../button'
|
||||
import { copyToClipboard } from 'web/lib/util/copy'
|
||||
import { track, withTracking } from 'web/lib/service/analytics'
|
||||
import { withTracking } from 'web/lib/service/analytics'
|
||||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
import { User } from 'common/user'
|
||||
import { SiteLink } from '../site-link'
|
||||
|
@ -22,6 +17,8 @@ import { useState } from 'react'
|
|||
import { CHALLENGES_ENABLED } from 'common/challenge'
|
||||
import ChallengeIcon from 'web/lib/icons/challenge-icon'
|
||||
import { QRCode } from '../qr-code'
|
||||
import { CopyLinkButton } from '../copy-link-button'
|
||||
import { Button } from '../button'
|
||||
|
||||
export function ShareModal(props: {
|
||||
contract: Contract
|
||||
|
@ -34,7 +31,6 @@ export function ShareModal(props: {
|
|||
|
||||
const [openCreateChallengeModal, setOpenCreateChallengeModal] =
|
||||
useState(false)
|
||||
const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" />
|
||||
const showChallenge =
|
||||
user && outcomeType === 'BINARY' && !resolution && CHALLENGES_ENABLED
|
||||
|
||||
|
@ -61,40 +57,24 @@ export function ShareModal(props: {
|
|||
width={150}
|
||||
height={150}
|
||||
/>
|
||||
<Button
|
||||
size="2xl"
|
||||
color="indigo"
|
||||
className={'mb-2 flex max-w-xs self-center'}
|
||||
onClick={() => {
|
||||
copyToClipboard(shareUrl)
|
||||
toast.success('Link copied!', {
|
||||
icon: linkIcon,
|
||||
})
|
||||
track('copy share link')
|
||||
}}
|
||||
>
|
||||
{linkIcon} Copy link
|
||||
</Button>
|
||||
<CopyLinkButton url={shareUrl} tracking="copy share link" />
|
||||
|
||||
<Row className="z-0 flex-wrap justify-center gap-4 self-center">
|
||||
<TweetButton
|
||||
className="self-start"
|
||||
tweetText={getTweetText(contract, shareUrl)}
|
||||
/>
|
||||
<TweetButton tweetText={getTweetText(contract, shareUrl)} />
|
||||
|
||||
<ShareEmbedButton contract={contract} />
|
||||
|
||||
{showChallenge && (
|
||||
<button
|
||||
className={
|
||||
'btn btn-xs flex-nowrap border-2 !border-indigo-500 !bg-white normal-case text-indigo-500'
|
||||
}
|
||||
<Button
|
||||
size="2xs"
|
||||
color="override"
|
||||
className="gap-1 border-2 border-indigo-500 text-indigo-500 hover:bg-indigo-500 hover:text-white"
|
||||
onClick={withTracking(
|
||||
() => setOpenCreateChallengeModal(true),
|
||||
'click challenge button'
|
||||
)}
|
||||
>
|
||||
️<ChallengeIcon className="mr-1 h-4 w-4" /> Challenge
|
||||
<ChallengeIcon className="h-4 w-4" /> Challenge
|
||||
<CreateChallengeModal
|
||||
isOpen={openCreateChallengeModal}
|
||||
setOpen={(open) => {
|
||||
|
@ -106,7 +86,7 @@ export function ShareModal(props: {
|
|||
user={user}
|
||||
contract={contract}
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
</Col>
|
||||
|
|
|
@ -21,10 +21,11 @@ export function CopyLinkButton(props: {
|
|||
return (
|
||||
<Row className="w-full">
|
||||
<input
|
||||
className="input input-bordered flex-1 rounded-r-none text-gray-500"
|
||||
className="block w-full rounded-none rounded-l-md border-gray-300 text-gray-400 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||
readOnly
|
||||
type="text"
|
||||
value={displayUrl ?? url}
|
||||
onFocus={(e) => e.target.select()}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
|
@ -37,7 +38,7 @@ export function CopyLinkButton(props: {
|
|||
>
|
||||
<Menu.Button
|
||||
className={clsx(
|
||||
'btn btn-xs border-2 border-green-600 bg-white normal-case text-green-600 hover:border-green-600 hover:bg-white',
|
||||
'relative -ml-px inline-flex items-center space-x-2 rounded-r-md border border-gray-300 bg-gray-50 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500',
|
||||
buttonClassName
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -13,6 +13,8 @@ import { Group } from 'common/group'
|
|||
|
||||
export function CreatePost(props: { group?: Group }) {
|
||||
const [title, setTitle] = useState('')
|
||||
const [subtitle, setSubtitle] = useState('')
|
||||
|
||||
const [error, setError] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
|
@ -22,12 +24,17 @@ export function CreatePost(props: { group?: Group }) {
|
|||
disabled: isSubmitting,
|
||||
})
|
||||
|
||||
const isValid = editor && title.length > 0 && editor.isEmpty === false
|
||||
const isValid =
|
||||
editor &&
|
||||
title.length > 0 &&
|
||||
subtitle.length > 0 &&
|
||||
editor.isEmpty === false
|
||||
|
||||
async function savePost(title: string) {
|
||||
if (!editor) return
|
||||
const newPost = {
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
content: editor.getJSON(),
|
||||
groupId: group?.id,
|
||||
}
|
||||
|
@ -62,6 +69,20 @@ export function CreatePost(props: { group?: Group }) {
|
|||
onChange={(e) => setTitle(e.target.value || '')}
|
||||
/>
|
||||
<Spacer h={6} />
|
||||
<label className="label">
|
||||
<span className="mb-1">
|
||||
Subtitle<span className={'text-red-700'}> *</span>
|
||||
</span>
|
||||
</label>
|
||||
<Textarea
|
||||
placeholder="e.g. How Elon Musk is getting everyone's attention"
|
||||
className="input input-bordered resize-none"
|
||||
autoFocus
|
||||
maxLength={MAX_POST_TITLE_LENGTH}
|
||||
value={subtitle}
|
||||
onChange={(e) => setSubtitle(e.target.value || '')}
|
||||
/>
|
||||
<Spacer h={6} />
|
||||
<label className="label">
|
||||
<span className="mb-1">
|
||||
Content<span className={'text-red-700'}> *</span>
|
||||
|
|
|
@ -3,27 +3,22 @@ import clsx from 'clsx'
|
|||
import { Contract } from 'common/contract'
|
||||
import { getMappedValue } from 'common/pseudo-numeric'
|
||||
import { trackCallback } from 'web/lib/service/analytics'
|
||||
import { buttonClass } from './button'
|
||||
|
||||
export function DuplicateContractButton(props: {
|
||||
contract: Contract
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, className } = props
|
||||
export function DuplicateContractButton(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
|
||||
return (
|
||||
<a
|
||||
className={clsx('btn btn-xs flex-nowrap normal-case', className)}
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
border: '2px solid #a78bfa',
|
||||
// violet-400
|
||||
color: '#a78bfa',
|
||||
}}
|
||||
className={clsx(
|
||||
buttonClass('2xs', 'override'),
|
||||
'gap-1 border-2 border-violet-400 text-violet-400 hover:bg-violet-400 hover:text-white'
|
||||
)}
|
||||
href={duplicateContractHref(contract)}
|
||||
onClick={trackCallback('duplicate market')}
|
||||
target="_blank"
|
||||
>
|
||||
<DuplicateIcon className="mr-1.5 h-4 w-4" aria-hidden="true" />
|
||||
<DuplicateIcon className="h-4 w-4" aria-hidden="true" />
|
||||
<div>Duplicate</div>
|
||||
</a>
|
||||
)
|
||||
|
@ -40,6 +35,7 @@ function duplicateContractHref(contract: Contract) {
|
|||
closeTime,
|
||||
description: descriptionString,
|
||||
outcomeType: contract.outcomeType,
|
||||
visibility: contract.visibility,
|
||||
} as Record<string, any>
|
||||
|
||||
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
|
|
@ -319,12 +319,12 @@ const useUploadMutation = (editor: Editor | null) =>
|
|||
{
|
||||
onSuccess(urls) {
|
||||
if (!editor) return
|
||||
let trans = editor.view.state.tr
|
||||
urls.forEach((src: any) => {
|
||||
const node = editor.view.state.schema.nodes.image.create({ src })
|
||||
trans = trans.insert(editor.view.state.selection.to, node)
|
||||
let trans = editor.chain().focus()
|
||||
urls.forEach((src) => {
|
||||
trans = trans.createParagraphNear()
|
||||
trans = trans.setImage({ src })
|
||||
})
|
||||
editor.view.dispatch(trans)
|
||||
trans.run()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from '@tiptap/react'
|
||||
import clsx from 'clsx'
|
||||
import { useContract } from 'web/hooks/use-contract'
|
||||
import { ContractCard } from '../contract/contract-card'
|
||||
import { ContractMention } from '../contract/contract-mention'
|
||||
|
||||
const name = 'contract-mention-component'
|
||||
|
||||
|
@ -14,13 +14,8 @@ const ContractMentionComponent = (props: any) => {
|
|||
const contract = useContract(props.node.attrs.id)
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={clsx(name, 'not-prose')}>
|
||||
{contract && (
|
||||
<ContractCard
|
||||
contract={contract}
|
||||
className="my-2 w-full border border-gray-100"
|
||||
/>
|
||||
)}
|
||||
<NodeViewWrapper className={clsx(name, 'not-prose inline')}>
|
||||
{contract && <ContractMention contract={contract} />}
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
}
|
||||
|
@ -37,6 +32,5 @@ export const DisplayContractMention = Mention.extend({
|
|||
addNodeView: () =>
|
||||
ReactNodeViewRenderer(ContractMentionComponent, {
|
||||
// On desktop, render cards below half-width so you can stack two
|
||||
className: 'inline-block sm:w-[calc(50%-1rem)] sm:mr-1',
|
||||
}),
|
||||
})
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Answer } from 'common/answer'
|
|||
import { FreeResponseContract } from 'common/contract'
|
||||
import { ContractComment } from 'common/comment'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { sum } from 'lodash'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { Avatar } from 'web/components/avatar'
|
||||
|
@ -14,6 +15,8 @@ import {
|
|||
} from 'web/components/feed/feed-comments'
|
||||
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { useEvent } from 'web/hooks/use-event'
|
||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||
import { UserLink } from 'web/components/user-link'
|
||||
|
||||
|
@ -27,11 +30,17 @@ export function FeedAnswerCommentGroup(props: {
|
|||
const { username, avatarUrl, name, text } = answer
|
||||
|
||||
const [replyTo, setReplyTo] = useState<ReplyTo>()
|
||||
const user = useUser()
|
||||
const router = useRouter()
|
||||
const answerElementId = `answer-${answer.id}`
|
||||
const highlighted = router.asPath.endsWith(`#${answerElementId}`)
|
||||
const answerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const onSubmitComment = useEvent(() => setReplyTo(undefined))
|
||||
const onReplyClick = useEvent((comment: ContractComment) => {
|
||||
setReplyTo({ id: comment.id, username: comment.userUsername })
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (highlighted && answerRef.current != null) {
|
||||
answerRef.current.scrollIntoView(true)
|
||||
|
@ -95,10 +104,10 @@ export function FeedAnswerCommentGroup(props: {
|
|||
indent={true}
|
||||
contract={contract}
|
||||
comment={comment}
|
||||
tips={tips[comment.id] ?? {}}
|
||||
onReplyClick={() =>
|
||||
setReplyTo({ id: comment.id, username: comment.userUsername })
|
||||
}
|
||||
myTip={user ? tips[comment.id]?.[user.id] : undefined}
|
||||
totalTip={sum(Object.values(tips[comment.id] ?? {}))}
|
||||
showTip={true}
|
||||
onReplyClick={onReplyClick}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
|
@ -112,7 +121,7 @@ export function FeedAnswerCommentGroup(props: {
|
|||
contract={contract}
|
||||
parentAnswerOutcome={answer.number.toString()}
|
||||
replyTo={replyTo}
|
||||
onSubmitComment={() => setReplyTo(undefined)}
|
||||
onSubmitComment={onSubmitComment}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import React, { memo, useEffect } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { Contract } from 'common/contract'
|
||||
import { Bet } from 'common/bet'
|
||||
|
@ -8,7 +9,6 @@ import clsx from 'clsx'
|
|||
import { formatMoney, formatPercent } from 'common/util/format'
|
||||
import { OutcomeLabel } from 'web/components/outcome-label'
|
||||
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
||||
import React, { useEffect } from 'react'
|
||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||
import { SiteLink } from 'web/components/site-link'
|
||||
import { getChallenge, getChallengeUrl } from 'web/lib/firebase/challenges'
|
||||
|
@ -16,7 +16,10 @@ import { Challenge } from 'common/challenge'
|
|||
import { UserLink } from 'web/components/user-link'
|
||||
import { BETTOR } from 'common/user'
|
||||
|
||||
export function FeedBet(props: { contract: Contract; bet: Bet }) {
|
||||
export const FeedBet = memo(function FeedBet(props: {
|
||||
contract: Contract
|
||||
bet: Bet
|
||||
}) {
|
||||
const { contract, bet } = props
|
||||
const { userAvatarUrl, userUsername, createdTime } = bet
|
||||
const showUser = dayjs(createdTime).isAfter('2022-06-01')
|
||||
|
@ -36,7 +39,7 @@ export function FeedBet(props: { contract: Contract; bet: Bet }) {
|
|||
/>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export function BetStatusText(props: {
|
||||
contract: Contract
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import React, { memo, useEffect, useRef, useState } from 'react'
|
||||
import { Editor } from '@tiptap/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { sum } from 'lodash'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { ContractComment } from 'common/comment'
|
||||
import { Contract } from 'common/contract'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import clsx from 'clsx'
|
||||
import { Avatar } from 'web/components/avatar'
|
||||
import { OutcomeLabel } from 'web/components/outcome-label'
|
||||
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
||||
|
@ -14,9 +17,9 @@ import { createCommentOnContract } from 'web/lib/firebase/comments'
|
|||
import { Col } from 'web/components/layout/col'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { Tipper } from '../tipper'
|
||||
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
|
||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||
import { useEvent } from 'web/hooks/use-event'
|
||||
import { Content } from '../editor'
|
||||
import { Editor } from '@tiptap/react'
|
||||
import { UserLink } from 'web/components/user-link'
|
||||
import { CommentInput } from '../comment-input'
|
||||
import { AwardBountyButton } from 'web/components/award-bounty-button'
|
||||
|
@ -32,6 +35,12 @@ export function FeedCommentThread(props: {
|
|||
const { contract, threadComments, tips, parentComment } = props
|
||||
const [replyTo, setReplyTo] = useState<ReplyTo>()
|
||||
|
||||
const user = useUser()
|
||||
const onSubmitComment = useEvent(() => setReplyTo(undefined))
|
||||
const onReplyClick = useEvent((comment: ContractComment) => {
|
||||
setReplyTo({ id: comment.id, username: comment.userUsername })
|
||||
})
|
||||
|
||||
return (
|
||||
<Col className="relative w-full items-stretch gap-3 pb-4">
|
||||
<span
|
||||
|
@ -44,10 +53,10 @@ export function FeedCommentThread(props: {
|
|||
indent={commentIdx != 0}
|
||||
contract={contract}
|
||||
comment={comment}
|
||||
tips={tips[comment.id] ?? {}}
|
||||
onReplyClick={() =>
|
||||
setReplyTo({ id: comment.id, username: comment.userUsername })
|
||||
}
|
||||
myTip={user ? tips[comment.id]?.[user.id] : undefined}
|
||||
totalTip={sum(Object.values(tips[comment.id] ?? {}))}
|
||||
showTip={true}
|
||||
onReplyClick={onReplyClick}
|
||||
/>
|
||||
))}
|
||||
{replyTo && (
|
||||
|
@ -60,7 +69,7 @@ export function FeedCommentThread(props: {
|
|||
contract={contract}
|
||||
parentCommentId={parentComment.id}
|
||||
replyTo={replyTo}
|
||||
onSubmitComment={() => setReplyTo(undefined)}
|
||||
onSubmitComment={onSubmitComment}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
|
@ -68,14 +77,17 @@ export function FeedCommentThread(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export function FeedComment(props: {
|
||||
export const FeedComment = memo(function FeedComment(props: {
|
||||
contract: Contract
|
||||
comment: ContractComment
|
||||
tips?: CommentTips
|
||||
showTip?: boolean
|
||||
myTip?: number
|
||||
totalTip?: number
|
||||
indent?: boolean
|
||||
onReplyClick?: () => void
|
||||
onReplyClick?: (comment: ContractComment) => void
|
||||
}) {
|
||||
const { contract, comment, tips, indent, onReplyClick } = props
|
||||
const { contract, comment, myTip, totalTip, showTip, indent, onReplyClick } =
|
||||
props
|
||||
const {
|
||||
text,
|
||||
content,
|
||||
|
@ -180,12 +192,18 @@ export function FeedComment(props: {
|
|||
{onReplyClick && (
|
||||
<button
|
||||
className="font-bold hover:underline"
|
||||
onClick={onReplyClick}
|
||||
onClick={() => onReplyClick(comment)}
|
||||
>
|
||||
Reply
|
||||
</button>
|
||||
)}
|
||||
{tips && <Tipper comment={comment} tips={tips} />}
|
||||
{showTip && (
|
||||
<Tipper
|
||||
comment={comment}
|
||||
myTip={myTip ?? 0}
|
||||
totalTip={totalTip ?? 0}
|
||||
/>
|
||||
)}
|
||||
{(contract.openCommentBounties ?? 0) > 0 && (
|
||||
<AwardBountyButton comment={comment} contract={contract} />
|
||||
)}
|
||||
|
@ -193,7 +211,7 @@ export function FeedComment(props: {
|
|||
</div>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function CommentStatus(props: {
|
||||
contract: Contract
|
||||
|
|
|
@ -53,30 +53,30 @@ export function ContractGroupsList(props: {
|
|||
/>
|
||||
</Col>
|
||||
)}
|
||||
{groups.length === 0 && (
|
||||
<Col className="ml-2 h-full justify-center text-gray-500">
|
||||
No groups yet...
|
||||
</Col>
|
||||
)}
|
||||
{groups.map((group) => (
|
||||
<Row
|
||||
key={group.id}
|
||||
className={clsx('items-center justify-between gap-2 p-2')}
|
||||
>
|
||||
<Row className="line-clamp-1 items-center gap-2">
|
||||
<GroupLinkItem group={group} />
|
||||
<Col className="h-96 overflow-auto">
|
||||
{groups.length === 0 && (
|
||||
<Col className="text-greyscale-4">No groups yet...</Col>
|
||||
)}
|
||||
{groups.map((group) => (
|
||||
<Row
|
||||
key={group.id}
|
||||
className={clsx('items-center justify-between gap-2 p-2')}
|
||||
>
|
||||
<Row className="line-clamp-1 items-center gap-2">
|
||||
<GroupLinkItem group={group} />
|
||||
</Row>
|
||||
{user && canModifyGroupContracts(group, user.id) && (
|
||||
<Button
|
||||
color={'gray-white'}
|
||||
size={'xs'}
|
||||
onClick={() => removeContractFromGroup(group, contract)}
|
||||
>
|
||||
<XIcon className="text-greyscale-4 h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
{user && canModifyGroupContracts(group, user.id) && (
|
||||
<Button
|
||||
color={'gray-white'}
|
||||
size={'xs'}
|
||||
onClick={() => removeContractFromGroup(group, contract)}
|
||||
>
|
||||
<XIcon className="h-4 w-4 text-gray-500" />
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
))}
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import clsx from 'clsx'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import { groupPath } from 'web/lib/firebase/groups'
|
||||
|
@ -87,10 +86,8 @@ export function CreateGroupButton(props: {
|
|||
}}
|
||||
submitBtn={{
|
||||
label: 'Create',
|
||||
className: clsx(
|
||||
'normal-case',
|
||||
isSubmitting ? 'loading btn-disabled' : ' btn-primary'
|
||||
),
|
||||
color: 'green',
|
||||
isSubmitting,
|
||||
}}
|
||||
onSubmitWithSuccess={onSubmit}
|
||||
onOpenChanged={(isOpen) => {
|
||||
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx'
|
|||
import { PencilIcon } from '@heroicons/react/outline'
|
||||
|
||||
import { Group } from 'common/group'
|
||||
import { deleteGroup, joinGroup } from 'web/lib/firebase/groups'
|
||||
import { deleteGroup, joinGroup, updateGroup } from 'web/lib/firebase/groups'
|
||||
import { Spacer } from '../layout/spacer'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Modal } from 'web/components/layout/modal'
|
||||
|
@ -31,6 +31,7 @@ export function EditGroupButton(props: { group: Group; className?: string }) {
|
|||
setIsSubmitting(true)
|
||||
|
||||
await Promise.all(addMemberUsers.map((user) => joinGroup(group, user.id)))
|
||||
await updateGroup(group, { name })
|
||||
|
||||
setIsSubmitting(false)
|
||||
updateOpen(false)
|
||||
|
|
|
@ -6,12 +6,13 @@ import { Spacer } from '../layout/spacer'
|
|||
import { Group } from 'common/group'
|
||||
import { deleteFieldFromGroup, updateGroup } from 'web/lib/firebase/groups'
|
||||
import PencilIcon from '@heroicons/react/solid/PencilIcon'
|
||||
import { DocumentRemoveIcon } from '@heroicons/react/solid'
|
||||
import { TrashIcon } from '@heroicons/react/solid'
|
||||
import { createPost } from 'web/lib/firebase/api'
|
||||
import { Post } from 'common/post'
|
||||
import { deletePost, updatePost } from 'web/lib/firebase/posts'
|
||||
import { useState } from 'react'
|
||||
import { usePost } from 'web/hooks/use-post'
|
||||
import { Col } from '../layout/col'
|
||||
|
||||
export function GroupOverviewPost(props: {
|
||||
group: Group
|
||||
|
@ -43,6 +44,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {
|
|||
if (!editor) return
|
||||
const newPost = {
|
||||
title: group.name,
|
||||
subtitle: 'About post for the group',
|
||||
content: editor.getJSON(),
|
||||
}
|
||||
|
||||
|
@ -98,35 +100,31 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {
|
|||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<div className="absolute top-0 right-0 z-10 space-x-2">
|
||||
<Col>
|
||||
<Content content={post.content} />
|
||||
<Row className="place-content-end">
|
||||
<Button
|
||||
color="gray"
|
||||
size="xs"
|
||||
color="gray-white"
|
||||
size="2xs"
|
||||
onClick={() => {
|
||||
setEditing(true)
|
||||
editor?.commands.focus('end')
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="inline h-4 w-4" />
|
||||
Edit
|
||||
<PencilIcon className="inline h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="gray"
|
||||
size="xs"
|
||||
color="gray-white"
|
||||
size="2xs"
|
||||
onClick={() => {
|
||||
deleteGroupAboutPost()
|
||||
}}
|
||||
>
|
||||
<DocumentRemoveIcon className="inline h-4 w-4" />
|
||||
Delete
|
||||
<TrashIcon className="inline h-5 w-5 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Content content={post.content} />
|
||||
<Spacer h={2} />
|
||||
</div>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -36,8 +36,11 @@ import { CopyLinkButton } from '../copy-link-button'
|
|||
import { REFERRAL_AMOUNT } from 'common/economy'
|
||||
import toast from 'react-hot-toast'
|
||||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
import { PostCard } from '../post-card'
|
||||
import { PostCard, PostCardList } from '../post-card'
|
||||
import { LoadingIndicator } from '../loading-indicator'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { CreatePost } from '../create-post'
|
||||
import { Modal } from '../layout/modal'
|
||||
|
||||
const MAX_TRENDING_POSTS = 6
|
||||
|
||||
|
@ -59,7 +62,6 @@ export function GroupOverview(props: {
|
|||
posts={posts}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
|
||||
{(group.aboutPostId != null || isEditable) && (
|
||||
<>
|
||||
<SectionHeader label={'About'} href={'/post/' + group.slug} />
|
||||
|
@ -87,10 +89,55 @@ export function GroupOverview(props: {
|
|||
user={user}
|
||||
memberIds={memberIds}
|
||||
/>
|
||||
|
||||
<GroupPosts group={group} posts={posts} />
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export function GroupPosts(props: { posts: Post[]; group: Group }) {
|
||||
const { posts, group } = props
|
||||
const [showCreatePost, setShowCreatePost] = useState(false)
|
||||
const user = useUser()
|
||||
|
||||
const createPost = (
|
||||
<Modal size="xl" open={showCreatePost} setOpen={setShowCreatePost}>
|
||||
<div className="w-full bg-white py-10">
|
||||
<CreatePost group={group} />
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
const postList = (
|
||||
<div className=" align-start w-full items-start">
|
||||
<Row className="flex justify-between">
|
||||
<Col>
|
||||
<SectionHeader label={'Latest Posts'} />
|
||||
</Col>
|
||||
<Col>
|
||||
{user && (
|
||||
<Button
|
||||
className="btn-md"
|
||||
onClick={() => setShowCreatePost(!showCreatePost)}
|
||||
>
|
||||
Add a Post
|
||||
</Button>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="mt-2">
|
||||
<PostCardList posts={posts} />
|
||||
{posts.length === 0 && (
|
||||
<div className="text-center text-gray-500">No posts yet</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return showCreatePost ? createPost : postList
|
||||
}
|
||||
|
||||
function GroupOverviewPinned(props: {
|
||||
group: Group
|
||||
posts: Post[]
|
||||
|
|
|
@ -163,7 +163,7 @@ export function GroupSelector(props: {
|
|||
user={creator}
|
||||
onOpenStateChange={setIsCreatingNewGroup}
|
||||
className={
|
||||
'w-full justify-start rounded-none border-0 bg-white pl-2 font-normal text-gray-900 hover:bg-indigo-500 hover:text-white'
|
||||
'flex w-full flex-row items-center justify-start rounded-none border-0 bg-white pl-2 font-normal text-gray-900 hover:bg-indigo-500 hover:text-white'
|
||||
}
|
||||
label={'Create a new Group'}
|
||||
addGroupIdParamOnSubmit
|
||||
|
|
|
@ -13,6 +13,7 @@ import { joinGroup, leaveGroup } from 'web/lib/firebase/groups'
|
|||
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||
import { GroupLinkItem } from 'web/pages/groups'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Button } from '../button'
|
||||
|
||||
export function GroupsButton(props: { user: User; className?: string }) {
|
||||
const { user, className } = props
|
||||
|
@ -92,23 +93,22 @@ export function JoinOrLeaveGroupButton(props: {
|
|||
group: Group
|
||||
isMember: boolean
|
||||
user: User | undefined | null
|
||||
small?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const { group, small, className, isMember, user } = props
|
||||
const smallStyle =
|
||||
'btn !btn-xs border-2 border-gray-500 bg-white normal-case text-gray-500 hover:border-gray-500 hover:bg-white hover:text-gray-500'
|
||||
const { group, className, isMember, user } = props
|
||||
|
||||
if (!user) {
|
||||
if (!group.anyoneCanJoin)
|
||||
return <div className={clsx(className, 'text-gray-500')}>Closed</div>
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
onClick={firebaseLogin}
|
||||
className={clsx('btn btn-sm', small && smallStyle, className)}
|
||||
className={className}
|
||||
>
|
||||
Login to follow
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const onJoinGroup = () => {
|
||||
|
@ -124,27 +124,27 @@ export function JoinOrLeaveGroupButton(props: {
|
|||
|
||||
if (isMember) {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-outline btn-xs',
|
||||
small && smallStyle,
|
||||
className
|
||||
)}
|
||||
<Button
|
||||
size="xs"
|
||||
color="gray-white"
|
||||
className={`${className} border-greyscale-4 border !border-solid`}
|
||||
onClick={withTracking(onLeaveGroup, 'leave group')}
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
if (!group.anyoneCanJoin)
|
||||
return <div className={clsx(className, 'text-gray-500')}>Closed</div>
|
||||
return (
|
||||
<button
|
||||
className={clsx('btn btn-sm', small && smallStyle, className)}
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
className={className}
|
||||
onClick={withTracking(onJoinGroup, 'join group')}
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -63,7 +63,8 @@ export default function Sidebar(props: {
|
|||
)}
|
||||
<Spacer h={6} />
|
||||
|
||||
{!user && <SignInButton className="mb-4" />}
|
||||
{user === undefined && <div className="h-[178px]" />}
|
||||
{user === null && <SignInButton className="mb-4" />}
|
||||
|
||||
{user && <ProfileSummary user={user} />}
|
||||
|
||||
|
|
|
@ -73,13 +73,6 @@ export function NumericResolutionPanel(props: {
|
|||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
const submitButtonClass =
|
||||
outcomeMode === 'CANCEL'
|
||||
? 'bg-yellow-400 hover:bg-yellow-500'
|
||||
: outcome !== undefined
|
||||
? 'btn-primary'
|
||||
: 'btn-disabled'
|
||||
|
||||
return (
|
||||
<Col
|
||||
className={clsx(
|
||||
|
@ -129,7 +122,6 @@ export function NumericResolutionPanel(props: {
|
|||
onResolve={resolve}
|
||||
isSubmitting={isSubmitting}
|
||||
openModalButtonClass={clsx('w-full mt-2')}
|
||||
submitButtonClass={submitButtonClass}
|
||||
color={outcomeMode === 'CANCEL' ? 'yellow' : 'indigo'}
|
||||
disabled={outcomeMode === undefined}
|
||||
/>
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Contract } from 'common/contract'
|
|||
import { Group } from 'common/group'
|
||||
import { Post } from 'common/post'
|
||||
import { useState } from 'react'
|
||||
import { PostCardList } from 'web/pages/group/[...slugs]'
|
||||
import { Button } from './button'
|
||||
import { PillButton } from './buttons/pill-button'
|
||||
import { ContractSearch } from './contract-search'
|
||||
|
@ -10,6 +9,7 @@ import { Col } from './layout/col'
|
|||
import { Modal } from './layout/modal'
|
||||
import { Row } from './layout/row'
|
||||
import { LoadingIndicator } from './loading-indicator'
|
||||
import { PostCardList } from './post-card'
|
||||
|
||||
export function PinnedSelectModal(props: {
|
||||
title: string
|
||||
|
|
|
@ -18,8 +18,8 @@ const MARGIN_Y = MARGIN.top + MARGIN.bottom
|
|||
export type GraphMode = 'profit' | 'value'
|
||||
|
||||
export const PortfolioTooltip = (props: TooltipProps<Date, HistoryPoint>) => {
|
||||
const { mouseX, xScale } = props
|
||||
const d = dayjs(xScale.invert(mouseX))
|
||||
const { x, xScale } = props
|
||||
const d = dayjs(xScale.invert(x))
|
||||
return (
|
||||
<Col className="text-xs font-semibold sm:text-sm">
|
||||
<div>{d.format('MMM/D/YY')}</div>
|
||||
|
|
|
@ -32,10 +32,10 @@ export const PortfolioValueSection = memo(
|
|||
return (
|
||||
<>
|
||||
<Row className="mb-2 justify-between">
|
||||
<Row className="gap-4 sm:gap-8">
|
||||
<Row className="gap-2">
|
||||
<Col
|
||||
className={clsx(
|
||||
'cursor-pointer',
|
||||
'w-24 cursor-pointer sm:w-28 ',
|
||||
graphMode != 'profit'
|
||||
? 'cursor-pointer opacity-40 hover:opacity-80'
|
||||
: ''
|
||||
|
@ -72,7 +72,7 @@ export const PortfolioValueSection = memo(
|
|||
|
||||
<Col
|
||||
className={clsx(
|
||||
'cursor-pointer',
|
||||
'w-24 cursor-pointer sm:w-28',
|
||||
graphMode != 'value' ? 'opacity-40 hover:opacity-80' : ''
|
||||
)}
|
||||
onClick={() => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { track } from '@amplitude/analytics-browser'
|
||||
import { DocumentIcon } from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import { Post } from 'common/post'
|
||||
import Link from 'next/link'
|
||||
|
@ -6,8 +7,8 @@ import { useUserById } from 'web/hooks/use-user'
|
|||
import { postPath } from 'web/lib/firebase/posts'
|
||||
import { fromNow } from 'web/lib/util/time'
|
||||
import { Avatar } from './avatar'
|
||||
import { Card } from './card'
|
||||
import { CardHighlightOptions } from './contract/contracts-grid'
|
||||
import { Row } from './layout/row'
|
||||
import { UserLink } from './user-link'
|
||||
|
||||
export function PostCard(props: {
|
||||
|
@ -25,9 +26,9 @@ export function PostCard(props: {
|
|||
|
||||
return (
|
||||
<div className="relative py-1">
|
||||
<Row
|
||||
<Card
|
||||
className={clsx(
|
||||
' relative gap-3 rounded-lg bg-white py-2 shadow-md hover:cursor-pointer hover:bg-gray-100',
|
||||
'relative flex gap-3 py-2 px-3',
|
||||
itemIds?.includes(post.id) && highlightClassName
|
||||
)}
|
||||
>
|
||||
|
@ -44,9 +45,20 @@ export function PostCard(props: {
|
|||
<span className="mx-1">•</span>
|
||||
<span className="text-gray-500">{fromNow(post.createdTime)}</span>
|
||||
</div>
|
||||
<div className="text-lg font-medium text-gray-900">{post.title}</div>
|
||||
<div className=" break-words text-lg font-medium text-gray-900">
|
||||
{post.title}
|
||||
</div>
|
||||
<div className="font-small text-md break-words text-gray-500">
|
||||
{post.subtitle}
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
<div>
|
||||
<span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-indigo-300 px-2 py-0.5 text-xs font-medium text-white">
|
||||
<DocumentIcon className={'h3 w-3'} />
|
||||
Post
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
{onPostClick ? (
|
||||
<a
|
||||
className="absolute top-0 left-0 right-0 bottom-0"
|
||||
|
@ -80,3 +92,23 @@ export function PostCard(props: {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PostCardList(props: {
|
||||
posts: Post[]
|
||||
highlightOptions?: CardHighlightOptions
|
||||
onPostClick?: (post: Post) => void
|
||||
}) {
|
||||
const { posts, onPostClick, highlightOptions } = props
|
||||
return (
|
||||
<div className="w-full">
|
||||
{posts.map((post) => (
|
||||
<PostCard
|
||||
key={post.id}
|
||||
post={post}
|
||||
onPostClick={onPostClick}
|
||||
highlightOptions={highlightOptions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
import { DateTimeTooltip } from './datetime-tooltip'
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { fromNow } from 'web/lib/util/time'
|
||||
|
||||
export function RelativeTimestamp(props: { time: number }) {
|
||||
const { time } = props
|
||||
const [isClient, setIsClient] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Only render on client to prevent difference from server.
|
||||
setIsClient(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<DateTimeTooltip
|
||||
className="ml-1 whitespace-nowrap text-gray-400"
|
||||
time={time}
|
||||
>
|
||||
{fromNow(time)}
|
||||
{isClient ? fromNow(time) : ''}
|
||||
</DateTimeTooltip>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react'
|
||||
import { CodeIcon } from '@heroicons/react/outline'
|
||||
import { Menu } from '@headlessui/react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { Contract } from 'common/contract'
|
||||
|
@ -8,6 +7,7 @@ import { contractPath } from 'web/lib/firebase/contracts'
|
|||
import { DOMAIN } from 'common/envs/constants'
|
||||
import { copyToClipboard } from 'web/lib/util/copy'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { Button } from './button'
|
||||
|
||||
export function embedContractCode(contract: Contract) {
|
||||
const title = contract.question
|
||||
|
@ -15,6 +15,7 @@ export function embedContractCode(contract: Contract) {
|
|||
return `<iframe src="${src}" title="${title}" frameborder="0"></iframe>`
|
||||
}
|
||||
|
||||
// TODO: move this function elsewhere
|
||||
export function embedContractGridCode(contracts: Contract[]) {
|
||||
const height = (contracts.length - (contracts.length % 2)) * 100 + 'px'
|
||||
const src = `https://${DOMAIN}/embed/grid/${contracts
|
||||
|
@ -26,24 +27,21 @@ export function embedContractGridCode(contracts: Contract[]) {
|
|||
export function ShareEmbedButton(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
|
||||
const codeIcon = <CodeIcon className="mr-1.5 h-4 w-4" aria-hidden="true" />
|
||||
const codeIcon = <CodeIcon className="h-4 w-4" aria-hidden />
|
||||
|
||||
return (
|
||||
<Menu
|
||||
as="div"
|
||||
className="relative z-10 flex-shrink-0"
|
||||
onMouseUp={() => {
|
||||
<Button
|
||||
size="2xs"
|
||||
color="gray-outline"
|
||||
className="gap-1"
|
||||
onClick={() => {
|
||||
copyToClipboard(embedContractCode(contract))
|
||||
toast.success('Embed code copied!', {
|
||||
icon: codeIcon,
|
||||
})
|
||||
toast.success('Embed code copied!', { icon: codeIcon })
|
||||
track('copy embed code')
|
||||
}}
|
||||
>
|
||||
<Menu.Button className="btn btn-xs border-2 !border-gray-500 !bg-white normal-case text-gray-500">
|
||||
{codeIcon}
|
||||
Embed
|
||||
</Menu.Button>
|
||||
</Menu>
|
||||
{codeIcon}
|
||||
Embed
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
import { LinkIcon } from '@heroicons/react/outline'
|
||||
import toast from 'react-hot-toast'
|
||||
import { copyToClipboard } from 'web/lib/util/copy'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { Modal } from './layout/modal'
|
||||
import { Col } from './layout/col'
|
||||
import { Title } from './title'
|
||||
import { Button } from './button'
|
||||
import { TweetButton } from './tweet-button'
|
||||
import { Row } from './layout/row'
|
||||
import { CopyLinkButton } from './copy-link-button'
|
||||
|
||||
export function SharePostModal(props: {
|
||||
shareUrl: string
|
||||
|
@ -16,30 +11,14 @@ export function SharePostModal(props: {
|
|||
}) {
|
||||
const { isOpen, setOpen, shareUrl } = props
|
||||
|
||||
const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" />
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} setOpen={setOpen} size="md">
|
||||
<Col className="gap-4 rounded bg-white p-4">
|
||||
<Title className="!mt-0 !mb-2" text="Share this post" />
|
||||
<Button
|
||||
size="2xl"
|
||||
color="gradient"
|
||||
className={'mb-2 flex max-w-xs self-center'}
|
||||
onClick={() => {
|
||||
copyToClipboard(shareUrl)
|
||||
toast.success('Link copied!', {
|
||||
icon: linkIcon,
|
||||
})
|
||||
track('copy share post link')
|
||||
}}
|
||||
>
|
||||
{linkIcon} Copy link
|
||||
</Button>
|
||||
|
||||
<Row className="z-0 justify-start gap-4 self-center">
|
||||
<TweetButton className="self-start" tweetText={shareUrl} />
|
||||
</Row>
|
||||
<CopyLinkButton url={shareUrl} tracking="copy share post link" />
|
||||
<div className="self-center">
|
||||
<TweetButton tweetText={shareUrl} />
|
||||
</div>
|
||||
</Col>
|
||||
</Modal>
|
||||
)
|
||||
|
|
|
@ -29,7 +29,14 @@ export const SizedContainer = (props: {
|
|||
}, [threshold, fullHeight, mobileHeight])
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{width != null && height != null && children(width, height)}
|
||||
{width != null && height != null ? (
|
||||
children(width, height)
|
||||
) : (
|
||||
<>
|
||||
<div className="sm:hidden" style={{ height: mobileHeight }} />
|
||||
<div className="hidden sm:flex" style={{ height: fullHeight }} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { debounce, sum } from 'lodash'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
import { Comment } from 'common/comment'
|
||||
import { User } from 'common/user'
|
||||
import { CommentTips } from 'web/hooks/use-tip-txns'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { transact } from 'web/lib/firebase/api'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
|
@ -13,25 +12,27 @@ import { Row } from './layout/row'
|
|||
import { LIKE_TIP_AMOUNT } from 'common/like'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
|
||||
export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
||||
const { comment, tips } = prop
|
||||
export function Tipper(prop: {
|
||||
comment: Comment
|
||||
myTip: number
|
||||
totalTip: number
|
||||
}) {
|
||||
const { comment, myTip, totalTip } = prop
|
||||
|
||||
const me = useUser()
|
||||
const myId = me?.id ?? ''
|
||||
const savedTip = tips[myId] ?? 0
|
||||
|
||||
const [localTip, setLocalTip] = useState(savedTip)
|
||||
const [localTip, setLocalTip] = useState(myTip)
|
||||
|
||||
// listen for user being set
|
||||
const initialized = useRef(false)
|
||||
useEffect(() => {
|
||||
if (tips[myId] && !initialized.current) {
|
||||
setLocalTip(tips[myId])
|
||||
if (myTip && !initialized.current) {
|
||||
setLocalTip(myTip)
|
||||
initialized.current = true
|
||||
}
|
||||
}, [tips, myId])
|
||||
}, [myTip])
|
||||
|
||||
const total = sum(Object.values(tips)) - savedTip + localTip
|
||||
const total = totalTip - myTip + localTip
|
||||
|
||||
// declare debounced function only on first render
|
||||
const [saveTip] = useState(() =>
|
||||
|
@ -73,7 +74,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
|||
|
||||
const addTip = (delta: number) => {
|
||||
setLocalTip(localTip + delta)
|
||||
me && saveTip(me, comment, localTip - savedTip + delta)
|
||||
me && saveTip(me, comment, localTip - myTip + delta)
|
||||
toast(`You tipped ${comment.userName} ${formatMoney(LIKE_TIP_AMOUNT)}!`)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
import clsx from 'clsx'
|
||||
import TwitterLogo from 'web/lib/icons/twitter-logo'
|
||||
import { trackCallback } from 'web/lib/service/analytics'
|
||||
import { buttonClass } from './button'
|
||||
|
||||
export function TweetButton(props: { className?: string; tweetText: string }) {
|
||||
const { tweetText, className } = props
|
||||
export function TweetButton(props: { tweetText: string }) {
|
||||
const { tweetText } = props
|
||||
|
||||
return (
|
||||
<a
|
||||
className={clsx('btn btn-xs flex-nowrap normal-case', className)}
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
border: '2px solid #1da1f2',
|
||||
color: '#1da1f2',
|
||||
}}
|
||||
// #1da1f2 is twitter blue
|
||||
className={clsx(
|
||||
buttonClass('2xs', 'override'),
|
||||
'gap-1 border-2 border-[#1da1f2] text-[#1da1f2] hover:bg-[#1da1f2] hover:text-white'
|
||||
)}
|
||||
href={getTweetHref(tweetText)}
|
||||
onClick={trackCallback('share tweet')}
|
||||
target="_blank"
|
||||
>
|
||||
<img className="mr-2" src={'/twitter-logo.svg'} width={15} height={15} />
|
||||
<TwitterLogo width={15} height={15} />
|
||||
<div>Tweet</div>
|
||||
</a>
|
||||
)
|
||||
|
|
|
@ -3,12 +3,10 @@ import Router from 'next/router'
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { getProbability } from 'common/calculate'
|
||||
import { Contract, CPMMBinaryContract } from 'common/contract'
|
||||
import { CPMMBinaryContract } from 'common/contract'
|
||||
import { Customize, USAMap } from './usa-map'
|
||||
import {
|
||||
getContractFromSlug,
|
||||
listenForContract,
|
||||
} from 'web/lib/firebase/contracts'
|
||||
import { listenForContract } from 'web/lib/firebase/contracts'
|
||||
import { interpolateColor } from 'common/util/color'
|
||||
|
||||
export interface StateElectionMarket {
|
||||
creatorUsername: string
|
||||
|
@ -17,10 +15,14 @@ export interface StateElectionMarket {
|
|||
state: string
|
||||
}
|
||||
|
||||
export function StateElectionMap(props: { markets: StateElectionMarket[] }) {
|
||||
export function StateElectionMap(props: {
|
||||
markets: StateElectionMarket[]
|
||||
contracts: CPMMBinaryContract[]
|
||||
}) {
|
||||
const { markets } = props
|
||||
const [contracts, setContracts] = useState(props.contracts)
|
||||
useUpdateContracts(contracts, setContracts)
|
||||
|
||||
const contracts = useContracts(markets.map((m) => m.slug))
|
||||
const probs = contracts.map((c) =>
|
||||
c ? getProbability(c as CPMMBinaryContract) : 0.5
|
||||
)
|
||||
|
@ -45,35 +47,30 @@ export function StateElectionMap(props: { markets: StateElectionMarket[] }) {
|
|||
|
||||
const probToColor = (prob: number, isWinRepublican: boolean) => {
|
||||
const p = isWinRepublican ? prob : 1 - prob
|
||||
const hue = p > 0.5 ? 350 : 240
|
||||
const saturation = 100
|
||||
const lightness = 100 - 50 * Math.abs(p - 0.5)
|
||||
return `hsl(${hue}, ${saturation}%, ${lightness}%)`
|
||||
const color = p > 0.5 ? '#e4534b' : '#5f6eb0'
|
||||
return interpolateColor('#ebe4ec', color, Math.abs(p - 0.5) * 2)
|
||||
}
|
||||
|
||||
const useContracts = (slugs: string[]) => {
|
||||
const [contracts, setContracts] = useState<(Contract | undefined)[]>(
|
||||
slugs.map(() => undefined)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all(slugs.map((slug) => getContractFromSlug(slug))).then(
|
||||
(contracts) => setContracts(contracts)
|
||||
)
|
||||
}, [slugs])
|
||||
|
||||
const useUpdateContracts = (
|
||||
contracts: CPMMBinaryContract[],
|
||||
setContracts: (newContracts: CPMMBinaryContract[]) => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (contracts.some((c) => c === undefined)) return
|
||||
|
||||
// listen to contract updates
|
||||
const unsubs = (contracts as Contract[]).map((c, i) =>
|
||||
listenForContract(
|
||||
c.id,
|
||||
(newC) => newC && setContracts(setAt(contracts, i, newC))
|
||||
const unsubs = contracts
|
||||
.filter((c) => !!c)
|
||||
.map((c, i) =>
|
||||
listenForContract(
|
||||
c.id,
|
||||
(newC) =>
|
||||
newC &&
|
||||
setContracts(setAt(contracts, i, newC as CPMMBinaryContract))
|
||||
)
|
||||
)
|
||||
)
|
||||
return () => unsubs.forEach((u) => u())
|
||||
}, [contracts])
|
||||
}, [contracts, setContracts])
|
||||
|
||||
return contracts
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// https://github.com/jb-1980/usa-map-react
|
||||
// MIT License
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { DATA } from './data'
|
||||
import { USAState } from './usa-state'
|
||||
|
||||
|
@ -53,8 +54,6 @@ export const USAMap = ({
|
|||
onClick = (e) => {
|
||||
console.log(e.currentTarget.dataset.name)
|
||||
},
|
||||
width = 959,
|
||||
height = 593,
|
||||
title = 'US states map',
|
||||
defaultFill = '#d3d3d3',
|
||||
customize,
|
||||
|
@ -68,10 +67,8 @@ export const USAMap = ({
|
|||
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
className={clsx('flex h-96 w-full sm:h-full', className)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 959 593"
|
||||
>
|
||||
<title>{title}</title>
|
||||
|
|
|
@ -11,12 +11,13 @@ export function WarningConfirmationButton(props: {
|
|||
amount: number | undefined
|
||||
marketType: 'freeResponse' | 'binary'
|
||||
warning?: string
|
||||
onSubmit: () => void
|
||||
onSubmit?: () => void
|
||||
disabled: boolean
|
||||
isSubmitting: boolean
|
||||
openModalButtonClass?: string
|
||||
color: ColorType
|
||||
size: SizeType
|
||||
actionLabel: string
|
||||
}) {
|
||||
const {
|
||||
amount,
|
||||
|
@ -27,8 +28,15 @@ export function WarningConfirmationButton(props: {
|
|||
openModalButtonClass,
|
||||
size,
|
||||
color,
|
||||
actionLabel,
|
||||
} = props
|
||||
|
||||
const buttonText = isSubmitting
|
||||
? 'Submitting...'
|
||||
: amount
|
||||
? `${actionLabel} ${formatMoney(amount)}`
|
||||
: actionLabel
|
||||
|
||||
if (!warning) {
|
||||
return (
|
||||
<Button
|
||||
|
@ -38,11 +46,7 @@ export function WarningConfirmationButton(props: {
|
|||
onClick={onSubmit}
|
||||
color={color}
|
||||
>
|
||||
{isSubmitting
|
||||
? 'Submitting...'
|
||||
: amount
|
||||
? `Wager ${formatMoney(amount)}`
|
||||
: 'Wager'}
|
||||
{buttonText}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
@ -50,18 +54,18 @@ export function WarningConfirmationButton(props: {
|
|||
return (
|
||||
<ConfirmationButton
|
||||
openModalBtn={{
|
||||
label: amount ? `Wager ${formatMoney(amount)}` : 'Wager',
|
||||
label: buttonText,
|
||||
size: size,
|
||||
color: 'yellow',
|
||||
disabled: isSubmitting,
|
||||
disabled: isSubmitting || disabled,
|
||||
}}
|
||||
cancelBtn={{
|
||||
label: 'Cancel',
|
||||
className: 'btn btn-warning',
|
||||
color: 'yellow',
|
||||
}}
|
||||
submitBtn={{
|
||||
label: 'Submit',
|
||||
className: clsx('btn border-none btn-sm btn-ghost self-center'),
|
||||
color: 'gray',
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
|
|
|
@ -244,7 +244,7 @@ function Button(props: {
|
|||
type="button"
|
||||
className={clsx(
|
||||
'inline-flex flex-1 items-center justify-center rounded-md border border-transparent px-8 py-3 font-medium shadow-sm',
|
||||
color === 'green' && 'bg-teal-500 bg-teal-600 text-white',
|
||||
color === 'green' && 'bg-teal-500 text-white hover:bg-teal-600',
|
||||
color === 'red' && 'bg-red-400 text-white hover:bg-red-500',
|
||||
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
|
||||
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
|
||||
|
|
|
@ -92,7 +92,7 @@ export const useContractsByDailyScoreGroups = (
|
|||
|
||||
const q = new QueryClient()
|
||||
export const getCachedContracts = async () =>
|
||||
q.fetchQuery(['contracts'], () => listAllContracts(1000), {
|
||||
q.fetchQuery(['contracts'], () => listAllContracts(10000), {
|
||||
staleTime: Infinity,
|
||||
})
|
||||
|
||||
|
|
|
@ -91,9 +91,15 @@ export const usePagination = <T>(opts: PaginationOptions<T>) => {
|
|||
const nextQ = lastDoc
|
||||
? query(state.baseQ, startAfter(lastDoc), limit(state.pageSize))
|
||||
: query(state.baseQ, limit(state.pageSize))
|
||||
return onSnapshot(nextQ, (snapshot) => {
|
||||
dispatch({ type: 'LOAD', snapshot })
|
||||
})
|
||||
return onSnapshot(
|
||||
nextQ,
|
||||
(snapshot) => {
|
||||
dispatch({ type: 'LOAD', snapshot })
|
||||
},
|
||||
(error) => {
|
||||
console.error('error', error)
|
||||
}
|
||||
)
|
||||
}
|
||||
}, [state.isLoading, state.baseQ, state.docs, state.pageSize])
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useStateCheckEquality } from './use-state-check-equality'
|
||||
import { NextRouter } from 'next/router'
|
||||
import { NextRouter, useRouter } from 'next/router'
|
||||
|
||||
export type PersistenceOptions<T> = { key: string; store: PersistentStore<T> }
|
||||
|
||||
export interface PersistentStore<T> {
|
||||
get: (k: string) => T | undefined
|
||||
set: (k: string, v: T | undefined) => void
|
||||
readsUrl?: boolean
|
||||
}
|
||||
|
||||
const withURLParam = (location: Location, k: string, v?: string) => {
|
||||
|
@ -61,6 +62,7 @@ export const urlParamStore = (router: NextRouter): PersistentStore<string> => ({
|
|||
window.history.replaceState(updatedState, '', url)
|
||||
}
|
||||
},
|
||||
readsUrl: true,
|
||||
})
|
||||
|
||||
export const historyStore = <T>(prefix = '__manifold'): PersistentStore<T> => ({
|
||||
|
@ -102,5 +104,20 @@ export const usePersistentState = <T>(
|
|||
store.set(key, state)
|
||||
}
|
||||
}, [key, state])
|
||||
|
||||
if (store?.readsUrl) {
|
||||
// On page load, router isn't ready immediately, so set state once it is.
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const router = useRouter()
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
if (router.isReady) {
|
||||
const savedValue = key != null ? store.get(key) : undefined
|
||||
setState(savedValue ?? initial)
|
||||
}
|
||||
}, [router.isReady])
|
||||
}
|
||||
|
||||
return [state, setState] as const
|
||||
}
|
||||
|
|
|
@ -108,7 +108,6 @@ export const tournamentContractsByGroupSlugQuery = (slug: string) =>
|
|||
query(
|
||||
contracts,
|
||||
where('groupSlugs', 'array-contains', slug),
|
||||
where('isResolved', '==', false),
|
||||
orderBy('popularityScore', 'desc')
|
||||
)
|
||||
|
||||
|
|
28
web/lib/icons/twitter-logo.tsx
Normal file
28
web/lib/icons/twitter-logo.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
export default function TwitterLogo(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Logo"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 248 204"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<g id="Logo_1_">
|
||||
<path
|
||||
id="white_background"
|
||||
className="st0"
|
||||
d="M221.95,51.29c0.15,2.17,0.15,4.34,0.15,6.53c0,66.73-50.8,143.69-143.69,143.69v-0.04
|
||||
C50.97,201.51,24.1,193.65,1,178.83c3.99,0.48,8,0.72,12.02,0.73c22.74,0.02,44.83-7.61,62.72-21.66
|
||||
c-21.61-0.41-40.56-14.5-47.18-35.07c7.57,1.46,15.37,1.16,22.8-0.87C27.8,117.2,10.85,96.5,10.85,72.46c0-0.22,0-0.43,0-0.64
|
||||
c7.02,3.91,14.88,6.08,22.92,6.32C11.58,63.31,4.74,33.79,18.14,10.71c25.64,31.55,63.47,50.73,104.08,52.76
|
||||
c-4.07-17.54,1.49-35.92,14.61-48.25c20.34-19.12,52.33-18.14,71.45,2.19c11.31-2.23,22.15-6.38,32.07-12.26
|
||||
c-3.77,11.69-11.66,21.62-22.2,27.93c10.01-1.18,19.79-3.86,29-7.95C240.37,35.29,231.83,44.14,221.95,51.29z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
|
@ -5,30 +5,32 @@
|
|||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
import { PROD_CONFIG } from 'common/envs/prod'
|
||||
|
||||
try {
|
||||
;(function (l, e, a, p) {
|
||||
if (window.Sprig) return
|
||||
window.Sprig = function (...args) {
|
||||
S._queue.push(args)
|
||||
}
|
||||
const S = window.Sprig
|
||||
S.appId = a
|
||||
S._queue = []
|
||||
window.UserLeap = S
|
||||
a = l.createElement('script')
|
||||
a.async = 1
|
||||
a.src = e + '?id=' + S.appId
|
||||
p = l.getElementsByTagName('script')[0]
|
||||
ENV_CONFIG.domain === PROD_CONFIG.domain && p.parentNode.insertBefore(a, p)
|
||||
})(document, 'https://cdn.sprig.com/shim.js', ENV_CONFIG.sprigEnvironmentId)
|
||||
} catch (error) {
|
||||
console.log('Error initializing Sprig, please complain to Barak', error)
|
||||
if (ENV_CONFIG.domain === PROD_CONFIG.domain) {
|
||||
try {
|
||||
;(function (l, e, a, p) {
|
||||
if (window.Sprig) return
|
||||
window.Sprig = function (...args) {
|
||||
S._queue.push(args)
|
||||
}
|
||||
const S = window.Sprig
|
||||
S.appId = a
|
||||
S._queue = []
|
||||
window.UserLeap = S
|
||||
a = l.createElement('script')
|
||||
a.async = 1
|
||||
a.src = e + '?id=' + S.appId
|
||||
p = l.getElementsByTagName('script')[0]
|
||||
p.parentNode.insertBefore(a, p)
|
||||
})(document, 'https://cdn.sprig.com/shim.js', ENV_CONFIG.sprigEnvironmentId)
|
||||
} catch (error) {
|
||||
console.log('Error initializing Sprig, please complain to Barak', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function setUserId(userId: string): void {
|
||||
window.Sprig('setUserId', userId)
|
||||
if (window.Sprig) window.Sprig('setUserId', userId)
|
||||
}
|
||||
|
||||
export function setAttributes(attributes: Record<string, unknown>): void {
|
||||
window.Sprig('setAttributes', attributes)
|
||||
if (window.Sprig) window.Sprig('setAttributes', attributes)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"@amplitude/analytics-browser": "0.4.1",
|
||||
"@floating-ui/react-dom-interactions": "0.9.2",
|
||||
"@headlessui/react": "1.6.1",
|
||||
"@hello-pangea/dnd": "16.0.0",
|
||||
"@heroicons/react": "1.0.6",
|
||||
"@react-query-firebase/firestore": "0.4.2",
|
||||
"@tiptap/core": "2.0.0-beta.182",
|
||||
|
@ -54,11 +55,10 @@
|
|||
"next": "12.3.1",
|
||||
"node-fetch": "3.2.4",
|
||||
"prosemirror-state": "1.4.1",
|
||||
"react": "17.0.2",
|
||||
"react-beautiful-dnd": "13.1.1",
|
||||
"react": "18.2.0",
|
||||
"react-confetti": "6.0.1",
|
||||
"react-dom": "17.0.2",
|
||||
"react-expanding-textarea": "2.3.5",
|
||||
"react-dom": "18.2.0",
|
||||
"react-expanding-textarea": "2.3.6",
|
||||
"react-hot-toast": "2.2.0",
|
||||
"react-instantsearch-hooks-web": "6.24.1",
|
||||
"react-masonry-css": "1.0.16",
|
||||
|
@ -75,9 +75,8 @@
|
|||
"@types/d3": "7.4.0",
|
||||
"@types/lodash": "4.14.178",
|
||||
"@types/node": "16.11.11",
|
||||
"@types/react": "17.0.43",
|
||||
"@types/react-beautiful-dnd": "13.1.2",
|
||||
"@types/react-dom": "17.0.2",
|
||||
"@types/react": "18.0.21",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/string-similarity": "^4.0.0",
|
||||
"autoprefixer": "10.2.6",
|
||||
"critters": "0.0.16",
|
||||
|
|
|
@ -207,14 +207,18 @@ export function ContractPageContent(
|
|||
return (
|
||||
<Page
|
||||
rightSidebar={
|
||||
<>
|
||||
<ContractPageSidebar contract={contract} />
|
||||
{isCreator && (
|
||||
<Col className={'xl:hidden'}>
|
||||
<RecommendedContractsWidget contract={contract} />
|
||||
</Col>
|
||||
)}
|
||||
</>
|
||||
user || user === null ? (
|
||||
<>
|
||||
<ContractPageSidebar contract={contract} />
|
||||
{isCreator && (
|
||||
<Col className={'xl:hidden'}>
|
||||
<RecommendedContractsWidget contract={contract} />
|
||||
</Col>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
>
|
||||
{showConfetti && (
|
||||
|
|
|
@ -42,7 +42,7 @@ function MyApp({ Component, pageProps }: AppProps<ManifoldPageProps>) {
|
|||
`}
|
||||
</Script>
|
||||
<Head>
|
||||
<title>Manifold Markets — A market for every question</title>
|
||||
<title>{'Manifold Markets — A market for every question'}</title>
|
||||
|
||||
<meta
|
||||
property="og:title"
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Page } from 'web/components/page'
|
|||
import { useTracking } from 'web/hooks/use-tracking'
|
||||
import { trackCallback } from 'web/lib/service/analytics'
|
||||
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
||||
import { Button } from 'web/components/button'
|
||||
|
||||
export const getServerSideProps = redirectIfLoggedOut('/')
|
||||
|
||||
|
@ -33,15 +34,15 @@ export default function AddFundsPage() {
|
|||
<Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md">
|
||||
<Title className="!mt-0" text="Get Mana" />
|
||||
<img
|
||||
className="mb-6 block -scale-x-100 self-center"
|
||||
src="/stylized-crane-black.png"
|
||||
className="mb-6 block self-center"
|
||||
src="/welcome/manipurple.png"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
|
||||
<div className="mb-6 text-gray-500">
|
||||
Buy mana (M$) to trade in your favorite markets. <br /> (Not
|
||||
redeemable for cash.)
|
||||
Buy mana (M$) to trade in your favorite markets. <br />{' '}
|
||||
<i>Not redeemable for cash.</i>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 text-sm text-gray-500">Amount</div>
|
||||
|
@ -63,13 +64,15 @@ export default function AddFundsPage() {
|
|||
method="POST"
|
||||
className="mt-8"
|
||||
>
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full bg-gradient-to-r from-indigo-500 to-blue-500 font-medium hover:from-indigo-600 hover:to-blue-600"
|
||||
color="gradient"
|
||||
size="xl"
|
||||
className="w-full"
|
||||
onClick={trackCallback('checkout', { amount: amountSelected })}
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
</Col>
|
||||
</Col>
|
||||
|
|
|
@ -38,6 +38,7 @@ import { ExternalLinkIcon } from '@heroicons/react/outline'
|
|||
import { SiteLink } from 'web/components/site-link'
|
||||
import { Button } from 'web/components/button'
|
||||
import { AddFundsModal } from 'web/components/add-funds-modal'
|
||||
import ShortToggle from 'web/components/widgets/short-toggle'
|
||||
|
||||
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
|
||||
return { props: { auth: await getUserAndPrivateUser(creds.uid) } }
|
||||
|
@ -50,6 +51,7 @@ type NewQuestionParams = {
|
|||
description: string
|
||||
closeTime: string
|
||||
outcomeType: string
|
||||
visibility: string
|
||||
// Params for PSEUDO_NUMERIC outcomeType
|
||||
min?: string
|
||||
max?: string
|
||||
|
@ -136,7 +138,9 @@ export function NewContract(props: {
|
|||
const [maxString, setMaxString] = useState(params?.max ?? '')
|
||||
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
||||
const [initialValueString, setInitialValueString] = useState(initValue)
|
||||
|
||||
const [visibility, setVisibility] = useState<visibility>(
|
||||
(params?.visibility as visibility) ?? 'public'
|
||||
)
|
||||
// for multiple choice, init to 3 empty answers
|
||||
const [answers, setAnswers] = useState(['', '', ''])
|
||||
|
||||
|
@ -168,7 +172,6 @@ export function NewContract(props: {
|
|||
undefined
|
||||
)
|
||||
const [showGroupSelector, setShowGroupSelector] = useState(true)
|
||||
const [visibility, setVisibility] = useState<visibility>('public')
|
||||
|
||||
const [fundsModalOpen, setFundsModalOpen] = useState(false)
|
||||
|
||||
|
@ -414,14 +417,9 @@ export function NewContract(props: {
|
|||
|
||||
<Row className="form-control my-2 items-center gap-2 text-sm">
|
||||
<span>Display this market on homepage</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibility === 'public'}
|
||||
disabled={isSubmitting}
|
||||
className="cursor-pointer"
|
||||
onChange={(e) =>
|
||||
setVisibility(e.target.checked ? 'public' : 'unlisted')
|
||||
}
|
||||
<ShortToggle
|
||||
on={visibility === 'public'}
|
||||
setOn={(on) => setVisibility(on ? 'public' : 'unlisted')}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function CreateDateDocPage() {
|
|||
})
|
||||
|
||||
const title = `${user?.name}'s Date Doc`
|
||||
const subtitle = 'Manifold dating docs'
|
||||
const [birthday, setBirthday] = useState<undefined | string>(undefined)
|
||||
const [question, setQuestion] = useState(
|
||||
'Will I find a partner in the next 3 months?'
|
||||
|
@ -46,6 +47,7 @@ export default function CreateDateDocPage() {
|
|||
'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug'
|
||||
> & { question: string } = {
|
||||
title,
|
||||
subtitle,
|
||||
content: editor.getJSON(),
|
||||
bounty: 0,
|
||||
birthday: birthdayTime,
|
||||
|
|
|
@ -42,11 +42,7 @@ import { SelectMarketsModal } from 'web/components/contract-select-modal'
|
|||
import { BETTORS } from 'common/user'
|
||||
import { Page } from 'web/components/page'
|
||||
import { Tabs } from 'web/components/layout/tabs'
|
||||
import { Title } from 'web/components/title'
|
||||
import { CreatePost } from 'web/components/create-post'
|
||||
import { GroupOverview } from 'web/components/groups/group-overview'
|
||||
import { CardHighlightOptions } from 'web/components/contract/contracts-grid'
|
||||
import { PostCard } from 'web/components/post-card'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||
|
@ -183,16 +179,6 @@ export default function GroupPage(props: {
|
|||
</Col>
|
||||
)
|
||||
|
||||
const postsPage = (
|
||||
<>
|
||||
<Col>
|
||||
<div className="mt-4 flex flex-col gap-8 px-4 md:flex-row">
|
||||
{posts && <GroupPosts posts={groupPosts} group={group} />}
|
||||
</div>
|
||||
</Col>
|
||||
</>
|
||||
)
|
||||
|
||||
const overviewPage = (
|
||||
<>
|
||||
<GroupOverview
|
||||
|
@ -249,10 +235,6 @@ export default function GroupPage(props: {
|
|||
title: 'Leaderboards',
|
||||
content: leaderboardTab,
|
||||
},
|
||||
{
|
||||
title: 'Posts',
|
||||
content: postsPage,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
|
@ -336,63 +318,6 @@ function GroupLeaderboard(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export function GroupPosts(props: { posts: Post[]; group: Group }) {
|
||||
const { posts, group } = props
|
||||
const [showCreatePost, setShowCreatePost] = useState(false)
|
||||
const user = useUser()
|
||||
|
||||
const createPost = <CreatePost group={group} />
|
||||
|
||||
const postList = (
|
||||
<div className=" align-start w-full items-start">
|
||||
<Row className="flex justify-between">
|
||||
<Col>
|
||||
<Title text={'Posts'} className="!mt-0" />
|
||||
</Col>
|
||||
<Col>
|
||||
{user && (
|
||||
<Button
|
||||
className="btn-md"
|
||||
onClick={() => setShowCreatePost(!showCreatePost)}
|
||||
>
|
||||
Add a Post
|
||||
</Button>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="mt-2">
|
||||
<PostCardList posts={posts} />
|
||||
{posts.length === 0 && (
|
||||
<div className="text-center text-gray-500">No posts yet</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return showCreatePost ? createPost : postList
|
||||
}
|
||||
|
||||
export function PostCardList(props: {
|
||||
posts: Post[]
|
||||
highlightOptions?: CardHighlightOptions
|
||||
onPostClick?: (post: Post) => void
|
||||
}) {
|
||||
const { posts, onPostClick, highlightOptions } = props
|
||||
return (
|
||||
<div className="w-full">
|
||||
{posts.map((post) => (
|
||||
<PostCard
|
||||
key={post.id}
|
||||
post={post}
|
||||
onPostClick={onPostClick}
|
||||
highlightOptions={highlightOptions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AddContractButton(props: { group: Group; user: User }) {
|
||||
const { group, user } = props
|
||||
const [open, setOpen] = useState(false)
|
||||
|
|
|
@ -21,7 +21,6 @@ import { Group } from 'common/group'
|
|||
import { SiteLink } from 'web/components/site-link'
|
||||
import { usePrivateUser, useUser } from 'web/hooks/use-user'
|
||||
import {
|
||||
useMemberGroupIds,
|
||||
useMemberGroupsSubscription,
|
||||
useTrendingGroups,
|
||||
} from 'web/hooks/use-group'
|
||||
|
@ -80,6 +79,7 @@ export default function Home() {
|
|||
const dailyTrendingContracts = useContractsByDailyScoreNotBetOn(user?.id, 6)
|
||||
|
||||
const groups = useMemberGroupsSubscription(user)
|
||||
const trendingGroups = useTrendingGroups()
|
||||
const groupContracts = useContractsByDailyScoreGroups(
|
||||
groups?.map((g) => g.slug)
|
||||
)
|
||||
|
@ -113,16 +113,26 @@ export default function Home() {
|
|||
<LoadingIndicator />
|
||||
) : (
|
||||
<>
|
||||
{renderSections(user, sections, {
|
||||
{renderSections(sections, {
|
||||
score: trendingContracts,
|
||||
newest: newContracts,
|
||||
'daily-trending': dailyTrendingContracts,
|
||||
'daily-movers': dailyMovers,
|
||||
})}
|
||||
|
||||
<TrendingGroupsSection user={user} />
|
||||
|
||||
{renderGroupSections(user, groups, groupContracts)}
|
||||
{groups && groupContracts && trendingGroups.length > 0 ? (
|
||||
<>
|
||||
<TrendingGroupsSection
|
||||
className="mb-4"
|
||||
user={user}
|
||||
myGroups={groups}
|
||||
trendingGroups={trendingGroups}
|
||||
/>
|
||||
{renderGroupSections(user, groups, groupContracts)}
|
||||
</>
|
||||
) : (
|
||||
<LoadingIndicator />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
|
@ -171,7 +181,6 @@ export const getHomeItems = (sections: string[]) => {
|
|||
}
|
||||
|
||||
function renderSections(
|
||||
user: User,
|
||||
sections: { id: string; label: string }[],
|
||||
sectionContracts: {
|
||||
'daily-movers': CPMMBinaryContract[]
|
||||
|
@ -192,7 +201,7 @@ function renderSections(
|
|||
}
|
||||
if (id === 'daily-trending') {
|
||||
return (
|
||||
<ContractsSection
|
||||
<SearchSection
|
||||
key={id}
|
||||
label={label}
|
||||
contracts={contracts}
|
||||
|
@ -202,11 +211,11 @@ function renderSections(
|
|||
)
|
||||
}
|
||||
return (
|
||||
<ContractsSection
|
||||
<SearchSection
|
||||
key={id}
|
||||
label={label}
|
||||
contracts={contracts}
|
||||
sort={id === 'daily-trending' ? 'daily-score' : (id as Sort)}
|
||||
sort={id as Sort}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
@ -216,13 +225,9 @@ function renderSections(
|
|||
|
||||
function renderGroupSections(
|
||||
user: User,
|
||||
groups: Group[] | undefined,
|
||||
groupContracts: Dictionary<CPMMBinaryContract[]> | undefined
|
||||
groups: Group[],
|
||||
groupContracts: Dictionary<CPMMBinaryContract[]>
|
||||
) {
|
||||
if (!groups || !groupContracts) {
|
||||
return <LoadingIndicator />
|
||||
}
|
||||
|
||||
const filteredGroups = groups.filter((g) => groupContracts[g.slug])
|
||||
const orderedGroups = sortBy(filteredGroups, (g) =>
|
||||
// Sort by sum of top two daily scores.
|
||||
|
@ -285,7 +290,7 @@ function SectionHeader(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function ContractsSection(props: {
|
||||
function SearchSection(props: {
|
||||
label: string
|
||||
contracts: CPMMBinaryContract[]
|
||||
sort: Sort
|
||||
|
@ -406,15 +411,16 @@ function DailyStats(props: {
|
|||
}
|
||||
|
||||
export function TrendingGroupsSection(props: {
|
||||
user: User | null | undefined
|
||||
user: User
|
||||
myGroups: Group[]
|
||||
trendingGroups: Group[]
|
||||
className?: string
|
||||
}) {
|
||||
const { user, className } = props
|
||||
const memberGroupIds = useMemberGroupIds(user) || []
|
||||
const { user, myGroups, trendingGroups, className } = props
|
||||
|
||||
const groups = useTrendingGroups().filter(
|
||||
(g) => !memberGroupIds.includes(g.id)
|
||||
)
|
||||
const myGroupIds = new Set(myGroups.map((g) => g.id))
|
||||
|
||||
const groups = trendingGroups.filter((g) => !myGroupIds.has(g.id))
|
||||
const count = 20
|
||||
const chosenGroups = groups.slice(0, count)
|
||||
|
||||
|
@ -433,10 +439,9 @@ export function TrendingGroupsSection(props: {
|
|||
<PillButton
|
||||
className="flex flex-row items-center gap-1"
|
||||
key={g.id}
|
||||
selected={memberGroupIds.includes(g.id)}
|
||||
selected={myGroupIds.has(g.id)}
|
||||
onSelect={() => {
|
||||
if (!user) return
|
||||
if (memberGroupIds.includes(g.id)) leaveGroup(g, user?.id)
|
||||
if (myGroupIds.has(g.id)) leaveGroup(g, user.id)
|
||||
else {
|
||||
const homeSections = (user.homeSections ?? [])
|
||||
.filter((id) => id !== g.id)
|
||||
|
|
|
@ -44,8 +44,8 @@ export default function LabsPage() {
|
|||
/>
|
||||
|
||||
<LabCard
|
||||
title="💌 Dating docs"
|
||||
description="Browse dating docs or create your own"
|
||||
title="💌 Dating"
|
||||
description="Browse dating profiles and bet on relationships"
|
||||
href="/date-docs"
|
||||
/>
|
||||
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { CPMMBinaryContract } from 'common/contract'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { Spacer } from 'web/components/layout/spacer'
|
||||
import { Page } from 'web/components/page'
|
||||
import { SEO } from 'web/components/SEO'
|
||||
import { Title } from 'web/components/title'
|
||||
import {
|
||||
StateElectionMarket,
|
||||
StateElectionMap,
|
||||
} from 'web/components/usa-map/state-election-map'
|
||||
import { getContractFromSlug } from 'web/lib/firebase/contracts'
|
||||
|
||||
const senateMidterms: StateElectionMarket[] = [
|
||||
{
|
||||
|
@ -175,76 +178,94 @@ const governorMidterms: StateElectionMarket[] = [
|
|||
},
|
||||
]
|
||||
|
||||
const App = () => {
|
||||
export async function getStaticProps() {
|
||||
const senateContracts = await Promise.all(
|
||||
senateMidterms.map((m) =>
|
||||
getContractFromSlug(m.slug).then((c) => c ?? null)
|
||||
)
|
||||
)
|
||||
|
||||
const governorContracts = await Promise.all(
|
||||
governorMidterms.map((m) =>
|
||||
getContractFromSlug(m.slug).then((c) => c ?? null)
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
props: { senateContracts, governorContracts },
|
||||
revalidate: 60, // regenerate after a minute
|
||||
}
|
||||
}
|
||||
|
||||
const App = (props: {
|
||||
senateContracts: CPMMBinaryContract[]
|
||||
governorContracts: CPMMBinaryContract[]
|
||||
}) => {
|
||||
const { senateContracts, governorContracts } = props
|
||||
|
||||
return (
|
||||
<Page className="">
|
||||
<Col className="items-center justify-center">
|
||||
<Title text="2022 US Midterm Elections" className="mt-2" />
|
||||
<SEO
|
||||
title="2022 US Midterm Elections"
|
||||
description="Bet on the midterm elections using prediction markets. See Manifold's state-by-state breakdown of senate and governor races."
|
||||
/>
|
||||
<div className="mt-2 text-2xl">Senate</div>
|
||||
<StateElectionMap markets={senateMidterms} />
|
||||
<StateElectionMap
|
||||
markets={senateMidterms}
|
||||
contracts={senateContracts}
|
||||
/>
|
||||
<iframe
|
||||
src="https://manifold.markets/TomShlomi/will-the-gop-control-the-us-senate"
|
||||
title="Will the Democrats control the Senate after the Midterms?"
|
||||
frameBorder="0"
|
||||
width={800}
|
||||
height={400}
|
||||
className="mt-8"
|
||||
className="mt-8 flex h-96 w-full"
|
||||
></iframe>
|
||||
<Spacer h={8} />
|
||||
|
||||
<div className="mt-8 text-2xl">Governors</div>
|
||||
<StateElectionMap markets={governorMidterms} />
|
||||
<StateElectionMap
|
||||
markets={governorMidterms}
|
||||
contracts={governorContracts}
|
||||
/>
|
||||
<iframe
|
||||
src="https://manifold.markets/ManifoldMarkets/democrats-go-down-at-least-one-gove"
|
||||
title="Democrats go down at least one governor on net in 2022"
|
||||
frameBorder="0"
|
||||
width={800}
|
||||
height={400}
|
||||
className="mt-8"
|
||||
className="mt-8 flex h-96 w-full"
|
||||
></iframe>
|
||||
<Spacer h={8} />
|
||||
|
||||
<div className="mt-8 text-2xl">House</div>
|
||||
<iframe
|
||||
src="https://manifold.markets/BoltonBailey/will-democrats-maintain-control-of"
|
||||
title="Will the Democrats control the House after the Midterms?"
|
||||
frameBorder="0"
|
||||
width={800}
|
||||
height={400}
|
||||
className="mt-8"
|
||||
className="mt-8 flex h-96 w-full"
|
||||
></iframe>
|
||||
<Spacer h={8} />
|
||||
|
||||
<div className="mt-8 text-2xl">Related markets</div>
|
||||
<iframe
|
||||
src="https://manifold.markets/BoltonBailey/balance-of-power-in-us-congress-aft"
|
||||
title="Balance of Power in US Congress after 2022 Midterms"
|
||||
frameBorder="0"
|
||||
width={800}
|
||||
height={400}
|
||||
className="mt-8"
|
||||
className="mt-8 flex h-96 w-full"
|
||||
></iframe>
|
||||
<iframe
|
||||
src="https://manifold.markets/SG/will-a-democrat-win-the-2024-us-pre"
|
||||
title="Will a Democrat win the 2024 US presidential election?"
|
||||
frameBorder="0"
|
||||
width={800}
|
||||
height={400}
|
||||
className="mt-8"
|
||||
className="mt-8 flex h-96 w-full"
|
||||
></iframe>
|
||||
<iframe
|
||||
src="https://manifold.markets/Ibozz91/will-the-2022-alaska-house-general"
|
||||
title="Will the 2022 Alaska House General Nonspecial Election result in a Condorcet failure?"
|
||||
frameBorder="0"
|
||||
width={800}
|
||||
height={400}
|
||||
className="mt-8"
|
||||
className="mt-8 flex h-96 w-full"
|
||||
></iframe>
|
||||
|
||||
<iframe
|
||||
src="https://manifold.markets/NathanpmYoung/how-many-supreme-court-justices-wil-1e597c3853ad"
|
||||
title="Will the 2022 Alaska House General Nonspecial Election result in a Condorcet failure?"
|
||||
frameBorder="0"
|
||||
width={800}
|
||||
height={400}
|
||||
className="mt-8"
|
||||
className="mt-8 flex h-96 w-full"
|
||||
></iframe>
|
||||
</Col>
|
||||
</Page>
|
||||
|
|
|
@ -38,6 +38,7 @@ import { formatMoney } from 'common/util/format'
|
|||
import { groupPath } from 'web/lib/firebase/groups'
|
||||
import {
|
||||
BETTING_STREAK_BONUS_AMOUNT,
|
||||
BETTING_STREAK_BONUS_MAX,
|
||||
UNIQUE_BETTOR_BONUS_AMOUNT,
|
||||
} from 'common/economy'
|
||||
import { groupBy, sum, uniqBy } from 'lodash'
|
||||
|
@ -440,7 +441,8 @@ function IncomeNotificationItem(props: {
|
|||
} else if (sourceType === 'tip') {
|
||||
reasonText = !simple ? `tipped you on` : `in tips on`
|
||||
} else if (sourceType === 'betting_streak_bonus') {
|
||||
if (sourceText && +sourceText === 50) reasonText = '(max) for your'
|
||||
if (sourceText && +sourceText === BETTING_STREAK_BONUS_MAX)
|
||||
reasonText = '(max) for your'
|
||||
else reasonText = 'for your'
|
||||
} else if (sourceType === 'loan' && sourceText) {
|
||||
reasonText = `of your invested predictions returned as a`
|
||||
|
|
|
@ -25,6 +25,7 @@ import { useCommentsOnPost } from 'web/hooks/use-comments'
|
|||
import { useUser } from 'web/hooks/use-user'
|
||||
import { usePost } from 'web/hooks/use-post'
|
||||
import { SEO } from 'web/components/SEO'
|
||||
import { Subtitle } from 'web/components/subtitle'
|
||||
|
||||
export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||
const { slugs } = props.params
|
||||
|
@ -75,7 +76,11 @@ export default function PostPage(props: {
|
|||
url={'/post/' + post.slug}
|
||||
/>
|
||||
<div className="mx-auto w-full max-w-3xl ">
|
||||
<Title className="!mt-0 py-4 px-2" text={post.title} />
|
||||
<div>
|
||||
<Title className="!my-0 px-2 pt-4" text={post.title} />
|
||||
<br />
|
||||
<Subtitle className="!mt-2 px-2 pb-4" text={post.subtitle} />
|
||||
</div>
|
||||
<Row>
|
||||
<Col className="flex-1 px-2">
|
||||
<div className={'inline-flex'}>
|
||||
|
@ -202,25 +207,20 @@ export function RichEditPost(props: { post: Post }) {
|
|||
</Row>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative">
|
||||
<div className="absolute top-0 right-0 z-10 space-x-2">
|
||||
<Button
|
||||
color="gray"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setEditing(true)
|
||||
editor?.commands.focus('end')
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="inline h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Content content={post.content} />
|
||||
<Spacer h={2} />
|
||||
</div>
|
||||
</>
|
||||
<Col>
|
||||
<Content content={post.content} />
|
||||
<Row className="place-content-end">
|
||||
<Button
|
||||
color="gray-white"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setEditing(true)
|
||||
editor?.commands.focus('end')
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="inline h-4 w-4" />
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -215,7 +215,6 @@ export default function ProfilePage(props: {
|
|||
}}
|
||||
submitBtn={{
|
||||
label: 'Update key',
|
||||
className: 'btn-primary',
|
||||
}}
|
||||
onSubmitWithSuccess={async () => {
|
||||
updateApiKey()
|
||||
|
|
|
@ -4,7 +4,7 @@ import { getServerSideSitemap, ISitemapField } from 'next-sitemap'
|
|||
import { listAllContracts } from 'web/lib/firebase/contracts'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const contracts = await listAllContracts(1000, undefined, 'popularityScore')
|
||||
const contracts = await listAllContracts(5000, undefined, 'popularityScore')
|
||||
|
||||
const score = (popularity: number) => Math.tanh(Math.log10(popularity + 1))
|
||||
|
||||
|
@ -14,7 +14,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||
loc: `https://manifold.markets/${market.creatorUsername}/${market.slug}`,
|
||||
changefreq: market.volume24Hours > 10 ? 'hourly' : 'daily',
|
||||
priority: score(market.popularityScore ?? 0),
|
||||
lastmod: market.lastUpdatedTime,
|
||||
lastmod: new Date(market.lastUpdatedTime ?? 0).toISOString(),
|
||||
})) as ISitemapField[]
|
||||
|
||||
return await getServerSideSitemap(ctx, fields)
|
||||
|
|
|
@ -103,7 +103,7 @@ export function CustomAnalytics(props: Stats) {
|
|||
title: 'Daily (7d avg)',
|
||||
content: (
|
||||
<DailyChart
|
||||
dailyValues={dailyActiveUsersWeeklyAvg}
|
||||
dailyValues={dailyActiveUsersWeeklyAvg.map(Math.round)}
|
||||
startDate={startDate}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -24,6 +24,7 @@ import { SiteLink } from 'web/components/site-link'
|
|||
import { Carousel } from 'web/components/carousel'
|
||||
import { usePagination } from 'web/hooks/use-pagination'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
import { Title } from 'web/components/title'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
@ -48,7 +49,7 @@ const Salem = {
|
|||
title: 'CSPI/Salem Forecasting Tournament',
|
||||
blurb: 'Top 5 traders qualify for a UT Austin research fellowship.',
|
||||
url: 'https://salemcenter.manifold.markets/',
|
||||
award: '$25,000',
|
||||
award: 'US$25,000',
|
||||
endTime: toDate('Jul 31, 2023'),
|
||||
contractIds: [],
|
||||
images: [
|
||||
|
@ -76,35 +77,36 @@ const Salem = {
|
|||
}
|
||||
|
||||
const tourneys: Tourney[] = [
|
||||
{
|
||||
title: 'Fantasy Football Stock Exchange',
|
||||
blurb: 'How many points will each NFL player score this season?',
|
||||
award: 'US$2,500',
|
||||
endTime: toDate('Jan 6, 2023'),
|
||||
groupId: 'SxGRqXRpV3RAQKudbcNb',
|
||||
},
|
||||
{
|
||||
title: 'Clearer Thinking Regrant Project',
|
||||
blurb: 'Which projects will Clearer Thinking give a grant to?',
|
||||
award: '$13,000',
|
||||
award: 'US$13,000',
|
||||
endTime: toDate('Sep 30, 2022'),
|
||||
groupId: 'fhksfIgqyWf7OxsV9nkM',
|
||||
},
|
||||
// {
|
||||
// title: 'Manifold F2P Tournament',
|
||||
// blurb:
|
||||
// 'Who can amass the most mana starting from a free-to-play (F2P) account?',
|
||||
// award: 'Poem',
|
||||
// endTime: toDate('Sep 15, 2022'),
|
||||
// groupId: '6rrIja7tVW00lUVwtsYS',
|
||||
// },
|
||||
// {
|
||||
// title: 'Cause Exploration Prizes',
|
||||
// blurb:
|
||||
// 'Which new charity ideas will Open Philanthropy find most promising?',
|
||||
// award: 'M$100k',
|
||||
// endTime: toDate('Sep 9, 2022'),
|
||||
// groupId: 'cMcpBQ2p452jEcJD2SFw',
|
||||
// },
|
||||
|
||||
{
|
||||
title: 'Fantasy Football Stock Exchange',
|
||||
blurb: 'How many points will each NFL player score this season?',
|
||||
award: '$2,500',
|
||||
endTime: toDate('Jan 6, 2023'),
|
||||
groupId: 'SxGRqXRpV3RAQKudbcNb',
|
||||
title: 'Cause Exploration Prizes',
|
||||
blurb:
|
||||
'Which new charity ideas will Open Philanthropy find most promising?',
|
||||
award: 'M$100k',
|
||||
endTime: toDate('Sep 9, 2022'),
|
||||
groupId: 'cMcpBQ2p452jEcJD2SFw',
|
||||
},
|
||||
{
|
||||
title: 'Manifold F2P Tournament',
|
||||
blurb:
|
||||
'Who can amass the most mana starting from a free-to-play (F2P) account?',
|
||||
award: 'Poem',
|
||||
endTime: toDate('Sep 15, 2022'),
|
||||
groupId: '6rrIja7tVW00lUVwtsYS',
|
||||
},
|
||||
|
||||
// Tournaments without awards get featured below
|
||||
|
@ -158,16 +160,31 @@ export async function getStaticProps() {
|
|||
export default function TournamentPage(props: { sections: SectionInfo[] }) {
|
||||
const { sections } = props
|
||||
|
||||
const description = `Win real prizes (including cash!) by participating in forecasting
|
||||
tournaments on current events, sports, science, and more.`
|
||||
return (
|
||||
<Page>
|
||||
<SEO
|
||||
title="Tournaments"
|
||||
description="Win money by predicting in forecasting tournaments on current events, sports, science, and more"
|
||||
/>
|
||||
<SEO title="Tournaments" description={description} />
|
||||
<Col className="m-4 gap-10 sm:mx-10 sm:gap-24 xl:w-[125%]">
|
||||
<Col>
|
||||
<Title text="🏆 Manifold tournaments" />
|
||||
<div>{description}</div>
|
||||
</Col>
|
||||
<div>
|
||||
<SectionHeader
|
||||
url={Salem.url}
|
||||
title={Salem.title}
|
||||
award={Salem.award}
|
||||
endTime={Salem.endTime}
|
||||
/>
|
||||
<span className="text-gray-500">{Salem.blurb}</span>
|
||||
<ImageCarousel url={Salem.url} images={Salem.images} />
|
||||
</div>
|
||||
|
||||
{sections.map(
|
||||
({ tourney, slug, numPeople }) =>
|
||||
tourney.award && (
|
||||
tourney.award &&
|
||||
(tourney.endTime ?? 0) > Date.now() && (
|
||||
<div key={slug}>
|
||||
<SectionHeader
|
||||
url={groupPath(slug, 'about')}
|
||||
|
@ -181,17 +198,40 @@ export default function TournamentPage(props: { sections: SectionInfo[] }) {
|
|||
</div>
|
||||
)
|
||||
)}
|
||||
<div>
|
||||
<SectionHeader
|
||||
url={Salem.url}
|
||||
title={Salem.title}
|
||||
award={Salem.award}
|
||||
endTime={Salem.endTime}
|
||||
/>
|
||||
<span className="text-gray-500">{Salem.blurb}</span>
|
||||
<ImageCarousel url={Salem.url} images={Salem.images} />
|
||||
|
||||
{/* Title break */}
|
||||
<div className="relative">
|
||||
<div
|
||||
className="absolute inset-0 flex items-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="bg-gray-50 px-3 text-lg font-medium text-gray-900">
|
||||
Past tournaments
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sections.map(
|
||||
({ tourney, slug, numPeople }) =>
|
||||
tourney.award &&
|
||||
(tourney.endTime ?? 0) <= Date.now() && (
|
||||
<div key={slug}>
|
||||
<SectionHeader
|
||||
url={groupPath(slug, 'about')}
|
||||
title={tourney.title}
|
||||
ppl={numPeople}
|
||||
award={tourney.award}
|
||||
endTime={tourney.endTime}
|
||||
/>
|
||||
<span className="text-gray-500">{tourney.blurb}</span>
|
||||
<MarketCarousel slug={slug} />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Title break */}
|
||||
<div className="relative">
|
||||
<div
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { track } from '@amplitude/analytics-browser'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { sum } from 'lodash'
|
||||
import clsx from 'clsx'
|
||||
import { PostComment } from 'common/comment'
|
||||
import { Post } from 'common/post'
|
||||
|
@ -109,6 +110,7 @@ export function PostComment(props: {
|
|||
const { text, content, userUsername, userName, userAvatarUrl, createdTime } =
|
||||
comment
|
||||
|
||||
const me = useUser()
|
||||
const [highlighted, setHighlighted] = useState(false)
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
|
@ -162,7 +164,11 @@ export function PostComment(props: {
|
|||
Reply
|
||||
</button>
|
||||
)}
|
||||
<Tipper comment={comment} tips={tips ?? {}} />
|
||||
<Tipper
|
||||
comment={comment}
|
||||
myTip={me ? tips?.[me.id] ?? 0 : 0}
|
||||
totalTip={sum(Object.values(tips ?? {}))}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
</Row>
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
||||
<url><loc>https://manifold.markets</loc><changefreq>hourly</changefreq><priority>1.0</priority></url>
|
||||
<url><loc>https://manifold.markets/search</loc><changefreq>hourly</changefreq><priority>1.0</priority></url>
|
||||
<url><loc>https://manifold.markets/home</loc><changefreq>hourly</changefreq><priority>0.2</priority></url>
|
||||
<url><loc>https://manifold.markets/leaderboards</loc><changefreq>daily</changefreq><priority>0.2</priority></url>
|
||||
<url><loc>https://manifold.markets/add-funds</loc><changefreq>daily</changefreq><priority>0.2</priority></url>
|
||||
<url><loc>https://manifold.markets/challenges</loc><changefreq>daily</changefreq><priority>0.2</priority></url>
|
||||
<url><loc>https://manifold.markets/charity</loc><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://manifold.markets/groups</loc><changefreq>daily</changefreq><priority>0.2</priority></url>
|
||||
<url><loc>https://manifold.markets/tournaments</loc><changefreq>daily</changefreq><priority>0.2</priority></url>
|
||||
<url><loc>https://manifold.markets/labs</loc><changefreq>daily</changefreq><priority>0.2</priority></url>
|
||||
</urlset>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 248 204" style="enable-background:new 0 0 248 204;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Logo_1_">
|
||||
<path id="white_background" class="st0" d="M221.95,51.29c0.15,2.17,0.15,4.34,0.15,6.53c0,66.73-50.8,143.69-143.69,143.69v-0.04
|
||||
C50.97,201.51,24.1,193.65,1,178.83c3.99,0.48,8,0.72,12.02,0.73c22.74,0.02,44.83-7.61,62.72-21.66
|
||||
c-21.61-0.41-40.56-14.5-47.18-35.07c7.57,1.46,15.37,1.16,22.8-0.87C27.8,117.2,10.85,96.5,10.85,72.46c0-0.22,0-0.43,0-0.64
|
||||
c7.02,3.91,14.88,6.08,22.92,6.32C11.58,63.31,4.74,33.79,18.14,10.71c25.64,31.55,63.47,50.73,104.08,52.76
|
||||
c-4.07-17.54,1.49-35.92,14.61-48.25c20.34-19.12,52.33-18.14,71.45,2.19c11.31-2.23,22.15-6.38,32.07-12.26
|
||||
c-3.77,11.69-11.66,21.62-22.2,27.93c10.01-1.18,19.79-3.86,29-7.95C240.37,35.29,231.83,44.14,221.95,51.29z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -18,6 +18,7 @@ module.exports = {
|
|||
colors: {
|
||||
'red-25': '#FDF7F6',
|
||||
'greyscale-1': '#FBFBFF',
|
||||
'greyscale-1.5': '#F4F4FB',
|
||||
'greyscale-2': '#E7E7F4',
|
||||
'greyscale-3': '#D8D8EB',
|
||||
'greyscale-4': '#B1B1C7',
|
||||
|
|
174
yarn.lock
174
yarn.lock
|
@ -1310,7 +1310,14 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.18.9":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
|
||||
integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.9.2":
|
||||
version "7.18.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
|
||||
integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==
|
||||
|
@ -2351,6 +2358,19 @@
|
|||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.6.1.tgz#d822792e589aac005462491dd62f86095e0c3bef"
|
||||
integrity sha512-gMd6uIs1U4Oz718Z5gFoV0o/vD43/4zvbyiJN9Dt7PK9Ubxn+TmJwTmYwyNJc5KxxU1t0CmgTNgwZX9+4NjCnQ==
|
||||
|
||||
"@hello-pangea/dnd@16.0.0":
|
||||
version "16.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-16.0.0.tgz#b97791286395924ffbdb4cd0f27f06f2985766d5"
|
||||
integrity sha512-FprEzwrGMvyclVf8pWTrPbUV7/ZFt6NmL76ePj1mMyZG195htDUkmvET6CBwKJTXmV+AE/GyK4Lv3wpCqrlY/g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.9"
|
||||
css-box-model "^1.2.1"
|
||||
memoize-one "^6.0.0"
|
||||
raf-schd "^4.0.3"
|
||||
react-redux "^8.0.2"
|
||||
redux "^4.2.0"
|
||||
use-memo-one "^1.1.2"
|
||||
|
||||
"@heroicons/react@1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.6.tgz#35dd26987228b39ef2316db3b1245c42eb19e324"
|
||||
|
@ -3434,7 +3454,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/hogan.js/-/hogan.js-3.0.1.tgz#64c54407b30da359763e14877f5702b8ae85d61c"
|
||||
integrity sha512-D03i/2OY7kGyMq9wdQ7oD8roE49z/ZCZThe/nbahtvuqCNZY9T2MfedOWyeBdbEpY2W8Gnh/dyJLdFtUCOkYbg==
|
||||
|
||||
"@types/hoist-non-react-statics@^3.3.0":
|
||||
"@types/hoist-non-react-statics@^3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
||||
|
@ -3567,30 +3587,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/react-beautiful-dnd@13.1.2":
|
||||
version "13.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz#510405abb09f493afdfd898bf83995dc6385c130"
|
||||
integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==
|
||||
"@types/react-dom@18.0.6":
|
||||
version "18.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.6.tgz#36652900024842b74607a17786b6662dd1e103a1"
|
||||
integrity sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-dom@17.0.2":
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.2.tgz#35654cf6c49ae162d5bc90843d5437dc38008d43"
|
||||
integrity sha512-Icd9KEgdnFfJs39KyRyr0jQ7EKhq8U6CcHRMGAS45fp5qgUvxL3ujUCfWFttUK2UErqZNj97t9gsVPNAqcwoCg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-redux@^7.1.20":
|
||||
version "7.1.24"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0"
|
||||
integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==
|
||||
dependencies:
|
||||
"@types/hoist-non-react-statics" "^3.3.0"
|
||||
"@types/react" "*"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
redux "^4.0.0"
|
||||
|
||||
"@types/react-router-config@*":
|
||||
version "5.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.6.tgz#87c5c57e72d241db900d9734512c50ccec062451"
|
||||
|
@ -3617,7 +3620,7 @@
|
|||
"@types/history" "^4.7.11"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@17.0.43", "@types/react@^17.0.2":
|
||||
"@types/react@*", "@types/react@^17.0.2":
|
||||
version "17.0.43"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55"
|
||||
integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==
|
||||
|
@ -3626,6 +3629,15 @@
|
|||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@18.0.21":
|
||||
version "18.0.21"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.21.tgz#b8209e9626bb00a34c76f55482697edd2b43cc67"
|
||||
integrity sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/retry@0.12.0":
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
||||
|
@ -3675,6 +3687,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
||||
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
||||
|
||||
"@types/use-sync-external-store@^0.0.3":
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
|
||||
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
|
||||
|
||||
"@types/ws@^8.5.1":
|
||||
version "8.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
|
||||
|
@ -5202,7 +5219,7 @@ crypto-random-string@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
|
||||
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
|
||||
|
||||
css-box-model@^1.2.0:
|
||||
css-box-model@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
|
||||
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
|
||||
|
@ -8683,10 +8700,10 @@ memfs@^3.1.2, memfs@^3.4.3:
|
|||
dependencies:
|
||||
fs-monkey "1.0.3"
|
||||
|
||||
memoize-one@^5.1.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
||||
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
||||
memoize-one@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
|
||||
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
|
||||
|
||||
merge-descriptors@1.0.1:
|
||||
version "1.0.1"
|
||||
|
@ -10413,7 +10430,7 @@ quick-lru@^5.1.1:
|
|||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
|
||||
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
|
||||
|
||||
raf-schd@^4.0.2:
|
||||
raf-schd@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
|
||||
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
|
||||
|
@ -10465,19 +10482,6 @@ react-base16-styling@^0.6.0:
|
|||
lodash.flow "^3.3.0"
|
||||
pure-color "^1.2.0"
|
||||
|
||||
react-beautiful-dnd@13.1.1:
|
||||
version "13.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2"
|
||||
integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.2"
|
||||
css-box-model "^1.2.0"
|
||||
memoize-one "^5.1.1"
|
||||
raf-schd "^4.0.2"
|
||||
react-redux "^7.2.0"
|
||||
redux "^4.0.4"
|
||||
use-memo-one "^1.1.1"
|
||||
|
||||
react-confetti@6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.0.1.tgz#d4f57b5a021dd908a6243b8f63b6009b00818d10"
|
||||
|
@ -10515,7 +10519,15 @@ react-dev-utils@^12.0.0:
|
|||
strip-ansi "^6.0.1"
|
||||
text-table "^0.2.0"
|
||||
|
||||
react-dom@17.0.2, react-dom@^17.0.1:
|
||||
react-dom@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react-dom@^17.0.1:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
|
||||
|
@ -10529,14 +10541,14 @@ react-error-overlay@^6.0.11:
|
|||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
|
||||
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
|
||||
|
||||
react-expanding-textarea@2.3.5:
|
||||
version "2.3.5"
|
||||
resolved "https://registry.yarnpkg.com/react-expanding-textarea/-/react-expanding-textarea-2.3.5.tgz#310c28ab242c724e042589ac3ea400dd68ad1488"
|
||||
integrity sha512-mPdtg3CxSgZFcsRLf80jueBWy1Zlh9AKy76S7dGXUKinvo4EypMavZa2iC0hEnLxY0tcwWR+n/K8B21TkttpUw==
|
||||
react-expanding-textarea@2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/react-expanding-textarea/-/react-expanding-textarea-2.3.6.tgz#daa50e5110dd71ca79e1df8e056b0b2eb0e8a84a"
|
||||
integrity sha512-LjkyZv1LilMlt+6/yYn9F1FlcK8iQU96myeq6PwU/a7IaQMkSwndSB1SAhMKgxSrUR71VbqsqnAHQ741WbAU/Q==
|
||||
dependencies:
|
||||
fast-shallow-equal "^1.0.0"
|
||||
react-with-forwarded-ref "^0.3.3"
|
||||
tslib "^2.0.3"
|
||||
react-with-forwarded-ref "^0.3.5"
|
||||
tslib "^2.4.0"
|
||||
|
||||
react-fast-compare@^3.2.0:
|
||||
version "3.2.0"
|
||||
|
@ -10585,10 +10597,10 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
|
|||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-is@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
react-is@^18.0.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||
|
||||
react-json-view@^1.21.3:
|
||||
version "1.21.3"
|
||||
|
@ -10626,17 +10638,17 @@ react-query@3.39.0:
|
|||
broadcast-channel "^3.4.1"
|
||||
match-sorter "^6.0.2"
|
||||
|
||||
react-redux@^7.2.0:
|
||||
version "7.2.8"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de"
|
||||
integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==
|
||||
react-redux@^8.0.2:
|
||||
version "8.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.4.tgz#80c31dffa8af9526967c4267022ae1525ff0e36a"
|
||||
integrity sha512-yMfQ7mX6bWuicz2fids6cR1YT59VTuT8MKyyE310wJQlINKENCeT1UcPdEiX6znI5tF8zXyJ/VYvDgeGuaaNwQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
"@types/react-redux" "^7.1.20"
|
||||
"@babel/runtime" "^7.12.1"
|
||||
"@types/hoist-non-react-statics" "^3.3.1"
|
||||
"@types/use-sync-external-store" "^0.0.3"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
react-is "^18.0.0"
|
||||
use-sync-external-store "^1.0.0"
|
||||
|
||||
react-router-config@^5.1.1:
|
||||
version "5.1.1"
|
||||
|
@ -10690,14 +10702,21 @@ react-twitter-embed@4.0.4:
|
|||
dependencies:
|
||||
scriptjs "^2.5.9"
|
||||
|
||||
react-with-forwarded-ref@^0.3.3:
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/react-with-forwarded-ref/-/react-with-forwarded-ref-0.3.4.tgz#b1e884ea081ec3c5dd578f37889159797454c0a5"
|
||||
integrity sha512-SRq/uTdTh+02JDwYzEEhY2aNNWl/CP2EKP2nQtXzhJw06w6PgYnJt2ObrebvFJu6+wGzX3vDHU3H/ux9hxyZUQ==
|
||||
react-with-forwarded-ref@^0.3.5:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/react-with-forwarded-ref/-/react-with-forwarded-ref-0.3.5.tgz#7d0bae2a9996fc91493f40ab179b8c54d29cfab9"
|
||||
integrity sha512-BJK4q0Nvqg4AFwc+LV+PZZb2nxS1ZqQlS9hY14TdIyg7Lapzirk/V/TtbYjPFSsm/fGm0wC4tsgI1IDhxSVVSQ==
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
|
||||
react@17.0.2, react@^17.0.1:
|
||||
react@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
react@^17.0.1:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
|
||||
|
@ -10789,7 +10808,7 @@ recursive-readdir@^2.2.2:
|
|||
dependencies:
|
||||
minimatch "3.0.4"
|
||||
|
||||
redux@^4.0.0, redux@^4.0.4:
|
||||
redux@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
|
||||
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
|
||||
|
@ -11169,6 +11188,13 @@ scheduler@^0.20.2:
|
|||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
scheduler@^0.23.0:
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
schema-utils@2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
|
||||
|
@ -12394,12 +12420,12 @@ use-latest@^1.2.1:
|
|||
dependencies:
|
||||
use-isomorphic-layout-effect "^1.1.1"
|
||||
|
||||
use-memo-one@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
|
||||
integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
|
||||
use-memo-one@^1.1.2:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99"
|
||||
integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==
|
||||
|
||||
use-sync-external-store@1.2.0:
|
||||
use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
|
Loading…
Reference in New Issue
Block a user