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 profit = payout + saleValue + redeemed - totalInvested
|
||||||
const profitPercent = (profit / totalInvested) * 100
|
const profitPercent = (profit / totalInvested) * 100
|
||||||
|
|
||||||
|
@ -221,8 +220,8 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invested,
|
invested,
|
||||||
|
loan,
|
||||||
payout,
|
payout,
|
||||||
netPayout,
|
|
||||||
profit,
|
profit,
|
||||||
profitPercent,
|
profitPercent,
|
||||||
totalShares,
|
totalShares,
|
||||||
|
@ -233,8 +232,8 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
||||||
export function getContractBetNullMetrics() {
|
export function getContractBetNullMetrics() {
|
||||||
return {
|
return {
|
||||||
invested: 0,
|
invested: 0,
|
||||||
|
loan: 0,
|
||||||
payout: 0,
|
payout: 0,
|
||||||
netPayout: 0,
|
|
||||||
profit: 0,
|
profit: 0,
|
||||||
profitPercent: 0,
|
profitPercent: 0,
|
||||||
totalShares: {} as { [outcome: string]: number },
|
totalShares: {} as { [outcome: string]: number },
|
||||||
|
|
|
@ -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.`,
|
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) => {
|
].map((charity) => {
|
||||||
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
|
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -30,7 +30,7 @@ export function contractTextDetails(contract: Contract) {
|
||||||
const { closeTime, groupLinks } = contract
|
const { closeTime, groupLinks } = contract
|
||||||
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(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 (
|
return (
|
||||||
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
|
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { JSONContent } from '@tiptap/core'
|
||||||
export type Post = {
|
export type Post = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
|
subtitle: string
|
||||||
content: JSONContent
|
content: JSONContent
|
||||||
creatorId: string // User id
|
creatorId: string // User id
|
||||||
createdTime: number
|
createdTime: number
|
||||||
|
@ -17,3 +18,4 @@ export type DateDoc = Post & {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MAX_POST_TITLE_LENGTH = 480
|
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) => {
|
dfs(newText, (current) => {
|
||||||
if (current.marks?.some((m) => m.type === TiptapSpoiler.name)) {
|
if (current.marks?.some((m) => m.type === TiptapSpoiler.name)) {
|
||||||
current.text = '[spoiler]'
|
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)
|
return generateText(newText, exhibitExts)
|
||||||
|
|
|
@ -3,7 +3,11 @@ import * as admin from 'firebase-admin'
|
||||||
import { getUser } from './utils'
|
import { getUser } from './utils'
|
||||||
import { slugify } from '../../common/util/slugify'
|
import { slugify } from '../../common/util/slugify'
|
||||||
import { randomString } from '../../common/util/random'
|
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 { APIError, newEndpoint, validate } from './api'
|
||||||
import { JSONContent } from '@tiptap/core'
|
import { JSONContent } from '@tiptap/core'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
@ -36,6 +40,7 @@ const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
|
||||||
|
|
||||||
const postSchema = z.object({
|
const postSchema = z.object({
|
||||||
title: z.string().min(1).max(MAX_POST_TITLE_LENGTH),
|
title: z.string().min(1).max(MAX_POST_TITLE_LENGTH),
|
||||||
|
subtitle: z.string().min(1).max(MAX_POST_SUBTITLE_LENGTH),
|
||||||
content: contentSchema,
|
content: contentSchema,
|
||||||
groupId: z.string().optional(),
|
groupId: z.string().optional(),
|
||||||
|
|
||||||
|
@ -48,10 +53,8 @@ const postSchema = z.object({
|
||||||
|
|
||||||
export const createpost = newEndpoint({}, async (req, auth) => {
|
export const createpost = newEndpoint({}, async (req, auth) => {
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
const { title, content, groupId, question, ...otherProps } = validate(
|
const { title, subtitle, content, groupId, question, ...otherProps } =
|
||||||
postSchema,
|
validate(postSchema, req.body)
|
||||||
req.body
|
|
||||||
)
|
|
||||||
|
|
||||||
const creator = await getUser(auth.uid)
|
const creator = await getUser(auth.uid)
|
||||||
if (!creator)
|
if (!creator)
|
||||||
|
@ -89,6 +92,7 @@ export const createpost = newEndpoint({}, async (req, auth) => {
|
||||||
creatorId: creator.id,
|
creatorId: creator.id,
|
||||||
slug,
|
slug,
|
||||||
title,
|
title,
|
||||||
|
subtitle,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
content: content,
|
content: content,
|
||||||
contractSlug,
|
contractSlug,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { APIError, newEndpoint } from './api'
|
import { APIError, newEndpoint } from './api'
|
||||||
import { sendPortfolioUpdateEmailsToAllUsers } from './weekly-portfolio-emails'
|
|
||||||
import { isProd } from './utils'
|
import { isProd } from './utils'
|
||||||
|
import { sendTrendingMarketsEmailsToAllUsers } from 'functions/src/weekly-markets-emails'
|
||||||
|
|
||||||
// Function for testing scheduled functions locally
|
// Function for testing scheduled functions locally
|
||||||
export const testscheduledfunction = newEndpoint(
|
export const testscheduledfunction = newEndpoint(
|
||||||
|
@ -10,7 +10,7 @@ export const testscheduledfunction = newEndpoint(
|
||||||
throw new APIError(400, 'This function is only available in dev mode')
|
throw new APIError(400, 'This function is only available in dev mode')
|
||||||
|
|
||||||
// Replace your function here
|
// Replace your function here
|
||||||
await sendPortfolioUpdateEmailsToAllUsers()
|
await sendTrendingMarketsEmailsToAllUsers()
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,17 +34,20 @@ export const unsubscribe: EndpointDefinition = {
|
||||||
const previousDestinations =
|
const previousDestinations =
|
||||||
user.notificationPreferences[notificationSubscriptionType]
|
user.notificationPreferences[notificationSubscriptionType]
|
||||||
|
|
||||||
|
let newDestinations = previousDestinations
|
||||||
|
if (wantsToOptOutAll) newDestinations.push('email')
|
||||||
|
else
|
||||||
|
newDestinations = previousDestinations.filter(
|
||||||
|
(destination) => destination !== 'email'
|
||||||
|
)
|
||||||
|
|
||||||
console.log(previousDestinations)
|
console.log(previousDestinations)
|
||||||
const { email } = user
|
const { email } = user
|
||||||
|
|
||||||
const update: Partial<PrivateUser> = {
|
const update: Partial<PrivateUser> = {
|
||||||
notificationPreferences: {
|
notificationPreferences: {
|
||||||
...user.notificationPreferences,
|
...user.notificationPreferences,
|
||||||
[notificationSubscriptionType]: wantsToOptOutAll
|
[notificationSubscriptionType]: newDestinations,
|
||||||
? previousDestinations.push('email')
|
|
||||||
: previousDestinations.filter(
|
|
||||||
(destination) => destination !== 'email'
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +63,7 @@ export const unsubscribe: EndpointDefinition = {
|
||||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Manifold Markets 7th Day Anniversary Gift!</title>
|
<title>Unsubscribe from Manifold Markets emails</title>
|
||||||
<!--[if !mso]><!-->
|
<!--[if !mso]><!-->
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<!--<![endif]-->
|
<!--<![endif]-->
|
||||||
|
@ -213,7 +216,7 @@ export const unsubscribe: EndpointDefinition = {
|
||||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Manifold Markets 7th Day Anniversary Gift!</title>
|
<title>Unsubscribe from Manifold Markets emails</title>
|
||||||
<!--[if !mso]><!-->
|
<!--[if !mso]><!-->
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<!--<![endif]-->
|
<!--<![endif]-->
|
||||||
|
|
|
@ -4,21 +4,24 @@ import * as admin from 'firebase-admin'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import {
|
import {
|
||||||
getAllPrivateUsers,
|
getAllPrivateUsers,
|
||||||
|
getGroup,
|
||||||
getPrivateUser,
|
getPrivateUser,
|
||||||
getUser,
|
getUser,
|
||||||
getValues,
|
getValues,
|
||||||
isProd,
|
isProd,
|
||||||
log,
|
log,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { sendInterestingMarketsEmail } from './emails'
|
|
||||||
import { createRNG, shuffle } from '../../common/util/random'
|
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 { filterDefined } from '../../common/util/array'
|
||||||
|
import { Follow } from '../../common/follow'
|
||||||
|
import { countBy, uniq, uniqBy } from 'lodash'
|
||||||
|
import { sendInterestingMarketsEmail } from './emails'
|
||||||
|
|
||||||
export const weeklyMarketsEmails = functions
|
export const weeklyMarketsEmails = functions
|
||||||
.runWith({ secrets: ['MAILGUN_KEY'], memory: '4GB' })
|
.runWith({ secrets: ['MAILGUN_KEY'], memory: '4GB' })
|
||||||
// every minute on Monday for an hour at 12pm PT (UTC -07:00)
|
// every minute on Monday for 2 hours starting at 12pm PT (UTC -07:00)
|
||||||
.pubsub.schedule('* 19 * * 1')
|
.pubsub.schedule('* 19-20 * * 1')
|
||||||
.timeZone('Etc/UTC')
|
.timeZone('Etc/UTC')
|
||||||
.onRun(async () => {
|
.onRun(async () => {
|
||||||
await sendTrendingMarketsEmailsToAllUsers()
|
await sendTrendingMarketsEmailsToAllUsers()
|
||||||
|
@ -40,20 +43,30 @@ export async function getTrendingContracts() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendTrendingMarketsEmailsToAllUsers() {
|
export async function sendTrendingMarketsEmailsToAllUsers() {
|
||||||
const numContractsToSend = 6
|
const numContractsToSend = 6
|
||||||
const privateUsers = isProd()
|
const privateUsers = isProd()
|
||||||
? await getAllPrivateUsers()
|
? await getAllPrivateUsers()
|
||||||
: filterDefined([await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2')])
|
: filterDefined([
|
||||||
// get all users that haven't unsubscribed from weekly emails
|
await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2'), // dev Ian
|
||||||
|
])
|
||||||
const privateUsersToSendEmailsTo = privateUsers
|
const privateUsersToSendEmailsTo = privateUsers
|
||||||
.filter((user) => {
|
// Get all users that haven't unsubscribed from weekly emails
|
||||||
return (
|
.filter(
|
||||||
|
(user) =>
|
||||||
user.notificationPreferences.trending_markets.includes('email') &&
|
user.notificationPreferences.trending_markets.includes('email') &&
|
||||||
!user.weeklyTrendingEmailSent
|
!user.weeklyTrendingEmailSent
|
||||||
)
|
)
|
||||||
})
|
.slice(0, 90) // Send the emails out in batches
|
||||||
.slice(150) // 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(
|
log(
|
||||||
'Sending weekly trending emails to',
|
'Sending weekly trending emails to',
|
||||||
privateUsersToSendEmailsTo.length,
|
privateUsersToSendEmailsTo.length,
|
||||||
|
@ -70,42 +83,358 @@ async function sendTrendingMarketsEmailsToAllUsers() {
|
||||||
!contract.groupSlugs?.includes('manifold-features') &&
|
!contract.groupSlugs?.includes('manifold-features') &&
|
||||||
!contract.groupSlugs?.includes('manifold-6748e065087e')
|
!contract.groupSlugs?.includes('manifold-6748e065087e')
|
||||||
)
|
)
|
||||||
.slice(0, 20)
|
.slice(0, 50)
|
||||||
log(
|
|
||||||
`Found ${trendingContracts.length} trending contracts:\n`,
|
|
||||||
trendingContracts.map((c) => c.question).join('\n ')
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: convert to Promise.all
|
const uniqueTrendingContracts = removeSimilarQuestions(
|
||||||
for (const privateUser of privateUsersToSendEmailsTo) {
|
trendingContracts,
|
||||||
|
trendingContracts,
|
||||||
|
true
|
||||||
|
).slice(0, 20)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
privateUsersToSendEmailsTo.map(async (privateUser) => {
|
||||||
if (!privateUser.email) {
|
if (!privateUser.email) {
|
||||||
log(`No email for ${privateUser.username}`)
|
log(`No email for ${privateUser.username}`)
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
const contractsAvailableToSend = trendingContracts.filter((contract) => {
|
|
||||||
return !contract.uniqueBettorIds?.includes(privateUser.id)
|
const unbetOnFollowedMarkets = await getUserUnBetOnFollowsMarkets(
|
||||||
})
|
privateUser.id
|
||||||
if (contractsAvailableToSend.length < numContractsToSend) {
|
)
|
||||||
log('not enough new, unbet-on contracts to send to user', 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({
|
await firestore.collection('private-users').doc(privateUser.id).update({
|
||||||
weeklyTrendingEmailSent: true,
|
weeklyTrendingEmailSent: true,
|
||||||
})
|
})
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
// choose random subset of contracts to send to user
|
// choose random subset of contracts to send to user
|
||||||
const contractsToSend = chooseRandomSubset(
|
const contractsToSend = chooseRandomSubset(
|
||||||
contractsAvailableToSend,
|
marketsAvailableToSend,
|
||||||
numContractsToSend
|
numContractsToSend
|
||||||
)
|
)
|
||||||
|
|
||||||
const user = await getUser(privateUser.id)
|
const user = await getUser(privateUser.id)
|
||||||
if (!user) continue
|
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 sendInterestingMarketsEmail(user, privateUser, contractsToSend)
|
||||||
await firestore.collection('private-users').doc(user.id).update({
|
await firestore.collection('private-users').doc(user.id).update({
|
||||||
weeklyTrendingEmailSent: true,
|
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
|
const fiveMinutes = 5 * 60 * 1000
|
||||||
|
@ -116,3 +445,40 @@ function chooseRandomSubset(contracts: Contract[], count: number) {
|
||||||
shuffle(contracts, rng)
|
shuffle(contracts, rng)
|
||||||
return contracts.slice(0, count)
|
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,
|
numericValue,
|
||||||
resolution,
|
resolution,
|
||||||
} = parsedReq
|
} = parsedReq
|
||||||
const MAX_QUESTION_CHARS = 100
|
|
||||||
const truncatedQuestion =
|
|
||||||
question.length > MAX_QUESTION_CHARS
|
|
||||||
? question.slice(0, MAX_QUESTION_CHARS) + '...'
|
|
||||||
: question
|
|
||||||
const hideAvatar = creatorAvatarUrl ? '' : 'hidden'
|
const hideAvatar = creatorAvatarUrl ? '' : 'hidden'
|
||||||
|
|
||||||
let resolutionColor = 'text-primary'
|
let resolutionColor = 'text-primary'
|
||||||
|
@ -69,7 +64,7 @@ export function getHtml(parsedReq: ParsedRequest) {
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Generated Image</title>
|
<title>Generated Image</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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>
|
</head>
|
||||||
<style>
|
<style>
|
||||||
${getTemplateCss(theme, fontSize)}
|
${getTemplateCss(theme, fontSize)}
|
||||||
|
@ -109,8 +104,8 @@ export function getHtml(parsedReq: ParsedRequest) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row justify-between gap-12 pt-36">
|
<div class="flex flex-row justify-between gap-12 pt-36">
|
||||||
<div class="text-indigo-700 text-6xl leading-tight">
|
<div class="text-indigo-700 text-6xl leading-tight line-clamp-4">
|
||||||
${truncatedQuestion}
|
${question}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
${
|
${
|
||||||
|
@ -127,7 +122,7 @@ export function getHtml(parsedReq: ParsedRequest) {
|
||||||
|
|
||||||
<!-- Metadata -->
|
<!-- Metadata -->
|
||||||
<div class="absolute bottom-16">
|
<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}
|
${metadata}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,8 +24,5 @@
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"typescript": "4.8.2"
|
"typescript": "4.8.2"
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"@types/react": "17.0.43"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function SEO(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Head>
|
<Head>
|
||||||
<title>{title} | Manifold Markets</title>
|
<title>{`${title} | Manifold Markets`}</title>
|
||||||
|
|
||||||
<meta
|
<meta
|
||||||
property="og:title"
|
property="og:title"
|
||||||
|
|
|
@ -192,6 +192,7 @@ export function AnswerBetPanel(props: {
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
disabled={!!betDisabled}
|
disabled={!!betDisabled}
|
||||||
color={'indigo'}
|
color={'indigo'}
|
||||||
|
actionLabel="Buy"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<BetSignUpPrompt />
|
<BetSignUpPrompt />
|
||||||
|
|
|
@ -20,11 +20,11 @@ import { AnswerBetPanel } from 'web/components/answers/answer-bet-panel'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { Avatar } from 'web/components/avatar'
|
import { Avatar } from 'web/components/avatar'
|
||||||
import { Linkify } from 'web/components/linkify'
|
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 { Button } from 'web/components/button'
|
||||||
import { useAdmin } from 'web/hooks/use-admin'
|
import { useAdmin } from 'web/hooks/use-admin'
|
||||||
import { needsAdminToResolve } from 'web/pages/[username]/[contractSlug]'
|
import { needsAdminToResolve } from 'web/pages/[username]/[contractSlug]'
|
||||||
|
import { CATEGORY_COLORS } from '../charts/contract/choice'
|
||||||
|
import { useChartAnswers } from '../charts/contract/choice'
|
||||||
|
|
||||||
export function AnswersPanel(props: {
|
export function AnswersPanel(props: {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
|
@ -38,6 +38,7 @@ export function AnswersPanel(props: {
|
||||||
const answers = (useAnswers(contract.id) ?? contract.answers).filter(
|
const answers = (useAnswers(contract.id) ?? contract.answers).filter(
|
||||||
(a) => a.number != 0 || contract.outcomeType === 'MULTIPLE_CHOICE'
|
(a) => a.number != 0 || contract.outcomeType === 'MULTIPLE_CHOICE'
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasZeroBetAnswers = answers.some((answer) => totalBets[answer.id] < 1)
|
const hasZeroBetAnswers = answers.some((answer) => totalBets[answer.id] < 1)
|
||||||
|
|
||||||
const [winningAnswers, losingAnswers] = partition(
|
const [winningAnswers, losingAnswers] = partition(
|
||||||
|
@ -104,6 +105,10 @@ export function AnswersPanel(props: {
|
||||||
? 'checkbox'
|
? 'checkbox'
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
const colorSortedAnswer = useChartAnswers(contract).map(
|
||||||
|
(value, _index) => value.text
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-3">
|
<Col className="gap-3">
|
||||||
{(resolveOption || resolution) &&
|
{(resolveOption || resolution) &&
|
||||||
|
@ -128,7 +133,12 @@ export function AnswersPanel(props: {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{answerItems.map((item) => (
|
{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 && (
|
{hasZeroBetAnswers && !showAllAnswers && (
|
||||||
<Button
|
<Button
|
||||||
|
@ -174,15 +184,18 @@ export function AnswersPanel(props: {
|
||||||
function OpenAnswer(props: {
|
function OpenAnswer(props: {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
answer: Answer
|
answer: Answer
|
||||||
|
colorIndex: number | undefined
|
||||||
}) {
|
}) {
|
||||||
const { answer, contract } = props
|
const { answer, contract, colorIndex } = props
|
||||||
const { username, avatarUrl, name, text } = answer
|
const { username, avatarUrl, text } = answer
|
||||||
const prob = getDpmOutcomeProbability(contract.totalShares, answer.id)
|
const prob = getDpmOutcomeProbability(contract.totalShares, answer.id)
|
||||||
const probPercent = formatPercent(prob)
|
const probPercent = formatPercent(prob)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const color =
|
||||||
|
colorIndex != undefined ? CATEGORY_COLORS[colorIndex] : '#B1B1C7'
|
||||||
|
|
||||||
return (
|
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">
|
<Modal open={open} setOpen={setOpen} position="center">
|
||||||
<AnswerBetPanel
|
<AnswerBetPanel
|
||||||
answer={answer}
|
answer={answer}
|
||||||
|
@ -193,40 +206,44 @@ function OpenAnswer(props: {
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div
|
<Col
|
||||||
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(
|
className={clsx(
|
||||||
'text-2xl',
|
'bg-greyscale-1 relative w-full rounded-lg transition-all',
|
||||||
tradingAllowed(contract) ? 'text-primary' : 'text-gray-500'
|
tradingAllowed(contract) ? 'text-greyscale-7' : 'text-greyscale-5'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{probPercent}
|
<Row className="z-20 -mb-1 justify-between gap-2 py-2 px-3">
|
||||||
</span>
|
<Row>
|
||||||
<BuyButton
|
<Avatar
|
||||||
className={clsx(
|
className="mt-0.5 mr-2 inline h-5 w-5 border border-transparent transition-transform hover:border-none"
|
||||||
'btn-sm flex-initial !px-6 sm:flex',
|
username={username}
|
||||||
tradingAllowed(contract) ? '' : '!hidden'
|
avatarUrl={avatarUrl}
|
||||||
)}
|
/>
|
||||||
onClick={() => setOpen(true)}
|
<Linkify
|
||||||
|
className="text-md cursor-pointer whitespace-pre-line"
|
||||||
|
text={text}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
<Row className="gap-2">
|
||||||
</Col>
|
<div className="my-auto text-xl">{probPercent}</div>
|
||||||
|
{tradingAllowed(contract) && (
|
||||||
|
<Button
|
||||||
|
size="2xs"
|
||||||
|
color="gray-outline"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="my-auto"
|
||||||
|
>
|
||||||
|
BUY
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Row>
|
</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>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx'
|
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 { MenuIcon } from '@heroicons/react/solid'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
|
|
||||||
|
|
|
@ -68,11 +68,11 @@ export function AuthProvider(props: {
|
||||||
}, [setAuthUser, serverUser])
|
}, [setAuthUser, serverUser])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authUser != null) {
|
if (authUser) {
|
||||||
// Persist to local storage, to reduce login blink next time.
|
// Persist to local storage, to reduce login blink next time.
|
||||||
// Note: Cap on localStorage size is ~5mb
|
// Note: Cap on localStorage size is ~5mb
|
||||||
localStorage.setItem(CACHED_USER_KEY, JSON.stringify(authUser))
|
localStorage.setItem(CACHED_USER_KEY, JSON.stringify(authUser))
|
||||||
} else {
|
} else if (authUser === null) {
|
||||||
localStorage.removeItem(CACHED_USER_KEY)
|
localStorage.removeItem(CACHED_USER_KEY)
|
||||||
}
|
}
|
||||||
}, [authUser])
|
}, [authUser])
|
||||||
|
|
|
@ -25,7 +25,7 @@ import {
|
||||||
NoLabel,
|
NoLabel,
|
||||||
YesLabel,
|
YesLabel,
|
||||||
} from './outcome-label'
|
} from './outcome-label'
|
||||||
import { getProbability } from 'common/calculate'
|
import { getContractBetMetrics, getProbability } from 'common/calculate'
|
||||||
import { useFocus } from 'web/hooks/use-focus'
|
import { useFocus } from 'web/hooks/use-focus'
|
||||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||||
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
||||||
|
@ -395,6 +395,7 @@ export function BuyPanel(props: {
|
||||||
disabled={!!betDisabled || outcome === undefined}
|
disabled={!!betDisabled || outcome === undefined}
|
||||||
size="xl"
|
size="xl"
|
||||||
color={outcome === 'NO' ? 'red' : 'green'}
|
color={outcome === 'NO' ? 'red' : 'green'}
|
||||||
|
actionLabel="Wager"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
@ -831,13 +832,21 @@ export function SellPanel(props: {
|
||||||
|
|
||||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
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
|
// Sell all shares if remaining shares would be < 1
|
||||||
const isSellingAllShares = amount === Math.floor(shares)
|
const isSellingAllShares = amount === Math.floor(shares)
|
||||||
|
|
||||||
const sellQuantity = isSellingAllShares ? shares : amount
|
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() {
|
async function submitSell() {
|
||||||
if (!user || !amount) return
|
if (!user || !amount) return
|
||||||
|
|
||||||
|
@ -882,8 +891,23 @@ export function SellPanel(props: {
|
||||||
sharesOutcome,
|
sharesOutcome,
|
||||||
unfilledBets
|
unfilledBets
|
||||||
)
|
)
|
||||||
|
const netProceeds = saleValue - loanPaid
|
||||||
|
const profit = saleValue - costBasis
|
||||||
const resultProb = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
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 openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
const [yesBets, noBets] = partition(
|
const [yesBets, noBets] = partition(
|
||||||
openUserBets,
|
openUserBets,
|
||||||
|
@ -923,14 +947,18 @@ export function SellPanel(props: {
|
||||||
label="Qty"
|
label="Qty"
|
||||||
error={error}
|
error={error}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
inputClassName="w-full"
|
inputClassName="w-full ml-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Col className="mt-3 w-full gap-3 text-sm">
|
<Col className="mt-3 w-full gap-3 text-sm">
|
||||||
<Row className="items-center justify-between gap-2 text-gray-500">
|
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||||
Sale proceeds
|
Sale amount
|
||||||
<span className="text-neutral">{formatMoney(saleValue)}</span>
|
<span className="text-neutral">{formatMoney(saleValue)}</span>
|
||||||
</Row>
|
</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">
|
<Row className="items-center justify-between">
|
||||||
<div className="text-gray-500">
|
<div className="text-gray-500">
|
||||||
{isPseudoNumeric ? 'Estimated value' : 'Probability'}
|
{isPseudoNumeric ? 'Estimated value' : 'Probability'}
|
||||||
|
@ -941,24 +969,33 @@ export function SellPanel(props: {
|
||||||
{format(resultProb)}
|
{format(resultProb)}
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</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>
|
</Col>
|
||||||
|
|
||||||
<Spacer h={8} />
|
<Spacer h={8} />
|
||||||
|
|
||||||
<button
|
<WarningConfirmationButton
|
||||||
className={clsx(
|
marketType="binary"
|
||||||
'btn flex-1',
|
amount={undefined}
|
||||||
betDisabled
|
warning={warning}
|
||||||
? 'btn-disabled'
|
isSubmitting={isSubmitting}
|
||||||
: sharesOutcome === 'YES'
|
onSubmit={betDisabled ? undefined : submitSell}
|
||||||
? 'btn-primary'
|
disabled={!!betDisabled}
|
||||||
: 'border-none bg-red-400 hover:bg-red-500',
|
size="xl"
|
||||||
isSubmitting ? 'loading' : ''
|
color="blue"
|
||||||
)}
|
actionLabel={`Sell ${Math.floor(soldShares)} shares`}
|
||||||
onClick={betDisabled ? undefined : submitSell}
|
/>
|
||||||
>
|
|
||||||
{isSubmitting ? 'Submitting...' : 'Submit sell'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{wasSubmitted && <div className="mt-4">Sell submitted!</div>}
|
{wasSubmitted && <div className="mt-4">Sell submitted!</div>}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -160,26 +160,31 @@ export function BetsList(props: { user: User }) {
|
||||||
unsettled,
|
unsettled,
|
||||||
(c) => contractsMetrics[c.id].payout
|
(c) => contractsMetrics[c.id].payout
|
||||||
)
|
)
|
||||||
const currentNetInvestment = sumBy(
|
const currentLoan = sumBy(unsettled, (c) => contractsMetrics[c.id].loan)
|
||||||
unsettled,
|
|
||||||
(c) => contractsMetrics[c.id].netPayout
|
|
||||||
)
|
|
||||||
|
|
||||||
const investedProfitPercent =
|
const investedProfitPercent =
|
||||||
((currentBetsValue - currentInvested) / (currentInvested + 0.1)) * 100
|
((currentBetsValue - currentInvested) / (currentInvested + 0.1)) * 100
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<Row className="justify-between gap-4 sm:flex-row">
|
<Col className="justify-between gap-4 sm:flex-row">
|
||||||
|
<Row className="gap-4">
|
||||||
<Col>
|
<Col>
|
||||||
<div className="text-greyscale-6 text-xs sm:text-sm">
|
<div className="text-greyscale-6 text-xs sm:text-sm">
|
||||||
Investment value
|
Investment value
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg">
|
<div className="text-lg">
|
||||||
{formatMoney(currentNetInvestment)}{' '}
|
{formatMoney(currentBetsValue)}{' '}
|
||||||
<ProfitBadge profitPercent={investedProfitPercent} />
|
<ProfitBadge profitPercent={investedProfitPercent} />
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</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">
|
<Row className="gap-2">
|
||||||
<select
|
<select
|
||||||
|
@ -206,7 +211,7 @@ export function BetsList(props: { user: User }) {
|
||||||
<option value="closeTime">Close date</option>
|
<option value="closeTime">Close date</option>
|
||||||
</select>
|
</select>
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Col>
|
||||||
|
|
||||||
<Col className="mt-6 divide-y">
|
<Col className="mt-6 divide-y">
|
||||||
{displayedContracts.length === 0 ? (
|
{displayedContracts.length === 0 ? (
|
||||||
|
@ -612,7 +617,7 @@ function SellButton(props: {
|
||||||
label: 'Sell',
|
label: 'Sell',
|
||||||
disabled: isSubmitting,
|
disabled: isSubmitting,
|
||||||
}}
|
}}
|
||||||
submitBtn={{ className: 'btn-primary', label: 'Sell' }}
|
submitBtn={{ label: 'Sell', color: 'green' }}
|
||||||
onSubmit={async () => {
|
onSubmit={async () => {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
await sellBet({ contractId: contract.id, betId: bet.id })
|
await sellBet({ contractId: contract.id, betId: bet.id })
|
||||||
|
|
|
@ -10,16 +10,54 @@ export type ColorType =
|
||||||
| 'indigo'
|
| 'indigo'
|
||||||
| 'yellow'
|
| 'yellow'
|
||||||
| 'gray'
|
| 'gray'
|
||||||
|
| 'gray-outline'
|
||||||
| 'gradient'
|
| 'gradient'
|
||||||
| 'gray-white'
|
| 'gray-white'
|
||||||
| 'highlight-blue'
|
| '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: {
|
export function Button(props: {
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: MouseEventHandler<any> | undefined
|
onClick?: MouseEventHandler<any> | undefined
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
size?: SizeType
|
size?: SizeType
|
||||||
color?: ColorType
|
color?: ColorType | 'override'
|
||||||
type?: 'button' | 'reset' | 'submit'
|
type?: 'button' | 'reset' | 'submit'
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
|
@ -35,42 +73,10 @@ export function Button(props: {
|
||||||
loading,
|
loading,
|
||||||
} = props
|
} = 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 (
|
return (
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
className={clsx(
|
className={clsx(buttonClass(size, color), className)}
|
||||||
'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
|
|
||||||
)}
|
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
onClick={onClick}
|
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 clsx from 'clsx'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { LinkIcon, SwitchVerticalIcon } from '@heroicons/react/outline'
|
import { SwitchVerticalIcon } from '@heroicons/react/outline'
|
||||||
import toast from 'react-hot-toast'
|
|
||||||
|
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
|
@ -16,7 +15,6 @@ import { SiteLink } from 'web/components/site-link'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { NoLabel, YesLabel } from '../outcome-label'
|
import { NoLabel, YesLabel } from '../outcome-label'
|
||||||
import { QRCode } from '../qr-code'
|
import { QRCode } from '../qr-code'
|
||||||
import { copyToClipboard } from 'web/lib/util/copy'
|
|
||||||
import { AmountInput } from '../amount-input'
|
import { AmountInput } from '../amount-input'
|
||||||
import { getProbability } from 'common/calculate'
|
import { getProbability } from 'common/calculate'
|
||||||
import { createMarket } from 'web/lib/firebase/api'
|
import { createMarket } from 'web/lib/firebase/api'
|
||||||
|
@ -26,6 +24,7 @@ import Textarea from 'react-expanding-textarea'
|
||||||
import { useTextEditor } from 'web/components/editor'
|
import { useTextEditor } from 'web/components/editor'
|
||||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
import { CopyLinkButton } from '../copy-link-button'
|
||||||
|
|
||||||
type challengeInfo = {
|
type challengeInfo = {
|
||||||
amount: number
|
amount: number
|
||||||
|
@ -302,16 +301,7 @@ function CreateChallengeForm(props: {
|
||||||
<Title className="!my-0" text="Challenge Created!" />
|
<Title className="!my-0" text="Challenge Created!" />
|
||||||
|
|
||||||
<div>Share the challenge using the link.</div>
|
<div>Share the challenge using the link.</div>
|
||||||
<button
|
<CopyLinkButton url={challengeSlug} />
|
||||||
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>
|
|
||||||
|
|
||||||
<QRCode url={challengeSlug} className="self-center" />
|
<QRCode url={challengeSlug} className="self-center" />
|
||||||
<Row className={'gap-1 text-gray-500'}>
|
<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 { useCharityTxns } from 'web/hooks/use-charity-txns'
|
||||||
import { manaToUSD } from '../../../common/util/format'
|
import { manaToUSD } from '../../../common/util/format'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
|
import { Col } from '../layout/col'
|
||||||
|
import { Card } from '../card'
|
||||||
|
|
||||||
export function CharityCard(props: { charity: Charity; match?: number }) {
|
export function CharityCard(props: { charity: Charity; match?: number }) {
|
||||||
const { charity } = props
|
const { charity } = props
|
||||||
|
@ -15,8 +17,9 @@ export function CharityCard(props: { charity: Charity; match?: number }) {
|
||||||
const raised = sumBy(txns, (txn) => txn.amount)
|
const raised = sumBy(txns, (txn) => txn.amount)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/charity/${slug}`} passHref>
|
<Link href={`/charity/${slug}`}>
|
||||||
<div className="card card-compact transition:shadow flex-1 cursor-pointer border-2 bg-white hover:shadow-md">
|
<a className="flex-1">
|
||||||
|
<Card className="!rounded-2xl">
|
||||||
<Row className="mt-6 mb-2">
|
<Row className="mt-6 mb-2">
|
||||||
{tags?.includes('Featured') && <FeaturedBadge />}
|
{tags?.includes('Featured') && <FeaturedBadge />}
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -29,8 +32,7 @@ export function CharityCard(props: { charity: Charity; match?: number }) {
|
||||||
)}
|
)}
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<Col className="p-8">
|
||||||
{/* <h3 className="card-title line-clamp-3">{name}</h3> */}
|
|
||||||
<div className="line-clamp-4 text-sm">{preview}</div>
|
<div className="line-clamp-4 text-sm">{preview}</div>
|
||||||
{raised > 0 && (
|
{raised > 0 && (
|
||||||
<>
|
<>
|
||||||
|
@ -50,8 +52,9 @@ export function CharityCard(props: { charity: Charity; match?: number }) {
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Col>
|
||||||
</div>
|
</Card>
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,9 +31,9 @@ const getBetPoints = (bets: Bet[]) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const BinaryChartTooltip = (props: TooltipProps<Date, HistoryPoint<Bet>>) => {
|
const BinaryChartTooltip = (props: TooltipProps<Date, HistoryPoint<Bet>>) => {
|
||||||
const { data, mouseX, xScale } = props
|
const { data, x, xScale } = props
|
||||||
const [start, end] = xScale.domain()
|
const [start, end] = xScale.domain()
|
||||||
const d = xScale.invert(mouseX)
|
const d = xScale.invert(x)
|
||||||
return (
|
return (
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
{data.obj && <Avatar size="xs" avatarUrl={data.obj.userAvatarUrl} />}
|
{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 { Row } from 'web/components/layout/row'
|
||||||
import { Avatar } from 'web/components/avatar'
|
import { Avatar } from 'web/components/avatar'
|
||||||
|
|
||||||
// thanks to https://observablehq.com/@jonhelfman/optimal-orders-for-choosing-categorical-colors
|
export const CATEGORY_COLORS = [
|
||||||
const CATEGORY_COLORS = [
|
'#7eb0d5',
|
||||||
'#00b8dd',
|
'#fd7f6f',
|
||||||
'#eecafe',
|
'#b2e061',
|
||||||
'#874c62',
|
'#bd7ebe',
|
||||||
'#6457ca',
|
'#ffb55a',
|
||||||
'#f773ba',
|
'#ffee65',
|
||||||
'#9c6bbc',
|
'#beb9db',
|
||||||
'#a87744',
|
'#fdcce5',
|
||||||
'#af8a04',
|
'#8bd3c7',
|
||||||
'#bff9aa',
|
'#bddfb7',
|
||||||
'#f3d89d',
|
'#e2e3f3',
|
||||||
'#c9a0f5',
|
'#fafafa',
|
||||||
'#ff00e5',
|
'#9fcdeb',
|
||||||
'#9dc6f7',
|
'#d3d3d3',
|
||||||
'#824475',
|
'#b1a296',
|
||||||
'#d973cc',
|
'#e1bdb6',
|
||||||
'#bc6808',
|
'#f2dbc0',
|
||||||
'#056e70',
|
'#fae5d3',
|
||||||
'#677932',
|
'#c5e0ec',
|
||||||
'#00b287',
|
'#e0f0ff',
|
||||||
'#c8ab6c',
|
'#ffddcd',
|
||||||
'#a2fb7a',
|
'#fbd5e2',
|
||||||
'#f8db68',
|
'#f2e7e5',
|
||||||
'#14675a',
|
'#ffe7ba',
|
||||||
'#8288f4',
|
'#eed9c4',
|
||||||
'#fe1ca0',
|
'#ea9999',
|
||||||
'#ad6aff',
|
'#f9cb9c',
|
||||||
'#786306',
|
'#ffe599',
|
||||||
'#9bfbaf',
|
'#b6d7a8',
|
||||||
'#b00cf7',
|
'#a2c4c9',
|
||||||
'#2f7ec5',
|
'#9fc5e8',
|
||||||
'#4b998b',
|
'#b4a7d6',
|
||||||
'#42fa0e',
|
'#d5a6bd',
|
||||||
'#5b80a1',
|
'#e06666',
|
||||||
'#962d9d',
|
'#f6b26b',
|
||||||
'#3385ff',
|
'#ffd966',
|
||||||
'#48c5ab',
|
'#93c47d',
|
||||||
'#b2c873',
|
'#76a5af',
|
||||||
'#4cf9a4',
|
'#6fa8dc',
|
||||||
'#00ffff',
|
'#8e7cc3',
|
||||||
'#3cca73',
|
'#c27ba0',
|
||||||
'#99ae17',
|
'#cc0000',
|
||||||
'#7af5cf',
|
'#e69138',
|
||||||
'#52af45',
|
'#f1c232',
|
||||||
'#fbb80f',
|
'#6aa84f',
|
||||||
'#29971b',
|
'#45818e',
|
||||||
'#187c9a',
|
'#3d85c6',
|
||||||
'#00d539',
|
'#674ea7',
|
||||||
'#bbfa1a',
|
'#a64d79',
|
||||||
'#61f55c',
|
'#990000',
|
||||||
'#cabc03',
|
'#b45f06',
|
||||||
'#ff9000',
|
'#bf9000',
|
||||||
'#779100',
|
|
||||||
'#bcfd6f',
|
|
||||||
'#70a560',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const MARGIN = { top: 20, right: 10, bottom: 20, left: 40 }
|
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: {
|
export const ChoiceContractChart = (props: {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
|
@ -153,10 +159,7 @@ export const ChoiceContractChart = (props: {
|
||||||
}) => {
|
}) => {
|
||||||
const { contract, bets, width, height, onMouseOver } = props
|
const { contract, bets, width, height, onMouseOver } = props
|
||||||
const [start, end] = getDateRange(contract)
|
const [start, end] = getDateRange(contract)
|
||||||
const answers = useMemo(
|
const answers = useChartAnswers(contract)
|
||||||
() => getTrackedAnswers(contract, CATEGORY_COLORS.length),
|
|
||||||
[contract]
|
|
||||||
)
|
|
||||||
const betPoints = useMemo(() => getBetPoints(answers, bets), [answers, bets])
|
const betPoints = useMemo(() => getBetPoints(answers, bets), [answers, bets])
|
||||||
const data = useMemo(
|
const data = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
@ -180,9 +183,9 @@ export const ChoiceContractChart = (props: {
|
||||||
|
|
||||||
const ChoiceTooltip = useMemo(
|
const ChoiceTooltip = useMemo(
|
||||||
() => (props: TooltipProps<Date, MultiPoint<Bet>>) => {
|
() => (props: TooltipProps<Date, MultiPoint<Bet>>) => {
|
||||||
const { data, mouseX, xScale } = props
|
const { data, x, xScale } = props
|
||||||
const [start, end] = xScale.domain()
|
const [start, end] = xScale.domain()
|
||||||
const d = xScale.invert(mouseX)
|
const d = xScale.invert(x)
|
||||||
const legendItems = sortBy(
|
const legendItems = sortBy(
|
||||||
data.y.map((p, i) => ({
|
data.y.map((p, i) => ({
|
||||||
color: CATEGORY_COLORS[i],
|
color: CATEGORY_COLORS[i],
|
||||||
|
|
|
@ -26,11 +26,11 @@ const getNumericChartData = (contract: NumericContract) => {
|
||||||
const NumericChartTooltip = (
|
const NumericChartTooltip = (
|
||||||
props: TooltipProps<number, DistributionPoint>
|
props: TooltipProps<number, DistributionPoint>
|
||||||
) => {
|
) => {
|
||||||
const { data, mouseX, xScale } = props
|
const { data, x, xScale } = props
|
||||||
const x = xScale.invert(mouseX)
|
const amount = xScale.invert(x)
|
||||||
return (
|
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>
|
<span className="text-greyscale-6">{formatPct(data.y, 2)}</span>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -45,9 +45,9 @@ const getBetPoints = (bets: Bet[], scaleP: (p: number) => number) => {
|
||||||
const PseudoNumericChartTooltip = (
|
const PseudoNumericChartTooltip = (
|
||||||
props: TooltipProps<Date, HistoryPoint<Bet>>
|
props: TooltipProps<Date, HistoryPoint<Bet>>
|
||||||
) => {
|
) => {
|
||||||
const { data, mouseX, xScale } = props
|
const { data, x, xScale } = props
|
||||||
const [start, end] = xScale.domain()
|
const [start, end] = xScale.domain()
|
||||||
const d = xScale.invert(mouseX)
|
const d = xScale.invert(x)
|
||||||
return (
|
return (
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
{data.obj && <Avatar size="xs" avatarUrl={data.obj.userAvatarUrl} />}
|
{data.obj && <Avatar size="xs" avatarUrl={data.obj.userAvatarUrl} />}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import {
|
||||||
CurveFactory,
|
CurveFactory,
|
||||||
SeriesPoint,
|
SeriesPoint,
|
||||||
curveLinear,
|
curveLinear,
|
||||||
|
curveStepBefore,
|
||||||
|
curveStepAfter,
|
||||||
stack,
|
stack,
|
||||||
stackOrderReverse,
|
stackOrderReverse,
|
||||||
} from 'd3-shape'
|
} from 'd3-shape'
|
||||||
|
@ -19,6 +21,8 @@ import {
|
||||||
AreaPath,
|
AreaPath,
|
||||||
AreaWithTopStroke,
|
AreaWithTopStroke,
|
||||||
Point,
|
Point,
|
||||||
|
SliceMarker,
|
||||||
|
TooltipParams,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
computeColorStops,
|
computeColorStops,
|
||||||
formatPct,
|
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 DistributionPoint<T = unknown> = Point<number, number, T>
|
||||||
export type ValueKind = 'm$' | 'percent' | 'amount'
|
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 getTickValues = (min: number, max: number, n: number) => {
|
||||||
const step = (max - min) / (n - 1)
|
const step = (max - min) / (n - 1)
|
||||||
return [min, ...range(1, n - 1).map((i) => min + step * i), max]
|
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[],
|
data: P[],
|
||||||
xScale: ContinuousScale<X>
|
xScale: ContinuousScale<X>
|
||||||
) => {
|
) => {
|
||||||
const bisect = bisector((p: P) => p.x)
|
const bisect = bisector((p: P) => p.x)
|
||||||
return (posX: number) => {
|
return (posX: number) => {
|
||||||
const x = xScale.invert(posX)
|
const x = xScale.invert(posX)
|
||||||
const item = data[bisect.left(data, x) - 1]
|
const i = bisect.left(data, x)
|
||||||
const result = item ? { ...item, x: posX } : undefined
|
const prev = data[i - 1] as P | undefined
|
||||||
return result
|
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 { data, w, h, color, margin, yScale, curve, Tooltip } = props
|
||||||
|
|
||||||
|
const [ttParams, setTTParams] = useState<TooltipParams<P>>()
|
||||||
const [viewXScale, setViewXScale] =
|
const [viewXScale, setViewXScale] =
|
||||||
useState<ScaleContinuousNumeric<number, number>>()
|
useState<ScaleContinuousNumeric<number, number>>()
|
||||||
const xScale = viewXScale ?? props.xScale
|
const xScale = viewXScale ?? props.xScale
|
||||||
|
@ -78,13 +104,19 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
||||||
return { xAxis, yAxis }
|
return { xAxis, yAxis }
|
||||||
}, [w, xScale, yScale])
|
}, [w, xScale, yScale])
|
||||||
|
|
||||||
const selector = betAtPointSelector(data, xScale)
|
const selector = dataAtPointSelector(data, xScale)
|
||||||
const onMouseOver = useEvent((mouseX: number) => {
|
const onMouseOver = useEvent((mouseX: number, mouseY: number) => {
|
||||||
const p = selector(mouseX)
|
const p = selector(mouseX)
|
||||||
props.onMouseOver?.(p)
|
props.onMouseOver?.(p.prev)
|
||||||
return p
|
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>) => {
|
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||||
if (ev.selection) {
|
if (ev.selection) {
|
||||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||||
|
@ -103,8 +135,10 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
||||||
margin={margin}
|
margin={margin}
|
||||||
xAxis={xAxis}
|
xAxis={xAxis}
|
||||||
yAxis={yAxis}
|
yAxis={yAxis}
|
||||||
|
ttParams={ttParams}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onMouseOver={onMouseOver}
|
onMouseOver={onMouseOver}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
Tooltip={Tooltip}
|
Tooltip={Tooltip}
|
||||||
>
|
>
|
||||||
<AreaWithTopStroke
|
<AreaWithTopStroke
|
||||||
|
@ -134,6 +168,7 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
||||||
}) => {
|
}) => {
|
||||||
const { data, w, h, colors, margin, yScale, yKind, curve, Tooltip } = 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 [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
|
||||||
const xScale = viewXScale ?? props.xScale
|
const xScale = viewXScale ?? props.xScale
|
||||||
|
|
||||||
|
@ -168,16 +203,23 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
||||||
return d3Stack(data)
|
return d3Stack(data)
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
const selector = betAtPointSelector(data, xScale)
|
const selector = dataAtPointSelector(data, xScale)
|
||||||
const onMouseOver = useEvent((mouseX: number) => {
|
const onMouseOver = useEvent((mouseX: number, mouseY: number) => {
|
||||||
const p = selector(mouseX)
|
const p = selector(mouseX)
|
||||||
props.onMouseOver?.(p)
|
props.onMouseOver?.(p.prev)
|
||||||
return p
|
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>) => {
|
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||||
if (ev.selection) {
|
if (ev.selection) {
|
||||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||||
|
|
||||||
setViewXScale(() =>
|
setViewXScale(() =>
|
||||||
xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
||||||
)
|
)
|
||||||
|
@ -193,8 +235,10 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
||||||
margin={margin}
|
margin={margin}
|
||||||
xAxis={xAxis}
|
xAxis={xAxis}
|
||||||
yAxis={yAxis}
|
yAxis={yAxis}
|
||||||
|
ttParams={ttParams}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onMouseOver={onMouseOver}
|
onMouseOver={onMouseOver}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
Tooltip={Tooltip}
|
Tooltip={Tooltip}
|
||||||
>
|
>
|
||||||
{series.map((s, i) => (
|
{series.map((s, i) => (
|
||||||
|
@ -226,13 +270,15 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
||||||
Tooltip?: TooltipComponent<Date, P>
|
Tooltip?: TooltipComponent<Date, P>
|
||||||
pct?: boolean
|
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 [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
|
||||||
const xScale = viewXScale ?? props.xScale
|
const xScale = viewXScale ?? props.xScale
|
||||||
|
|
||||||
const px = useCallback((p: P) => xScale(p.x), [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 py1 = useCallback((p: P) => yScale(p.y), [yScale])
|
||||||
|
|
||||||
const { xAxis, yAxis } = useMemo(() => {
|
const { xAxis, yAxis } = useMemo(() => {
|
||||||
|
@ -253,21 +299,53 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
||||||
return { xAxis, yAxis }
|
return { xAxis, yAxis }
|
||||||
}, [w, h, yKind, xScale, yScale])
|
}, [w, h, yKind, xScale, yScale])
|
||||||
|
|
||||||
const selector = betAtPointSelector(data, xScale)
|
const selector = dataAtPointSelector(data, xScale)
|
||||||
const onMouseOver = useEvent((mouseX: number) => {
|
const onMouseOver = useEvent((mouseX: number) => {
|
||||||
const p = selector(mouseX)
|
const p = selector(mouseX)
|
||||||
props.onMouseOver?.(p)
|
props.onMouseOver?.(p.prev)
|
||||||
return p
|
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>) => {
|
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||||
if (ev.selection) {
|
if (ev.selection) {
|
||||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||||
setViewXScale(() =>
|
const newViewXScale = xScale
|
||||||
xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
.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 {
|
} else {
|
||||||
setViewXScale(undefined)
|
setViewXScale(undefined)
|
||||||
|
yScale.domain([0, 1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -285,8 +363,12 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
||||||
margin={margin}
|
margin={margin}
|
||||||
xAxis={xAxis}
|
xAxis={xAxis}
|
||||||
yAxis={yAxis}
|
yAxis={yAxis}
|
||||||
|
ttParams={
|
||||||
|
mouse ? { x: mouse.x, y: mouse.y, data: mouse.data } : undefined
|
||||||
|
}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onMouseOver={onMouseOver}
|
onMouseOver={onMouseOver}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
Tooltip={Tooltip}
|
Tooltip={Tooltip}
|
||||||
>
|
>
|
||||||
{stops && (
|
{stops && (
|
||||||
|
@ -306,6 +388,9 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
||||||
py1={py1}
|
py1={py1}
|
||||||
curve={curve ?? curveLinear}
|
curve={curve ?? curveLinear}
|
||||||
/>
|
/>
|
||||||
|
{mouse && (
|
||||||
|
<SliceMarker color="#5BCEFF" x={mouse.x} y0={mouse.y0} y1={mouse.y1} />
|
||||||
|
)}
|
||||||
</SVGChart>
|
</SVGChart>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,4 @@
|
||||||
import {
|
import { ReactNode, SVGProps, memo, useRef, useEffect, useMemo } from 'react'
|
||||||
ReactNode,
|
|
||||||
SVGProps,
|
|
||||||
memo,
|
|
||||||
useRef,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { pointer, select } from 'd3-selection'
|
import { pointer, select } from 'd3-selection'
|
||||||
import { Axis, AxisScale } from 'd3-axis'
|
import { Axis, AxisScale } from 'd3-axis'
|
||||||
import { brushX, D3BrushEvent } from 'd3-brush'
|
import { brushX, D3BrushEvent } from 'd3-brush'
|
||||||
|
@ -17,6 +9,7 @@ import clsx from 'clsx'
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { useMeasureSize } from 'web/hooks/use-measure-size'
|
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 }
|
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: {
|
export const SVGChart = <X, TT>(props: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
w: number
|
w: number
|
||||||
|
@ -130,8 +145,10 @@ export const SVGChart = <X, TT>(props: {
|
||||||
margin: Margin
|
margin: Margin
|
||||||
xAxis: Axis<X>
|
xAxis: Axis<X>
|
||||||
yAxis: Axis<number>
|
yAxis: Axis<number>
|
||||||
|
ttParams: TooltipParams<TT> | undefined
|
||||||
onSelect?: (ev: D3BrushEvent<any>) => void
|
onSelect?: (ev: D3BrushEvent<any>) => void
|
||||||
onMouseOver?: (mouseX: number, mouseY: number) => TT | undefined
|
onMouseOver?: (mouseX: number, mouseY: number) => void
|
||||||
|
onMouseLeave?: () => void
|
||||||
Tooltip?: TooltipComponent<X, TT>
|
Tooltip?: TooltipComponent<X, TT>
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
|
@ -141,16 +158,18 @@ export const SVGChart = <X, TT>(props: {
|
||||||
margin,
|
margin,
|
||||||
xAxis,
|
xAxis,
|
||||||
yAxis,
|
yAxis,
|
||||||
onMouseOver,
|
ttParams,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onMouseOver,
|
||||||
|
onMouseLeave,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} = props
|
} = props
|
||||||
const [mouse, setMouse] = useState<{ x: number; y: number; data: TT }>()
|
|
||||||
const tooltipMeasure = useMeasureSize()
|
const tooltipMeasure = useMeasureSize()
|
||||||
const overlayRef = useRef<SVGGElement>(null)
|
const overlayRef = useRef<SVGGElement>(null)
|
||||||
const innerW = w - (margin.left + margin.right)
|
const innerW = w - (margin.left + margin.right)
|
||||||
const innerH = h - (margin.top + margin.bottom)
|
const innerH = h - (margin.top + margin.bottom)
|
||||||
const clipPathId = useMemo(() => nanoid(), [])
|
const clipPathId = useMemo(() => nanoid(), [])
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
const justSelected = useRef(false)
|
const justSelected = useRef(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -165,7 +184,7 @@ export const SVGChart = <X, TT>(props: {
|
||||||
if (!justSelected.current) {
|
if (!justSelected.current) {
|
||||||
justSelected.current = true
|
justSelected.current = true
|
||||||
onSelect(ev)
|
onSelect(ev)
|
||||||
setMouse(undefined)
|
onMouseLeave?.()
|
||||||
if (overlayRef.current) {
|
if (overlayRef.current) {
|
||||||
select(overlayRef.current).call(brush.clear)
|
select(overlayRef.current).call(brush.clear)
|
||||||
}
|
}
|
||||||
|
@ -181,44 +200,49 @@ export const SVGChart = <X, TT>(props: {
|
||||||
.select('.selection')
|
.select('.selection')
|
||||||
.attr('shape-rendering', 'null')
|
.attr('shape-rendering', 'null')
|
||||||
}
|
}
|
||||||
}, [innerW, innerH, onSelect])
|
}, [innerW, innerH, onSelect, onMouseLeave])
|
||||||
|
|
||||||
const onPointerMove = (ev: React.PointerEvent) => {
|
const onPointerMove = (ev: React.PointerEvent) => {
|
||||||
if (ev.pointerType === 'mouse' && onMouseOver) {
|
if (ev.pointerType === 'mouse' && onMouseOver) {
|
||||||
const [x, y] = pointer(ev)
|
const [x, y] = pointer(ev)
|
||||||
const data = onMouseOver(x, y)
|
onMouseOver(x, y)
|
||||||
if (data !== undefined) {
|
|
||||||
setMouse({ x, y, data })
|
|
||||||
} else {
|
|
||||||
setMouse(undefined)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = () => {
|
const onPointerLeave = () => {
|
||||||
setMouse(undefined)
|
onMouseLeave?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden">
|
<div className="relative overflow-hidden">
|
||||||
{mouse && Tooltip && (
|
{ttParams && Tooltip && (
|
||||||
<TooltipContainer
|
<TooltipContainer
|
||||||
setElem={tooltipMeasure.setElem}
|
setElem={tooltipMeasure.setElem}
|
||||||
margin={margin}
|
margin={margin}
|
||||||
pos={getTooltipPosition(
|
pos={getTooltipPosition(
|
||||||
mouse.x,
|
ttParams.x,
|
||||||
mouse.y,
|
ttParams.y,
|
||||||
innerW,
|
innerW,
|
||||||
innerH,
|
innerH,
|
||||||
tooltipMeasure.width,
|
tooltipMeasure.width ?? 140,
|
||||||
tooltipMeasure.height
|
tooltipMeasure.height ?? 35,
|
||||||
|
isMobile ?? false
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
xScale={xAxis.scale()}
|
xScale={xAxis.scale()}
|
||||||
mouseX={mouse.x}
|
x={ttParams.x}
|
||||||
mouseY={mouse.y}
|
y={ttParams.y}
|
||||||
data={mouse.data}
|
data={ttParams.data}
|
||||||
/>
|
/>
|
||||||
</TooltipContainer>
|
</TooltipContainer>
|
||||||
)}
|
)}
|
||||||
|
@ -230,6 +254,7 @@ export const SVGChart = <X, TT>(props: {
|
||||||
<XAxis axis={xAxis} w={innerW} h={innerH} />
|
<XAxis axis={xAxis} w={innerW} h={innerH} />
|
||||||
<YAxis axis={yAxis} w={innerW} h={innerH} />
|
<YAxis axis={yAxis} w={innerW} h={innerH} />
|
||||||
<g clipPath={`url(#${clipPathId})`}>{children}</g>
|
<g clipPath={`url(#${clipPathId})`}>{children}</g>
|
||||||
|
{!isMobile ? (
|
||||||
<g
|
<g
|
||||||
ref={overlayRef}
|
ref={overlayRef}
|
||||||
x="0"
|
x="0"
|
||||||
|
@ -242,6 +267,17 @@ export const SVGChart = <X, TT>(props: {
|
||||||
onPointerMove={onPointerMove}
|
onPointerMove={onPointerMove}
|
||||||
onPointerLeave={onPointerLeave}
|
onPointerLeave={onPointerLeave}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width={innerW}
|
||||||
|
height={innerH}
|
||||||
|
fill="transparent"
|
||||||
|
onTouchMove={onTouchMove}
|
||||||
|
onTouchEnd={onPointerLeave}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
@ -255,31 +291,34 @@ export const getTooltipPosition = (
|
||||||
mouseY: number,
|
mouseY: number,
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
containerHeight: number,
|
containerHeight: number,
|
||||||
tooltipWidth?: number,
|
tooltipWidth: number,
|
||||||
tooltipHeight?: number
|
tooltipHeight: number,
|
||||||
|
isMobile: boolean
|
||||||
) => {
|
) => {
|
||||||
let left = mouseX + 12
|
let left = mouseX + 12
|
||||||
let bottom = containerHeight - mouseY + 12
|
let bottom = !isMobile
|
||||||
|
? containerHeight - mouseY + 12
|
||||||
|
: containerHeight - tooltipHeight + 12
|
||||||
if (tooltipWidth != null) {
|
if (tooltipWidth != null) {
|
||||||
const overflow = left + tooltipWidth - containerWidth
|
const overflow = left + tooltipWidth - containerWidth
|
||||||
if (overflow > 0) {
|
if (overflow > 0) {
|
||||||
left -= overflow
|
left -= overflow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tooltipHeight != null) {
|
if (tooltipHeight != null) {
|
||||||
const overflow = tooltipHeight - mouseY
|
const overflow = tooltipHeight - mouseY
|
||||||
if (overflow > 0) {
|
if (overflow > 0) {
|
||||||
bottom -= overflow
|
bottom -= overflow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { left, bottom }
|
return { left, bottom }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TooltipProps<X, T> = {
|
export type TooltipParams<T> = { x: number; y: number; data: T }
|
||||||
mouseX: number
|
export type TooltipProps<X, T> = TooltipParams<T> & {
|
||||||
mouseY: number
|
|
||||||
xScale: ContinuousScale<X>
|
xScale: ContinuousScale<X>
|
||||||
data: T
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TooltipComponent<X, T> = React.ComponentType<TooltipProps<X, 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 DailyCountTooltip = (props: TooltipProps<Date, HistoryPoint>) => {
|
||||||
const { data, mouseX, xScale } = props
|
const { data, x, xScale } = props
|
||||||
const d = xScale.invert(mouseX)
|
const d = xScale.invert(x)
|
||||||
return (
|
return (
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
<span className="font-semibold">{dayjs(d).format('MMM DD')}</span>
|
<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 DailyPercentTooltip = (props: TooltipProps<Date, HistoryPoint>) => {
|
||||||
const { data, mouseX, xScale } = props
|
const { data, x, xScale } = props
|
||||||
const d = xScale.invert(mouseX)
|
const d = xScale.invert(x)
|
||||||
return (
|
return (
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
<span className="font-semibold">{dayjs(d).format('MMM DD')}</span>
|
<span className="font-semibold">{dayjs(d).format('MMM DD')}</span>
|
||||||
|
|
|
@ -138,16 +138,6 @@ export function CommentInputTextArea(props: {
|
||||||
<LoadingIndicator spinnerClassName="border-gray-500" />
|
<LoadingIndicator spinnerClassName="border-gray-500" />
|
||||||
)}
|
)}
|
||||||
</TextEditor>
|
</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?: {
|
cancelBtn?: {
|
||||||
label?: string
|
label?: string
|
||||||
className?: string
|
color?: ColorType
|
||||||
}
|
}
|
||||||
submitBtn?: {
|
submitBtn?: {
|
||||||
label?: string
|
label?: string
|
||||||
className?: string
|
color?: ColorType
|
||||||
isSubmitting?: boolean
|
isSubmitting?: boolean
|
||||||
}
|
}
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
@ -53,14 +53,14 @@ export function ConfirmationButton(props: {
|
||||||
<Col className="gap-4 rounded-md bg-white px-8 py-6">
|
<Col className="gap-4 rounded-md bg-white px-8 py-6">
|
||||||
{children}
|
{children}
|
||||||
<Row className="gap-4">
|
<Row className="gap-4">
|
||||||
<div
|
<Button
|
||||||
className={clsx('btn', cancelBtn?.className)}
|
color={cancelBtn?.color ?? 'gray-white'}
|
||||||
onClick={() => updateOpen(false)}
|
onClick={() => updateOpen(false)}
|
||||||
>
|
>
|
||||||
{cancelBtn?.label ?? 'Cancel'}
|
{cancelBtn?.label ?? 'Cancel'}
|
||||||
</div>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={clsx('btn', submitBtn?.className)}
|
color={submitBtn?.color ?? 'blue'}
|
||||||
onClick={
|
onClick={
|
||||||
onSubmitWithSuccess
|
onSubmitWithSuccess
|
||||||
? () =>
|
? () =>
|
||||||
|
@ -100,18 +100,11 @@ export function ResolveConfirmationButton(props: {
|
||||||
onResolve: () => void
|
onResolve: () => void
|
||||||
isSubmitting: boolean
|
isSubmitting: boolean
|
||||||
openModalButtonClass?: string
|
openModalButtonClass?: string
|
||||||
submitButtonClass?: string
|
|
||||||
color?: ColorType
|
color?: ColorType
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
const {
|
const { onResolve, isSubmitting, openModalButtonClass, color, disabled } =
|
||||||
onResolve,
|
props
|
||||||
isSubmitting,
|
|
||||||
openModalButtonClass,
|
|
||||||
submitButtonClass,
|
|
||||||
color,
|
|
||||||
disabled,
|
|
||||||
} = props
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationButton
|
<ConfirmationButton
|
||||||
openModalBtn={{
|
openModalBtn={{
|
||||||
|
@ -126,7 +119,7 @@ export function ResolveConfirmationButton(props: {
|
||||||
}}
|
}}
|
||||||
submitBtn={{
|
submitBtn={{
|
||||||
label: 'Resolve',
|
label: 'Resolve',
|
||||||
className: clsx('border-none', submitButtonClass),
|
color: color,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
}}
|
}}
|
||||||
onSubmit={onResolve}
|
onSubmit={onResolve}
|
||||||
|
|
|
@ -446,7 +446,7 @@ function ContractSearchControls(props: {
|
||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
/>
|
/>
|
||||||
{!isMobile && (
|
{!isMobile && !query && (
|
||||||
<SearchFilters
|
<SearchFilters
|
||||||
filter={filter}
|
filter={filter}
|
||||||
selectFilter={selectFilter}
|
selectFilter={selectFilter}
|
||||||
|
@ -457,7 +457,7 @@ function ContractSearchControls(props: {
|
||||||
includeProbSorts={includeProbSorts}
|
includeProbSorts={includeProbSorts}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isMobile && (
|
{isMobile && !query && (
|
||||||
<>
|
<>
|
||||||
<MobileSearchBar
|
<MobileSearchBar
|
||||||
children={
|
children={
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
BinaryContract,
|
BinaryContract,
|
||||||
Contract,
|
Contract,
|
||||||
CPMMBinaryContract,
|
CPMMBinaryContract,
|
||||||
|
CPMMContract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
MultipleChoiceContract,
|
MultipleChoiceContract,
|
||||||
NumericContract,
|
NumericContract,
|
||||||
|
@ -35,6 +36,7 @@ import { getMappedValue } from 'common/pseudo-numeric'
|
||||||
import { Tooltip } from '../tooltip'
|
import { Tooltip } from '../tooltip'
|
||||||
import { SiteLink } from '../site-link'
|
import { SiteLink } from '../site-link'
|
||||||
import { ProbChange } from './prob-change-table'
|
import { ProbChange } from './prob-change-table'
|
||||||
|
import { Card } from '../card'
|
||||||
|
|
||||||
export function ContractCard(props: {
|
export function ContractCard(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -75,12 +77,7 @@ export function ContractCard(props: {
|
||||||
!hideQuickBet
|
!hideQuickBet
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Card className={clsx('group relative flex gap-3', className)}>
|
||||||
className={clsx(
|
|
||||||
'group relative gap-3 rounded-lg bg-white shadow-md hover:cursor-pointer hover:bg-gray-100',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Col className="relative flex-1 gap-3 py-4 pb-12 pl-6">
|
<Col className="relative flex-1 gap-3 py-4 pb-12 pl-6">
|
||||||
<AvatarDetails
|
<AvatarDetails
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -195,7 +192,7 @@ export function ContractCard(props: {
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,7 +388,7 @@ export function PseudoNumericResolutionOrExpectation(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContractCardProbChange(props: {
|
export function ContractCardProbChange(props: {
|
||||||
contract: CPMMBinaryContract
|
contract: CPMMContract
|
||||||
noLinkAvatar?: boolean
|
noLinkAvatar?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
@ -399,12 +396,7 @@ export function ContractCardProbChange(props: {
|
||||||
const contract = useContractWithPreload(props.contract) as CPMMBinaryContract
|
const contract = useContractWithPreload(props.contract) as CPMMBinaryContract
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Card className={clsx(className, 'mb-4')}>
|
||||||
className={clsx(
|
|
||||||
className,
|
|
||||||
'mb-4 rounded-lg bg-white shadow hover:bg-gray-100 hover:shadow-lg'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<AvatarDetails
|
<AvatarDetails
|
||||||
contract={contract}
|
contract={contract}
|
||||||
className={'px-6 pt-4'}
|
className={'px-6 pt-4'}
|
||||||
|
@ -419,6 +411,6 @@ export function ContractCardProbChange(props: {
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
<ProbChange className="py-2 pr-4" contract={contract} />
|
<ProbChange className="py-2 pr-4" contract={contract} />
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -357,7 +357,7 @@ export function GroupDisplay(props: {
|
||||||
const groupSection = (
|
const groupSection = (
|
||||||
<a
|
<a
|
||||||
className={clsx(
|
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'
|
!disabled && 'hover:bg-greyscale-3 cursor-pointer'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -437,14 +437,14 @@ function EditableCloseDate(props: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
size="sm"
|
size="md"
|
||||||
open={isEditingCloseTime}
|
open={isEditingCloseTime}
|
||||||
setOpen={setIsEditingCloseTime}
|
setOpen={setIsEditingCloseTime}
|
||||||
position="top"
|
position="top"
|
||||||
>
|
>
|
||||||
<Col className="rounded bg-white px-8 pb-8">
|
<Col className="rounded bg-white px-8 pb-8">
|
||||||
<Subtitle text="Edit Close Date" />
|
<Subtitle text="Edit market close time" />
|
||||||
<Row className="z-10 mr-2 w-full shrink-0 flex-wrap items-center gap-2">
|
<Row className="z-10 mr-2 mt-4 w-full shrink-0 flex-wrap items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
className="input input-bordered w-full shrink-0 sm:w-fit"
|
className="input input-bordered w-full shrink-0 sm:w-fit"
|
||||||
|
@ -461,22 +461,18 @@ function EditableCloseDate(props: {
|
||||||
min="00:00"
|
min="00:00"
|
||||||
value={closeHoursMinutes}
|
value={closeHoursMinutes}
|
||||||
/>
|
/>
|
||||||
</Row>
|
<Button size={'xs'} color={'indigo'} onClick={() => onSave()}>
|
||||||
<Button
|
Set
|
||||||
className="mt-4"
|
|
||||||
size={'xs'}
|
|
||||||
color={'indigo'}
|
|
||||||
onClick={() => onSave()}
|
|
||||||
>
|
|
||||||
Done
|
|
||||||
</Button>
|
</Button>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="mt-4"
|
className="mt-8"
|
||||||
size={'xs'}
|
size={'xs'}
|
||||||
color={'gray-white'}
|
color="red"
|
||||||
onClick={() => onSave(Date.now())}
|
onClick={() => onSave(Date.now())}
|
||||||
>
|
>
|
||||||
Close Now
|
Close market now
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { SiteLink } from '../site-link'
|
||||||
import { firestoreConsolePath } from 'common/envs/constants'
|
import { firestoreConsolePath } from 'common/envs/constants'
|
||||||
import { deleteField } from 'firebase/firestore'
|
import { deleteField } from 'firebase/firestore'
|
||||||
import ShortToggle from '../widgets/short-toggle'
|
import ShortToggle from '../widgets/short-toggle'
|
||||||
import { DuplicateContractButton } from '../copy-contract-button'
|
import { DuplicateContractButton } from '../duplicate-contract-button'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { BETTORS, User } from 'common/user'
|
import { BETTORS, User } from 'common/user'
|
||||||
import { Button } from '../button'
|
import { 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={{
|
openModalBtn={{
|
||||||
label: '',
|
label: '',
|
||||||
icon: <FlagIcon className="h-5 w-5" />,
|
icon: <FlagIcon className="h-5 w-5" />,
|
||||||
className: clsx(flagClass, reporting && 'btn-disabled loading'),
|
disabled: reporting,
|
||||||
}}
|
className: clsx(flagClass),
|
||||||
cancelBtn={{
|
|
||||||
label: 'Cancel',
|
|
||||||
className: 'border-none btn-sm btn-ghost self-center',
|
|
||||||
}}
|
|
||||||
submitBtn={{
|
|
||||||
label: 'Submit',
|
|
||||||
className: 'btn-secondary',
|
|
||||||
}}
|
}}
|
||||||
onSubmitWithSuccess={onSubmit}
|
onSubmitWithSuccess={onSubmit}
|
||||||
disabled={userReported}
|
disabled={userReported}
|
||||||
|
|
|
@ -45,7 +45,7 @@ export function ContractsGrid(props: {
|
||||||
cardUIOptions || {}
|
cardUIOptions || {}
|
||||||
const { itemIds: contractIds, highlightClassName } = highlightOptions || {}
|
const { itemIds: contractIds, highlightClassName } = highlightOptions || {}
|
||||||
const onVisibilityUpdated = useCallback(
|
const onVisibilityUpdated = useCallback(
|
||||||
(visible) => {
|
(visible: boolean) => {
|
||||||
if (visible && loadMore) {
|
if (visible && loadMore) {
|
||||||
loadMore()
|
loadMore()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import { sortBy } from 'lodash'
|
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { contractPath } from 'web/lib/firebase/contracts'
|
|
||||||
import { CPMMContract } from 'common/contract'
|
import { CPMMContract } from 'common/contract'
|
||||||
import { formatPercent } from 'common/util/format'
|
import { formatPercent } from 'common/util/format'
|
||||||
import { SiteLink } from '../site-link'
|
import { sortBy } from 'lodash'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { Row } from '../layout/row'
|
|
||||||
import { LoadingIndicator } from '../loading-indicator'
|
import { LoadingIndicator } from '../loading-indicator'
|
||||||
import { useContractWithPreload } from 'web/hooks/use-contract'
|
import { ContractCardProbChange } from './contract-card'
|
||||||
|
|
||||||
export function ProbChangeTable(props: {
|
export function ProbChangeTable(props: {
|
||||||
changes: CPMMContract[] | undefined
|
changes: CPMMContract[] | undefined
|
||||||
|
@ -39,46 +36,21 @@ export function ProbChangeTable(props: {
|
||||||
if (rows === 0) return <div className="px-4 text-gray-500">None</div>
|
if (rows === 0) return <div className="px-4 text-gray-500">None</div>
|
||||||
|
|
||||||
return (
|
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="mb-4 w-full gap-4 rounded-lg md:flex-row">
|
||||||
<Col className="flex-1 divide-y">
|
<Col className="flex-1 gap-4">
|
||||||
{filteredPositiveChanges.map((contract) => (
|
{filteredPositiveChanges.map((contract) => (
|
||||||
<ProbChangeRow key={contract.id} contract={contract} />
|
<ContractCardProbChange key={contract.id} contract={contract} />
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="flex-1 divide-y">
|
<Col className="flex-1 gap-4">
|
||||||
{filteredNegativeChanges.map((contract) => (
|
{filteredNegativeChanges.map((contract) => (
|
||||||
<ProbChangeRow key={contract.id} contract={contract} />
|
<ContractCardProbChange key={contract.id} contract={contract} />
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
</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: {
|
export function ProbChange(props: {
|
||||||
contract: CPMMContract
|
contract: CPMMContract
|
||||||
className?: string
|
className?: string
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import { LinkIcon } from '@heroicons/react/outline'
|
|
||||||
import toast from 'react-hot-toast'
|
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { contractPath } from 'web/lib/firebase/contracts'
|
import { contractPath } from 'web/lib/firebase/contracts'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
|
@ -9,9 +6,7 @@ import { Row } from '../layout/row'
|
||||||
import { ShareEmbedButton } from '../share-embed-button'
|
import { ShareEmbedButton } from '../share-embed-button'
|
||||||
import { Title } from '../title'
|
import { Title } from '../title'
|
||||||
import { TweetButton } from '../tweet-button'
|
import { TweetButton } from '../tweet-button'
|
||||||
import { Button } from '../button'
|
import { withTracking } from 'web/lib/service/analytics'
|
||||||
import { copyToClipboard } from 'web/lib/util/copy'
|
|
||||||
import { track, withTracking } from 'web/lib/service/analytics'
|
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { SiteLink } from '../site-link'
|
import { SiteLink } from '../site-link'
|
||||||
|
@ -22,6 +17,8 @@ import { useState } from 'react'
|
||||||
import { CHALLENGES_ENABLED } from 'common/challenge'
|
import { CHALLENGES_ENABLED } from 'common/challenge'
|
||||||
import ChallengeIcon from 'web/lib/icons/challenge-icon'
|
import ChallengeIcon from 'web/lib/icons/challenge-icon'
|
||||||
import { QRCode } from '../qr-code'
|
import { QRCode } from '../qr-code'
|
||||||
|
import { CopyLinkButton } from '../copy-link-button'
|
||||||
|
import { Button } from '../button'
|
||||||
|
|
||||||
export function ShareModal(props: {
|
export function ShareModal(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -34,7 +31,6 @@ export function ShareModal(props: {
|
||||||
|
|
||||||
const [openCreateChallengeModal, setOpenCreateChallengeModal] =
|
const [openCreateChallengeModal, setOpenCreateChallengeModal] =
|
||||||
useState(false)
|
useState(false)
|
||||||
const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" />
|
|
||||||
const showChallenge =
|
const showChallenge =
|
||||||
user && outcomeType === 'BINARY' && !resolution && CHALLENGES_ENABLED
|
user && outcomeType === 'BINARY' && !resolution && CHALLENGES_ENABLED
|
||||||
|
|
||||||
|
@ -61,40 +57,24 @@ export function ShareModal(props: {
|
||||||
width={150}
|
width={150}
|
||||||
height={150}
|
height={150}
|
||||||
/>
|
/>
|
||||||
<Button
|
<CopyLinkButton url={shareUrl} tracking="copy share link" />
|
||||||
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>
|
|
||||||
|
|
||||||
<Row className="z-0 flex-wrap justify-center gap-4 self-center">
|
<Row className="z-0 flex-wrap justify-center gap-4 self-center">
|
||||||
<TweetButton
|
<TweetButton tweetText={getTweetText(contract, shareUrl)} />
|
||||||
className="self-start"
|
|
||||||
tweetText={getTweetText(contract, shareUrl)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ShareEmbedButton contract={contract} />
|
<ShareEmbedButton contract={contract} />
|
||||||
|
|
||||||
{showChallenge && (
|
{showChallenge && (
|
||||||
<button
|
<Button
|
||||||
className={
|
size="2xs"
|
||||||
'btn btn-xs flex-nowrap border-2 !border-indigo-500 !bg-white normal-case text-indigo-500'
|
color="override"
|
||||||
}
|
className="gap-1 border-2 border-indigo-500 text-indigo-500 hover:bg-indigo-500 hover:text-white"
|
||||||
onClick={withTracking(
|
onClick={withTracking(
|
||||||
() => setOpenCreateChallengeModal(true),
|
() => setOpenCreateChallengeModal(true),
|
||||||
'click challenge button'
|
'click challenge button'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
️<ChallengeIcon className="mr-1 h-4 w-4" /> Challenge
|
<ChallengeIcon className="h-4 w-4" /> Challenge
|
||||||
<CreateChallengeModal
|
<CreateChallengeModal
|
||||||
isOpen={openCreateChallengeModal}
|
isOpen={openCreateChallengeModal}
|
||||||
setOpen={(open) => {
|
setOpen={(open) => {
|
||||||
|
@ -106,7 +86,7 @@ export function ShareModal(props: {
|
||||||
user={user}
|
user={user}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -21,10 +21,11 @@ export function CopyLinkButton(props: {
|
||||||
return (
|
return (
|
||||||
<Row className="w-full">
|
<Row className="w-full">
|
||||||
<input
|
<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
|
readOnly
|
||||||
type="text"
|
type="text"
|
||||||
value={displayUrl ?? url}
|
value={displayUrl ?? url}
|
||||||
|
onFocus={(e) => e.target.select()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Menu
|
<Menu
|
||||||
|
@ -37,7 +38,7 @@ export function CopyLinkButton(props: {
|
||||||
>
|
>
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
className={clsx(
|
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
|
buttonClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { Group } from 'common/group'
|
||||||
|
|
||||||
export function CreatePost(props: { group?: Group }) {
|
export function CreatePost(props: { group?: Group }) {
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
|
const [subtitle, setSubtitle] = useState('')
|
||||||
|
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
@ -22,12 +24,17 @@ export function CreatePost(props: { group?: Group }) {
|
||||||
disabled: isSubmitting,
|
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) {
|
async function savePost(title: string) {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
const newPost = {
|
const newPost = {
|
||||||
title: title,
|
title: title,
|
||||||
|
subtitle: subtitle,
|
||||||
content: editor.getJSON(),
|
content: editor.getJSON(),
|
||||||
groupId: group?.id,
|
groupId: group?.id,
|
||||||
}
|
}
|
||||||
|
@ -62,6 +69,20 @@ export function CreatePost(props: { group?: Group }) {
|
||||||
onChange={(e) => setTitle(e.target.value || '')}
|
onChange={(e) => setTitle(e.target.value || '')}
|
||||||
/>
|
/>
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
<label className="label">
|
||||||
|
<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">
|
<label className="label">
|
||||||
<span className="mb-1">
|
<span className="mb-1">
|
||||||
Content<span className={'text-red-700'}> *</span>
|
Content<span className={'text-red-700'}> *</span>
|
||||||
|
|
|
@ -3,27 +3,22 @@ import clsx from 'clsx'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { getMappedValue } from 'common/pseudo-numeric'
|
import { getMappedValue } from 'common/pseudo-numeric'
|
||||||
import { trackCallback } from 'web/lib/service/analytics'
|
import { trackCallback } from 'web/lib/service/analytics'
|
||||||
|
import { buttonClass } from './button'
|
||||||
|
|
||||||
export function DuplicateContractButton(props: {
|
export function DuplicateContractButton(props: { contract: Contract }) {
|
||||||
contract: Contract
|
const { contract } = props
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const { contract, className } = props
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className={clsx('btn btn-xs flex-nowrap normal-case', className)}
|
className={clsx(
|
||||||
style={{
|
buttonClass('2xs', 'override'),
|
||||||
backgroundColor: 'white',
|
'gap-1 border-2 border-violet-400 text-violet-400 hover:bg-violet-400 hover:text-white'
|
||||||
border: '2px solid #a78bfa',
|
)}
|
||||||
// violet-400
|
|
||||||
color: '#a78bfa',
|
|
||||||
}}
|
|
||||||
href={duplicateContractHref(contract)}
|
href={duplicateContractHref(contract)}
|
||||||
onClick={trackCallback('duplicate market')}
|
onClick={trackCallback('duplicate market')}
|
||||||
target="_blank"
|
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>
|
<div>Duplicate</div>
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
@ -40,6 +35,7 @@ function duplicateContractHref(contract: Contract) {
|
||||||
closeTime,
|
closeTime,
|
||||||
description: descriptionString,
|
description: descriptionString,
|
||||||
outcomeType: contract.outcomeType,
|
outcomeType: contract.outcomeType,
|
||||||
|
visibility: contract.visibility,
|
||||||
} as Record<string, any>
|
} as Record<string, any>
|
||||||
|
|
||||||
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
|
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
|
|
@ -319,12 +319,12 @@ const useUploadMutation = (editor: Editor | null) =>
|
||||||
{
|
{
|
||||||
onSuccess(urls) {
|
onSuccess(urls) {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
let trans = editor.view.state.tr
|
let trans = editor.chain().focus()
|
||||||
urls.forEach((src: any) => {
|
urls.forEach((src) => {
|
||||||
const node = editor.view.state.schema.nodes.image.create({ src })
|
trans = trans.createParagraphNear()
|
||||||
trans = trans.insert(editor.view.state.selection.to, node)
|
trans = trans.setImage({ src })
|
||||||
})
|
})
|
||||||
editor.view.dispatch(trans)
|
trans.run()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
} from '@tiptap/react'
|
} from '@tiptap/react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useContract } from 'web/hooks/use-contract'
|
import { useContract } from 'web/hooks/use-contract'
|
||||||
import { ContractCard } from '../contract/contract-card'
|
import { ContractMention } from '../contract/contract-mention'
|
||||||
|
|
||||||
const name = 'contract-mention-component'
|
const name = 'contract-mention-component'
|
||||||
|
|
||||||
|
@ -14,13 +14,8 @@ const ContractMentionComponent = (props: any) => {
|
||||||
const contract = useContract(props.node.attrs.id)
|
const contract = useContract(props.node.attrs.id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper className={clsx(name, 'not-prose')}>
|
<NodeViewWrapper className={clsx(name, 'not-prose inline')}>
|
||||||
{contract && (
|
{contract && <ContractMention contract={contract} />}
|
||||||
<ContractCard
|
|
||||||
contract={contract}
|
|
||||||
className="my-2 w-full border border-gray-100"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -37,6 +32,5 @@ export const DisplayContractMention = Mention.extend({
|
||||||
addNodeView: () =>
|
addNodeView: () =>
|
||||||
ReactNodeViewRenderer(ContractMentionComponent, {
|
ReactNodeViewRenderer(ContractMentionComponent, {
|
||||||
// On desktop, render cards below half-width so you can stack two
|
// 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 { FreeResponseContract } from 'common/contract'
|
||||||
import { ContractComment } from 'common/comment'
|
import { ContractComment } from 'common/comment'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
import { sum } from 'lodash'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { Avatar } from 'web/components/avatar'
|
import { Avatar } from 'web/components/avatar'
|
||||||
|
@ -14,6 +15,8 @@ import {
|
||||||
} from 'web/components/feed/feed-comments'
|
} from 'web/components/feed/feed-comments'
|
||||||
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { useEvent } from 'web/hooks/use-event'
|
||||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||||
import { UserLink } from 'web/components/user-link'
|
import { UserLink } from 'web/components/user-link'
|
||||||
|
|
||||||
|
@ -27,11 +30,17 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
const { username, avatarUrl, name, text } = answer
|
const { username, avatarUrl, name, text } = answer
|
||||||
|
|
||||||
const [replyTo, setReplyTo] = useState<ReplyTo>()
|
const [replyTo, setReplyTo] = useState<ReplyTo>()
|
||||||
|
const user = useUser()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const answerElementId = `answer-${answer.id}`
|
const answerElementId = `answer-${answer.id}`
|
||||||
const highlighted = router.asPath.endsWith(`#${answerElementId}`)
|
const highlighted = router.asPath.endsWith(`#${answerElementId}`)
|
||||||
const answerRef = useRef<HTMLDivElement>(null)
|
const answerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const onSubmitComment = useEvent(() => setReplyTo(undefined))
|
||||||
|
const onReplyClick = useEvent((comment: ContractComment) => {
|
||||||
|
setReplyTo({ id: comment.id, username: comment.userUsername })
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (highlighted && answerRef.current != null) {
|
if (highlighted && answerRef.current != null) {
|
||||||
answerRef.current.scrollIntoView(true)
|
answerRef.current.scrollIntoView(true)
|
||||||
|
@ -95,10 +104,10 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
indent={true}
|
indent={true}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
tips={tips[comment.id] ?? {}}
|
myTip={user ? tips[comment.id]?.[user.id] : undefined}
|
||||||
onReplyClick={() =>
|
totalTip={sum(Object.values(tips[comment.id] ?? {}))}
|
||||||
setReplyTo({ id: comment.id, username: comment.userUsername })
|
showTip={true}
|
||||||
}
|
onReplyClick={onReplyClick}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -112,7 +121,7 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
parentAnswerOutcome={answer.number.toString()}
|
parentAnswerOutcome={answer.number.toString()}
|
||||||
replyTo={replyTo}
|
replyTo={replyTo}
|
||||||
onSubmitComment={() => setReplyTo(undefined)}
|
onSubmitComment={onSubmitComment}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React, { memo, useEffect } from 'react'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
|
@ -8,7 +9,6 @@ import clsx from 'clsx'
|
||||||
import { formatMoney, formatPercent } from 'common/util/format'
|
import { formatMoney, formatPercent } from 'common/util/format'
|
||||||
import { OutcomeLabel } from 'web/components/outcome-label'
|
import { OutcomeLabel } from 'web/components/outcome-label'
|
||||||
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
||||||
import React, { useEffect } from 'react'
|
|
||||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { getChallenge, getChallengeUrl } from 'web/lib/firebase/challenges'
|
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 { UserLink } from 'web/components/user-link'
|
||||||
import { BETTOR } from 'common/user'
|
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 { contract, bet } = props
|
||||||
const { userAvatarUrl, userUsername, createdTime } = bet
|
const { userAvatarUrl, userUsername, createdTime } = bet
|
||||||
const showUser = dayjs(createdTime).isAfter('2022-06-01')
|
const showUser = dayjs(createdTime).isAfter('2022-06-01')
|
||||||
|
@ -36,7 +39,7 @@ export function FeedBet(props: { contract: Contract; bet: Bet }) {
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export function BetStatusText(props: {
|
export function BetStatusText(props: {
|
||||||
contract: Contract
|
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 { ContractComment } from 'common/comment'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Avatar } from 'web/components/avatar'
|
import { Avatar } from 'web/components/avatar'
|
||||||
import { OutcomeLabel } from 'web/components/outcome-label'
|
import { OutcomeLabel } from 'web/components/outcome-label'
|
||||||
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
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 { Col } from 'web/components/layout/col'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { Tipper } from '../tipper'
|
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 { Content } from '../editor'
|
||||||
import { Editor } from '@tiptap/react'
|
|
||||||
import { UserLink } from 'web/components/user-link'
|
import { UserLink } from 'web/components/user-link'
|
||||||
import { CommentInput } from '../comment-input'
|
import { CommentInput } from '../comment-input'
|
||||||
import { AwardBountyButton } from 'web/components/award-bounty-button'
|
import { AwardBountyButton } from 'web/components/award-bounty-button'
|
||||||
|
@ -32,6 +35,12 @@ export function FeedCommentThread(props: {
|
||||||
const { contract, threadComments, tips, parentComment } = props
|
const { contract, threadComments, tips, parentComment } = props
|
||||||
const [replyTo, setReplyTo] = useState<ReplyTo>()
|
const [replyTo, setReplyTo] = useState<ReplyTo>()
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
const onSubmitComment = useEvent(() => setReplyTo(undefined))
|
||||||
|
const onReplyClick = useEvent((comment: ContractComment) => {
|
||||||
|
setReplyTo({ id: comment.id, username: comment.userUsername })
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="relative w-full items-stretch gap-3 pb-4">
|
<Col className="relative w-full items-stretch gap-3 pb-4">
|
||||||
<span
|
<span
|
||||||
|
@ -44,10 +53,10 @@ export function FeedCommentThread(props: {
|
||||||
indent={commentIdx != 0}
|
indent={commentIdx != 0}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
tips={tips[comment.id] ?? {}}
|
myTip={user ? tips[comment.id]?.[user.id] : undefined}
|
||||||
onReplyClick={() =>
|
totalTip={sum(Object.values(tips[comment.id] ?? {}))}
|
||||||
setReplyTo({ id: comment.id, username: comment.userUsername })
|
showTip={true}
|
||||||
}
|
onReplyClick={onReplyClick}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{replyTo && (
|
{replyTo && (
|
||||||
|
@ -60,7 +69,7 @@ export function FeedCommentThread(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
parentCommentId={parentComment.id}
|
parentCommentId={parentComment.id}
|
||||||
replyTo={replyTo}
|
replyTo={replyTo}
|
||||||
onSubmitComment={() => setReplyTo(undefined)}
|
onSubmitComment={onSubmitComment}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
@ -68,14 +77,17 @@ export function FeedCommentThread(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FeedComment(props: {
|
export const FeedComment = memo(function FeedComment(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
comment: ContractComment
|
comment: ContractComment
|
||||||
tips?: CommentTips
|
showTip?: boolean
|
||||||
|
myTip?: number
|
||||||
|
totalTip?: number
|
||||||
indent?: boolean
|
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 {
|
const {
|
||||||
text,
|
text,
|
||||||
content,
|
content,
|
||||||
|
@ -180,12 +192,18 @@ export function FeedComment(props: {
|
||||||
{onReplyClick && (
|
{onReplyClick && (
|
||||||
<button
|
<button
|
||||||
className="font-bold hover:underline"
|
className="font-bold hover:underline"
|
||||||
onClick={onReplyClick}
|
onClick={() => onReplyClick(comment)}
|
||||||
>
|
>
|
||||||
Reply
|
Reply
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{tips && <Tipper comment={comment} tips={tips} />}
|
{showTip && (
|
||||||
|
<Tipper
|
||||||
|
comment={comment}
|
||||||
|
myTip={myTip ?? 0}
|
||||||
|
totalTip={totalTip ?? 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{(contract.openCommentBounties ?? 0) > 0 && (
|
{(contract.openCommentBounties ?? 0) > 0 && (
|
||||||
<AwardBountyButton comment={comment} contract={contract} />
|
<AwardBountyButton comment={comment} contract={contract} />
|
||||||
)}
|
)}
|
||||||
|
@ -193,7 +211,7 @@ export function FeedComment(props: {
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
function CommentStatus(props: {
|
function CommentStatus(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
|
|
@ -53,10 +53,9 @@ export function ContractGroupsList(props: {
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
<Col className="h-96 overflow-auto">
|
||||||
{groups.length === 0 && (
|
{groups.length === 0 && (
|
||||||
<Col className="ml-2 h-full justify-center text-gray-500">
|
<Col className="text-greyscale-4">No groups yet...</Col>
|
||||||
No groups yet...
|
|
||||||
</Col>
|
|
||||||
)}
|
)}
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<Row
|
<Row
|
||||||
|
@ -72,11 +71,12 @@ export function ContractGroupsList(props: {
|
||||||
size={'xs'}
|
size={'xs'}
|
||||||
onClick={() => removeContractFromGroup(group, contract)}
|
onClick={() => removeContractFromGroup(group, contract)}
|
||||||
>
|
>
|
||||||
<XIcon className="h-4 w-4 text-gray-500" />
|
<XIcon className="text-greyscale-4 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import clsx from 'clsx'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
|
@ -87,10 +86,8 @@ export function CreateGroupButton(props: {
|
||||||
}}
|
}}
|
||||||
submitBtn={{
|
submitBtn={{
|
||||||
label: 'Create',
|
label: 'Create',
|
||||||
className: clsx(
|
color: 'green',
|
||||||
'normal-case',
|
isSubmitting,
|
||||||
isSubmitting ? 'loading btn-disabled' : ' btn-primary'
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
onSubmitWithSuccess={onSubmit}
|
onSubmitWithSuccess={onSubmit}
|
||||||
onOpenChanged={(isOpen) => {
|
onOpenChanged={(isOpen) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx'
|
||||||
import { PencilIcon } from '@heroicons/react/outline'
|
import { PencilIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
import { Group } from 'common/group'
|
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 { Spacer } from '../layout/spacer'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
|
@ -31,6 +31,7 @@ export function EditGroupButton(props: { group: Group; className?: string }) {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
await Promise.all(addMemberUsers.map((user) => joinGroup(group, user.id)))
|
await Promise.all(addMemberUsers.map((user) => joinGroup(group, user.id)))
|
||||||
|
await updateGroup(group, { name })
|
||||||
|
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
updateOpen(false)
|
updateOpen(false)
|
||||||
|
|
|
@ -6,12 +6,13 @@ import { Spacer } from '../layout/spacer'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { deleteFieldFromGroup, updateGroup } from 'web/lib/firebase/groups'
|
import { deleteFieldFromGroup, updateGroup } from 'web/lib/firebase/groups'
|
||||||
import PencilIcon from '@heroicons/react/solid/PencilIcon'
|
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 { createPost } from 'web/lib/firebase/api'
|
||||||
import { Post } from 'common/post'
|
import { Post } from 'common/post'
|
||||||
import { deletePost, updatePost } from 'web/lib/firebase/posts'
|
import { deletePost, updatePost } from 'web/lib/firebase/posts'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { usePost } from 'web/hooks/use-post'
|
import { usePost } from 'web/hooks/use-post'
|
||||||
|
import { Col } from '../layout/col'
|
||||||
|
|
||||||
export function GroupOverviewPost(props: {
|
export function GroupOverviewPost(props: {
|
||||||
group: Group
|
group: Group
|
||||||
|
@ -43,6 +44,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
const newPost = {
|
const newPost = {
|
||||||
title: group.name,
|
title: group.name,
|
||||||
|
subtitle: 'About post for the group',
|
||||||
content: editor.getJSON(),
|
content: editor.getJSON(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,35 +100,31 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative">
|
<Col>
|
||||||
<div className="absolute top-0 right-0 z-10 space-x-2">
|
<Content content={post.content} />
|
||||||
|
<Row className="place-content-end">
|
||||||
<Button
|
<Button
|
||||||
color="gray"
|
color="gray-white"
|
||||||
size="xs"
|
size="2xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditing(true)
|
setEditing(true)
|
||||||
editor?.commands.focus('end')
|
editor?.commands.focus('end')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PencilIcon className="inline h-4 w-4" />
|
<PencilIcon className="inline h-5 w-5" />
|
||||||
Edit
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="gray"
|
color="gray-white"
|
||||||
size="xs"
|
size="2xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
deleteGroupAboutPost()
|
deleteGroupAboutPost()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DocumentRemoveIcon className="inline h-4 w-4" />
|
<TrashIcon className="inline h-5 w-5 text-red-500" />
|
||||||
Delete
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Row>
|
||||||
|
</Col>
|
||||||
<Content content={post.content} />
|
|
||||||
<Spacer h={2} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -36,8 +36,11 @@ import { CopyLinkButton } from '../copy-link-button'
|
||||||
import { REFERRAL_AMOUNT } from 'common/economy'
|
import { REFERRAL_AMOUNT } from 'common/economy'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { PostCard } from '../post-card'
|
import { PostCard, PostCardList } from '../post-card'
|
||||||
import { LoadingIndicator } from '../loading-indicator'
|
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
|
const MAX_TRENDING_POSTS = 6
|
||||||
|
|
||||||
|
@ -59,7 +62,6 @@ export function GroupOverview(props: {
|
||||||
posts={posts}
|
posts={posts}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(group.aboutPostId != null || isEditable) && (
|
{(group.aboutPostId != null || isEditable) && (
|
||||||
<>
|
<>
|
||||||
<SectionHeader label={'About'} href={'/post/' + group.slug} />
|
<SectionHeader label={'About'} href={'/post/' + group.slug} />
|
||||||
|
@ -87,10 +89,55 @@ export function GroupOverview(props: {
|
||||||
user={user}
|
user={user}
|
||||||
memberIds={memberIds}
|
memberIds={memberIds}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<GroupPosts group={group} posts={posts} />
|
||||||
</Col>
|
</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: {
|
function GroupOverviewPinned(props: {
|
||||||
group: Group
|
group: Group
|
||||||
posts: Post[]
|
posts: Post[]
|
||||||
|
|
|
@ -163,7 +163,7 @@ export function GroupSelector(props: {
|
||||||
user={creator}
|
user={creator}
|
||||||
onOpenStateChange={setIsCreatingNewGroup}
|
onOpenStateChange={setIsCreatingNewGroup}
|
||||||
className={
|
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'}
|
label={'Create a new Group'}
|
||||||
addGroupIdParamOnSubmit
|
addGroupIdParamOnSubmit
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { joinGroup, leaveGroup } from 'web/lib/firebase/groups'
|
||||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||||
import { GroupLinkItem } from 'web/pages/groups'
|
import { GroupLinkItem } from 'web/pages/groups'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
import { Button } from '../button'
|
||||||
|
|
||||||
export function GroupsButton(props: { user: User; className?: string }) {
|
export function GroupsButton(props: { user: User; className?: string }) {
|
||||||
const { user, className } = props
|
const { user, className } = props
|
||||||
|
@ -92,23 +93,22 @@ export function JoinOrLeaveGroupButton(props: {
|
||||||
group: Group
|
group: Group
|
||||||
isMember: boolean
|
isMember: boolean
|
||||||
user: User | undefined | null
|
user: User | undefined | null
|
||||||
small?: boolean
|
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { group, small, className, isMember, user } = props
|
const { group, 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'
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
if (!group.anyoneCanJoin)
|
if (!group.anyoneCanJoin)
|
||||||
return <div className={clsx(className, 'text-gray-500')}>Closed</div>
|
return <div className={clsx(className, 'text-gray-500')}>Closed</div>
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="blue"
|
||||||
onClick={firebaseLogin}
|
onClick={firebaseLogin}
|
||||||
className={clsx('btn btn-sm', small && smallStyle, className)}
|
className={className}
|
||||||
>
|
>
|
||||||
Login to follow
|
Login to follow
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const onJoinGroup = () => {
|
const onJoinGroup = () => {
|
||||||
|
@ -124,27 +124,27 @@ export function JoinOrLeaveGroupButton(props: {
|
||||||
|
|
||||||
if (isMember) {
|
if (isMember) {
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
className={clsx(
|
size="xs"
|
||||||
'btn btn-outline btn-xs',
|
color="gray-white"
|
||||||
small && smallStyle,
|
className={`${className} border-greyscale-4 border !border-solid`}
|
||||||
className
|
|
||||||
)}
|
|
||||||
onClick={withTracking(onLeaveGroup, 'leave group')}
|
onClick={withTracking(onLeaveGroup, 'leave group')}
|
||||||
>
|
>
|
||||||
Unfollow
|
Unfollow
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!group.anyoneCanJoin)
|
if (!group.anyoneCanJoin)
|
||||||
return <div className={clsx(className, 'text-gray-500')}>Closed</div>
|
return <div className={clsx(className, 'text-gray-500')}>Closed</div>
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
className={clsx('btn btn-sm', small && smallStyle, className)}
|
size="xs"
|
||||||
|
color="blue"
|
||||||
|
className={className}
|
||||||
onClick={withTracking(onJoinGroup, 'join group')}
|
onClick={withTracking(onJoinGroup, 'join group')}
|
||||||
>
|
>
|
||||||
Follow
|
Follow
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,8 @@ export default function Sidebar(props: {
|
||||||
)}
|
)}
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
{!user && <SignInButton className="mb-4" />}
|
{user === undefined && <div className="h-[178px]" />}
|
||||||
|
{user === null && <SignInButton className="mb-4" />}
|
||||||
|
|
||||||
{user && <ProfileSummary user={user} />}
|
{user && <ProfileSummary user={user} />}
|
||||||
|
|
||||||
|
|
|
@ -73,13 +73,6 @@ export function NumericResolutionPanel(props: {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitButtonClass =
|
|
||||||
outcomeMode === 'CANCEL'
|
|
||||||
? 'bg-yellow-400 hover:bg-yellow-500'
|
|
||||||
: outcome !== undefined
|
|
||||||
? 'btn-primary'
|
|
||||||
: 'btn-disabled'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -129,7 +122,6 @@ export function NumericResolutionPanel(props: {
|
||||||
onResolve={resolve}
|
onResolve={resolve}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
openModalButtonClass={clsx('w-full mt-2')}
|
openModalButtonClass={clsx('w-full mt-2')}
|
||||||
submitButtonClass={submitButtonClass}
|
|
||||||
color={outcomeMode === 'CANCEL' ? 'yellow' : 'indigo'}
|
color={outcomeMode === 'CANCEL' ? 'yellow' : 'indigo'}
|
||||||
disabled={outcomeMode === undefined}
|
disabled={outcomeMode === undefined}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { Contract } from 'common/contract'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { Post } from 'common/post'
|
import { Post } from 'common/post'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { PostCardList } from 'web/pages/group/[...slugs]'
|
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import { PillButton } from './buttons/pill-button'
|
import { PillButton } from './buttons/pill-button'
|
||||||
import { ContractSearch } from './contract-search'
|
import { ContractSearch } from './contract-search'
|
||||||
|
@ -10,6 +9,7 @@ import { Col } from './layout/col'
|
||||||
import { Modal } from './layout/modal'
|
import { Modal } from './layout/modal'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { LoadingIndicator } from './loading-indicator'
|
import { LoadingIndicator } from './loading-indicator'
|
||||||
|
import { PostCardList } from './post-card'
|
||||||
|
|
||||||
export function PinnedSelectModal(props: {
|
export function PinnedSelectModal(props: {
|
||||||
title: string
|
title: string
|
||||||
|
|
|
@ -18,8 +18,8 @@ const MARGIN_Y = MARGIN.top + MARGIN.bottom
|
||||||
export type GraphMode = 'profit' | 'value'
|
export type GraphMode = 'profit' | 'value'
|
||||||
|
|
||||||
export const PortfolioTooltip = (props: TooltipProps<Date, HistoryPoint>) => {
|
export const PortfolioTooltip = (props: TooltipProps<Date, HistoryPoint>) => {
|
||||||
const { mouseX, xScale } = props
|
const { x, xScale } = props
|
||||||
const d = dayjs(xScale.invert(mouseX))
|
const d = dayjs(xScale.invert(x))
|
||||||
return (
|
return (
|
||||||
<Col className="text-xs font-semibold sm:text-sm">
|
<Col className="text-xs font-semibold sm:text-sm">
|
||||||
<div>{d.format('MMM/D/YY')}</div>
|
<div>{d.format('MMM/D/YY')}</div>
|
||||||
|
|
|
@ -32,10 +32,10 @@ export const PortfolioValueSection = memo(
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row className="mb-2 justify-between">
|
<Row className="mb-2 justify-between">
|
||||||
<Row className="gap-4 sm:gap-8">
|
<Row className="gap-2">
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'cursor-pointer',
|
'w-24 cursor-pointer sm:w-28 ',
|
||||||
graphMode != 'profit'
|
graphMode != 'profit'
|
||||||
? 'cursor-pointer opacity-40 hover:opacity-80'
|
? 'cursor-pointer opacity-40 hover:opacity-80'
|
||||||
: ''
|
: ''
|
||||||
|
@ -72,7 +72,7 @@ export const PortfolioValueSection = memo(
|
||||||
|
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'cursor-pointer',
|
'w-24 cursor-pointer sm:w-28',
|
||||||
graphMode != 'value' ? 'opacity-40 hover:opacity-80' : ''
|
graphMode != 'value' ? 'opacity-40 hover:opacity-80' : ''
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
|
import { DocumentIcon } from '@heroicons/react/solid'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Post } from 'common/post'
|
import { Post } from 'common/post'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
@ -6,8 +7,8 @@ import { useUserById } from 'web/hooks/use-user'
|
||||||
import { postPath } from 'web/lib/firebase/posts'
|
import { postPath } from 'web/lib/firebase/posts'
|
||||||
import { fromNow } from 'web/lib/util/time'
|
import { fromNow } from 'web/lib/util/time'
|
||||||
import { Avatar } from './avatar'
|
import { Avatar } from './avatar'
|
||||||
|
import { Card } from './card'
|
||||||
import { CardHighlightOptions } from './contract/contracts-grid'
|
import { CardHighlightOptions } from './contract/contracts-grid'
|
||||||
import { Row } from './layout/row'
|
|
||||||
import { UserLink } from './user-link'
|
import { UserLink } from './user-link'
|
||||||
|
|
||||||
export function PostCard(props: {
|
export function PostCard(props: {
|
||||||
|
@ -25,9 +26,9 @@ export function PostCard(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative py-1">
|
<div className="relative py-1">
|
||||||
<Row
|
<Card
|
||||||
className={clsx(
|
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
|
itemIds?.includes(post.id) && highlightClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -44,9 +45,20 @@ export function PostCard(props: {
|
||||||
<span className="mx-1">•</span>
|
<span className="mx-1">•</span>
|
||||||
<span className="text-gray-500">{fromNow(post.createdTime)}</span>
|
<span className="text-gray-500">{fromNow(post.createdTime)}</span>
|
||||||
</div>
|
</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>
|
||||||
</Row>
|
<div className="font-small text-md break-words text-gray-500">
|
||||||
|
{post.subtitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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 ? (
|
{onPostClick ? (
|
||||||
<a
|
<a
|
||||||
className="absolute top-0 left-0 right-0 bottom-0"
|
className="absolute top-0 left-0 right-0 bottom-0"
|
||||||
|
@ -80,3 +92,23 @@ export function PostCard(props: {
|
||||||
</div>
|
</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 { DateTimeTooltip } from './datetime-tooltip'
|
||||||
import React from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { fromNow } from 'web/lib/util/time'
|
import { fromNow } from 'web/lib/util/time'
|
||||||
|
|
||||||
export function RelativeTimestamp(props: { time: number }) {
|
export function RelativeTimestamp(props: { time: number }) {
|
||||||
const { time } = props
|
const { time } = props
|
||||||
|
const [isClient, setIsClient] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only render on client to prevent difference from server.
|
||||||
|
setIsClient(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DateTimeTooltip
|
<DateTimeTooltip
|
||||||
className="ml-1 whitespace-nowrap text-gray-400"
|
className="ml-1 whitespace-nowrap text-gray-400"
|
||||||
time={time}
|
time={time}
|
||||||
>
|
>
|
||||||
{fromNow(time)}
|
{isClient ? fromNow(time) : ''}
|
||||||
</DateTimeTooltip>
|
</DateTimeTooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { CodeIcon } from '@heroicons/react/outline'
|
import { CodeIcon } from '@heroicons/react/outline'
|
||||||
import { Menu } from '@headlessui/react'
|
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
|
@ -8,6 +7,7 @@ import { contractPath } from 'web/lib/firebase/contracts'
|
||||||
import { DOMAIN } from 'common/envs/constants'
|
import { DOMAIN } from 'common/envs/constants'
|
||||||
import { copyToClipboard } from 'web/lib/util/copy'
|
import { copyToClipboard } from 'web/lib/util/copy'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
import { Button } from './button'
|
||||||
|
|
||||||
export function embedContractCode(contract: Contract) {
|
export function embedContractCode(contract: Contract) {
|
||||||
const title = contract.question
|
const title = contract.question
|
||||||
|
@ -15,6 +15,7 @@ export function embedContractCode(contract: Contract) {
|
||||||
return `<iframe src="${src}" title="${title}" frameborder="0"></iframe>`
|
return `<iframe src="${src}" title="${title}" frameborder="0"></iframe>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: move this function elsewhere
|
||||||
export function embedContractGridCode(contracts: Contract[]) {
|
export function embedContractGridCode(contracts: Contract[]) {
|
||||||
const height = (contracts.length - (contracts.length % 2)) * 100 + 'px'
|
const height = (contracts.length - (contracts.length % 2)) * 100 + 'px'
|
||||||
const src = `https://${DOMAIN}/embed/grid/${contracts
|
const src = `https://${DOMAIN}/embed/grid/${contracts
|
||||||
|
@ -26,24 +27,21 @@ export function embedContractGridCode(contracts: Contract[]) {
|
||||||
export function ShareEmbedButton(props: { contract: Contract }) {
|
export function ShareEmbedButton(props: { contract: Contract }) {
|
||||||
const { contract } = props
|
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 (
|
return (
|
||||||
<Menu
|
<Button
|
||||||
as="div"
|
size="2xs"
|
||||||
className="relative z-10 flex-shrink-0"
|
color="gray-outline"
|
||||||
onMouseUp={() => {
|
className="gap-1"
|
||||||
|
onClick={() => {
|
||||||
copyToClipboard(embedContractCode(contract))
|
copyToClipboard(embedContractCode(contract))
|
||||||
toast.success('Embed code copied!', {
|
toast.success('Embed code copied!', { icon: codeIcon })
|
||||||
icon: codeIcon,
|
|
||||||
})
|
|
||||||
track('copy embed code')
|
track('copy embed code')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu.Button className="btn btn-xs border-2 !border-gray-500 !bg-white normal-case text-gray-500">
|
|
||||||
{codeIcon}
|
{codeIcon}
|
||||||
Embed
|
Embed
|
||||||
</Menu.Button>
|
</Button>
|
||||||
</Menu>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { Modal } from './layout/modal'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Title } from './title'
|
import { Title } from './title'
|
||||||
import { Button } from './button'
|
|
||||||
import { TweetButton } from './tweet-button'
|
import { TweetButton } from './tweet-button'
|
||||||
import { Row } from './layout/row'
|
import { CopyLinkButton } from './copy-link-button'
|
||||||
|
|
||||||
export function SharePostModal(props: {
|
export function SharePostModal(props: {
|
||||||
shareUrl: string
|
shareUrl: string
|
||||||
|
@ -16,30 +11,14 @@ export function SharePostModal(props: {
|
||||||
}) {
|
}) {
|
||||||
const { isOpen, setOpen, shareUrl } = props
|
const { isOpen, setOpen, shareUrl } = props
|
||||||
|
|
||||||
const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" />
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isOpen} setOpen={setOpen} size="md">
|
<Modal open={isOpen} setOpen={setOpen} size="md">
|
||||||
<Col className="gap-4 rounded bg-white p-4">
|
<Col className="gap-4 rounded bg-white p-4">
|
||||||
<Title className="!mt-0 !mb-2" text="Share this post" />
|
<Title className="!mt-0 !mb-2" text="Share this post" />
|
||||||
<Button
|
<CopyLinkButton url={shareUrl} tracking="copy share post link" />
|
||||||
size="2xl"
|
<div className="self-center">
|
||||||
color="gradient"
|
<TweetButton tweetText={shareUrl} />
|
||||||
className={'mb-2 flex max-w-xs self-center'}
|
</div>
|
||||||
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>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,7 +29,14 @@ export const SizedContainer = (props: {
|
||||||
}, [threshold, fullHeight, mobileHeight])
|
}, [threshold, fullHeight, mobileHeight])
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef}>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { debounce, sum } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
|
|
||||||
import { Comment } from 'common/comment'
|
import { Comment } from 'common/comment'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { CommentTips } from 'web/hooks/use-tip-txns'
|
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { transact } from 'web/lib/firebase/api'
|
import { transact } from 'web/lib/firebase/api'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
@ -13,25 +12,27 @@ import { Row } from './layout/row'
|
||||||
import { LIKE_TIP_AMOUNT } from 'common/like'
|
import { LIKE_TIP_AMOUNT } from 'common/like'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
|
|
||||||
export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
export function Tipper(prop: {
|
||||||
const { comment, tips } = prop
|
comment: Comment
|
||||||
|
myTip: number
|
||||||
|
totalTip: number
|
||||||
|
}) {
|
||||||
|
const { comment, myTip, totalTip } = prop
|
||||||
|
|
||||||
const me = useUser()
|
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
|
// listen for user being set
|
||||||
const initialized = useRef(false)
|
const initialized = useRef(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tips[myId] && !initialized.current) {
|
if (myTip && !initialized.current) {
|
||||||
setLocalTip(tips[myId])
|
setLocalTip(myTip)
|
||||||
initialized.current = true
|
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
|
// declare debounced function only on first render
|
||||||
const [saveTip] = useState(() =>
|
const [saveTip] = useState(() =>
|
||||||
|
@ -73,7 +74,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
||||||
|
|
||||||
const addTip = (delta: number) => {
|
const addTip = (delta: number) => {
|
||||||
setLocalTip(localTip + delta)
|
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)}!`)
|
toast(`You tipped ${comment.userName} ${formatMoney(LIKE_TIP_AMOUNT)}!`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import TwitterLogo from 'web/lib/icons/twitter-logo'
|
||||||
import { trackCallback } from 'web/lib/service/analytics'
|
import { trackCallback } from 'web/lib/service/analytics'
|
||||||
|
import { buttonClass } from './button'
|
||||||
|
|
||||||
export function TweetButton(props: { className?: string; tweetText: string }) {
|
export function TweetButton(props: { tweetText: string }) {
|
||||||
const { tweetText, className } = props
|
const { tweetText } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className={clsx('btn btn-xs flex-nowrap normal-case', className)}
|
// #1da1f2 is twitter blue
|
||||||
style={{
|
className={clsx(
|
||||||
backgroundColor: 'white',
|
buttonClass('2xs', 'override'),
|
||||||
border: '2px solid #1da1f2',
|
'gap-1 border-2 border-[#1da1f2] text-[#1da1f2] hover:bg-[#1da1f2] hover:text-white'
|
||||||
color: '#1da1f2',
|
)}
|
||||||
}}
|
|
||||||
href={getTweetHref(tweetText)}
|
href={getTweetHref(tweetText)}
|
||||||
onClick={trackCallback('share tweet')}
|
onClick={trackCallback('share tweet')}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<img className="mr-2" src={'/twitter-logo.svg'} width={15} height={15} />
|
<TwitterLogo width={15} height={15} />
|
||||||
<div>Tweet</div>
|
<div>Tweet</div>
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,12 +3,10 @@ import Router from 'next/router'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { getProbability } from 'common/calculate'
|
import { getProbability } from 'common/calculate'
|
||||||
import { Contract, CPMMBinaryContract } from 'common/contract'
|
import { CPMMBinaryContract } from 'common/contract'
|
||||||
import { Customize, USAMap } from './usa-map'
|
import { Customize, USAMap } from './usa-map'
|
||||||
import {
|
import { listenForContract } from 'web/lib/firebase/contracts'
|
||||||
getContractFromSlug,
|
import { interpolateColor } from 'common/util/color'
|
||||||
listenForContract,
|
|
||||||
} from 'web/lib/firebase/contracts'
|
|
||||||
|
|
||||||
export interface StateElectionMarket {
|
export interface StateElectionMarket {
|
||||||
creatorUsername: string
|
creatorUsername: string
|
||||||
|
@ -17,10 +15,14 @@ export interface StateElectionMarket {
|
||||||
state: string
|
state: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StateElectionMap(props: { markets: StateElectionMarket[] }) {
|
export function StateElectionMap(props: {
|
||||||
|
markets: StateElectionMarket[]
|
||||||
|
contracts: CPMMBinaryContract[]
|
||||||
|
}) {
|
||||||
const { markets } = props
|
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) =>
|
const probs = contracts.map((c) =>
|
||||||
c ? getProbability(c as CPMMBinaryContract) : 0.5
|
c ? getProbability(c as CPMMBinaryContract) : 0.5
|
||||||
)
|
)
|
||||||
|
@ -45,35 +47,30 @@ export function StateElectionMap(props: { markets: StateElectionMarket[] }) {
|
||||||
|
|
||||||
const probToColor = (prob: number, isWinRepublican: boolean) => {
|
const probToColor = (prob: number, isWinRepublican: boolean) => {
|
||||||
const p = isWinRepublican ? prob : 1 - prob
|
const p = isWinRepublican ? prob : 1 - prob
|
||||||
const hue = p > 0.5 ? 350 : 240
|
const color = p > 0.5 ? '#e4534b' : '#5f6eb0'
|
||||||
const saturation = 100
|
return interpolateColor('#ebe4ec', color, Math.abs(p - 0.5) * 2)
|
||||||
const lightness = 100 - 50 * Math.abs(p - 0.5)
|
|
||||||
return `hsl(${hue}, ${saturation}%, ${lightness}%)`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useContracts = (slugs: string[]) => {
|
const useUpdateContracts = (
|
||||||
const [contracts, setContracts] = useState<(Contract | undefined)[]>(
|
contracts: CPMMBinaryContract[],
|
||||||
slugs.map(() => undefined)
|
setContracts: (newContracts: CPMMBinaryContract[]) => void
|
||||||
)
|
) => {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Promise.all(slugs.map((slug) => getContractFromSlug(slug))).then(
|
|
||||||
(contracts) => setContracts(contracts)
|
|
||||||
)
|
|
||||||
}, [slugs])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contracts.some((c) => c === undefined)) return
|
if (contracts.some((c) => c === undefined)) return
|
||||||
|
|
||||||
// listen to contract updates
|
// listen to contract updates
|
||||||
const unsubs = (contracts as Contract[]).map((c, i) =>
|
const unsubs = contracts
|
||||||
|
.filter((c) => !!c)
|
||||||
|
.map((c, i) =>
|
||||||
listenForContract(
|
listenForContract(
|
||||||
c.id,
|
c.id,
|
||||||
(newC) => newC && setContracts(setAt(contracts, i, newC))
|
(newC) =>
|
||||||
|
newC &&
|
||||||
|
setContracts(setAt(contracts, i, newC as CPMMBinaryContract))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return () => unsubs.forEach((u) => u())
|
return () => unsubs.forEach((u) => u())
|
||||||
}, [contracts])
|
}, [contracts, setContracts])
|
||||||
|
|
||||||
return contracts
|
return contracts
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// https://github.com/jb-1980/usa-map-react
|
// https://github.com/jb-1980/usa-map-react
|
||||||
// MIT License
|
// MIT License
|
||||||
|
|
||||||
|
import clsx from 'clsx'
|
||||||
import { DATA } from './data'
|
import { DATA } from './data'
|
||||||
import { USAState } from './usa-state'
|
import { USAState } from './usa-state'
|
||||||
|
|
||||||
|
@ -53,8 +54,6 @@ export const USAMap = ({
|
||||||
onClick = (e) => {
|
onClick = (e) => {
|
||||||
console.log(e.currentTarget.dataset.name)
|
console.log(e.currentTarget.dataset.name)
|
||||||
},
|
},
|
||||||
width = 959,
|
|
||||||
height = 593,
|
|
||||||
title = 'US states map',
|
title = 'US states map',
|
||||||
defaultFill = '#d3d3d3',
|
defaultFill = '#d3d3d3',
|
||||||
customize,
|
customize,
|
||||||
|
@ -68,10 +67,8 @@ export const USAMap = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className={className}
|
className={clsx('flex h-96 w-full sm:h-full', className)}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
viewBox="0 0 959 593"
|
viewBox="0 0 959 593"
|
||||||
>
|
>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
|
|
|
@ -11,12 +11,13 @@ export function WarningConfirmationButton(props: {
|
||||||
amount: number | undefined
|
amount: number | undefined
|
||||||
marketType: 'freeResponse' | 'binary'
|
marketType: 'freeResponse' | 'binary'
|
||||||
warning?: string
|
warning?: string
|
||||||
onSubmit: () => void
|
onSubmit?: () => void
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
isSubmitting: boolean
|
isSubmitting: boolean
|
||||||
openModalButtonClass?: string
|
openModalButtonClass?: string
|
||||||
color: ColorType
|
color: ColorType
|
||||||
size: SizeType
|
size: SizeType
|
||||||
|
actionLabel: string
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
amount,
|
amount,
|
||||||
|
@ -27,8 +28,15 @@ export function WarningConfirmationButton(props: {
|
||||||
openModalButtonClass,
|
openModalButtonClass,
|
||||||
size,
|
size,
|
||||||
color,
|
color,
|
||||||
|
actionLabel,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
const buttonText = isSubmitting
|
||||||
|
? 'Submitting...'
|
||||||
|
: amount
|
||||||
|
? `${actionLabel} ${formatMoney(amount)}`
|
||||||
|
: actionLabel
|
||||||
|
|
||||||
if (!warning) {
|
if (!warning) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -38,11 +46,7 @@ export function WarningConfirmationButton(props: {
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
color={color}
|
color={color}
|
||||||
>
|
>
|
||||||
{isSubmitting
|
{buttonText}
|
||||||
? 'Submitting...'
|
|
||||||
: amount
|
|
||||||
? `Wager ${formatMoney(amount)}`
|
|
||||||
: 'Wager'}
|
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -50,18 +54,18 @@ export function WarningConfirmationButton(props: {
|
||||||
return (
|
return (
|
||||||
<ConfirmationButton
|
<ConfirmationButton
|
||||||
openModalBtn={{
|
openModalBtn={{
|
||||||
label: amount ? `Wager ${formatMoney(amount)}` : 'Wager',
|
label: buttonText,
|
||||||
size: size,
|
size: size,
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
disabled: isSubmitting,
|
disabled: isSubmitting || disabled,
|
||||||
}}
|
}}
|
||||||
cancelBtn={{
|
cancelBtn={{
|
||||||
label: 'Cancel',
|
label: 'Cancel',
|
||||||
className: 'btn btn-warning',
|
color: 'yellow',
|
||||||
}}
|
}}
|
||||||
submitBtn={{
|
submitBtn={{
|
||||||
label: 'Submit',
|
label: 'Submit',
|
||||||
className: clsx('btn border-none btn-sm btn-ghost self-center'),
|
color: 'gray',
|
||||||
}}
|
}}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
|
|
|
@ -244,7 +244,7 @@ function Button(props: {
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'inline-flex flex-1 items-center justify-center rounded-md border border-transparent px-8 py-3 font-medium shadow-sm',
|
'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 === 'red' && 'bg-red-400 text-white hover:bg-red-500',
|
||||||
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
|
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
|
||||||
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
|
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
|
||||||
|
|
|
@ -92,7 +92,7 @@ export const useContractsByDailyScoreGroups = (
|
||||||
|
|
||||||
const q = new QueryClient()
|
const q = new QueryClient()
|
||||||
export const getCachedContracts = async () =>
|
export const getCachedContracts = async () =>
|
||||||
q.fetchQuery(['contracts'], () => listAllContracts(1000), {
|
q.fetchQuery(['contracts'], () => listAllContracts(10000), {
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -91,9 +91,15 @@ export const usePagination = <T>(opts: PaginationOptions<T>) => {
|
||||||
const nextQ = lastDoc
|
const nextQ = lastDoc
|
||||||
? query(state.baseQ, startAfter(lastDoc), limit(state.pageSize))
|
? query(state.baseQ, startAfter(lastDoc), limit(state.pageSize))
|
||||||
: query(state.baseQ, limit(state.pageSize))
|
: query(state.baseQ, limit(state.pageSize))
|
||||||
return onSnapshot(nextQ, (snapshot) => {
|
return onSnapshot(
|
||||||
|
nextQ,
|
||||||
|
(snapshot) => {
|
||||||
dispatch({ type: 'LOAD', snapshot })
|
dispatch({ type: 'LOAD', snapshot })
|
||||||
})
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [state.isLoading, state.baseQ, state.docs, state.pageSize])
|
}, [state.isLoading, state.baseQ, state.docs, state.pageSize])
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useStateCheckEquality } from './use-state-check-equality'
|
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 type PersistenceOptions<T> = { key: string; store: PersistentStore<T> }
|
||||||
|
|
||||||
export interface PersistentStore<T> {
|
export interface PersistentStore<T> {
|
||||||
get: (k: string) => T | undefined
|
get: (k: string) => T | undefined
|
||||||
set: (k: string, v: T | undefined) => void
|
set: (k: string, v: T | undefined) => void
|
||||||
|
readsUrl?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const withURLParam = (location: Location, k: string, v?: string) => {
|
const withURLParam = (location: Location, k: string, v?: string) => {
|
||||||
|
@ -61,6 +62,7 @@ export const urlParamStore = (router: NextRouter): PersistentStore<string> => ({
|
||||||
window.history.replaceState(updatedState, '', url)
|
window.history.replaceState(updatedState, '', url)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
readsUrl: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const historyStore = <T>(prefix = '__manifold'): PersistentStore<T> => ({
|
export const historyStore = <T>(prefix = '__manifold'): PersistentStore<T> => ({
|
||||||
|
@ -102,5 +104,20 @@ export const usePersistentState = <T>(
|
||||||
store.set(key, state)
|
store.set(key, state)
|
||||||
}
|
}
|
||||||
}, [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
|
return [state, setState] as const
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,6 @@ export const tournamentContractsByGroupSlugQuery = (slug: string) =>
|
||||||
query(
|
query(
|
||||||
contracts,
|
contracts,
|
||||||
where('groupSlugs', 'array-contains', slug),
|
where('groupSlugs', 'array-contains', slug),
|
||||||
where('isResolved', '==', false),
|
|
||||||
orderBy('popularityScore', 'desc')
|
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,7 +5,8 @@
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { PROD_CONFIG } from 'common/envs/prod'
|
import { PROD_CONFIG } from 'common/envs/prod'
|
||||||
|
|
||||||
try {
|
if (ENV_CONFIG.domain === PROD_CONFIG.domain) {
|
||||||
|
try {
|
||||||
;(function (l, e, a, p) {
|
;(function (l, e, a, p) {
|
||||||
if (window.Sprig) return
|
if (window.Sprig) return
|
||||||
window.Sprig = function (...args) {
|
window.Sprig = function (...args) {
|
||||||
|
@ -19,16 +20,17 @@ try {
|
||||||
a.async = 1
|
a.async = 1
|
||||||
a.src = e + '?id=' + S.appId
|
a.src = e + '?id=' + S.appId
|
||||||
p = l.getElementsByTagName('script')[0]
|
p = l.getElementsByTagName('script')[0]
|
||||||
ENV_CONFIG.domain === PROD_CONFIG.domain && p.parentNode.insertBefore(a, p)
|
p.parentNode.insertBefore(a, p)
|
||||||
})(document, 'https://cdn.sprig.com/shim.js', ENV_CONFIG.sprigEnvironmentId)
|
})(document, 'https://cdn.sprig.com/shim.js', ENV_CONFIG.sprigEnvironmentId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error initializing Sprig, please complain to Barak', error)
|
console.log('Error initializing Sprig, please complain to Barak', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setUserId(userId: string): void {
|
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 {
|
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",
|
"@amplitude/analytics-browser": "0.4.1",
|
||||||
"@floating-ui/react-dom-interactions": "0.9.2",
|
"@floating-ui/react-dom-interactions": "0.9.2",
|
||||||
"@headlessui/react": "1.6.1",
|
"@headlessui/react": "1.6.1",
|
||||||
|
"@hello-pangea/dnd": "16.0.0",
|
||||||
"@heroicons/react": "1.0.6",
|
"@heroicons/react": "1.0.6",
|
||||||
"@react-query-firebase/firestore": "0.4.2",
|
"@react-query-firebase/firestore": "0.4.2",
|
||||||
"@tiptap/core": "2.0.0-beta.182",
|
"@tiptap/core": "2.0.0-beta.182",
|
||||||
|
@ -54,11 +55,10 @@
|
||||||
"next": "12.3.1",
|
"next": "12.3.1",
|
||||||
"node-fetch": "3.2.4",
|
"node-fetch": "3.2.4",
|
||||||
"prosemirror-state": "1.4.1",
|
"prosemirror-state": "1.4.1",
|
||||||
"react": "17.0.2",
|
"react": "18.2.0",
|
||||||
"react-beautiful-dnd": "13.1.1",
|
|
||||||
"react-confetti": "6.0.1",
|
"react-confetti": "6.0.1",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "18.2.0",
|
||||||
"react-expanding-textarea": "2.3.5",
|
"react-expanding-textarea": "2.3.6",
|
||||||
"react-hot-toast": "2.2.0",
|
"react-hot-toast": "2.2.0",
|
||||||
"react-instantsearch-hooks-web": "6.24.1",
|
"react-instantsearch-hooks-web": "6.24.1",
|
||||||
"react-masonry-css": "1.0.16",
|
"react-masonry-css": "1.0.16",
|
||||||
|
@ -75,9 +75,8 @@
|
||||||
"@types/d3": "7.4.0",
|
"@types/d3": "7.4.0",
|
||||||
"@types/lodash": "4.14.178",
|
"@types/lodash": "4.14.178",
|
||||||
"@types/node": "16.11.11",
|
"@types/node": "16.11.11",
|
||||||
"@types/react": "17.0.43",
|
"@types/react": "18.0.21",
|
||||||
"@types/react-beautiful-dnd": "13.1.2",
|
"@types/react-dom": "18.0.6",
|
||||||
"@types/react-dom": "17.0.2",
|
|
||||||
"@types/string-similarity": "^4.0.0",
|
"@types/string-similarity": "^4.0.0",
|
||||||
"autoprefixer": "10.2.6",
|
"autoprefixer": "10.2.6",
|
||||||
"critters": "0.0.16",
|
"critters": "0.0.16",
|
||||||
|
|
|
@ -207,6 +207,7 @@ export function ContractPageContent(
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
rightSidebar={
|
rightSidebar={
|
||||||
|
user || user === null ? (
|
||||||
<>
|
<>
|
||||||
<ContractPageSidebar contract={contract} />
|
<ContractPageSidebar contract={contract} />
|
||||||
{isCreator && (
|
{isCreator && (
|
||||||
|
@ -215,6 +216,9 @@ export function ContractPageContent(
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{showConfetti && (
|
{showConfetti && (
|
||||||
|
|
|
@ -42,7 +42,7 @@ function MyApp({ Component, pageProps }: AppProps<ManifoldPageProps>) {
|
||||||
`}
|
`}
|
||||||
</Script>
|
</Script>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Manifold Markets — A market for every question</title>
|
<title>{'Manifold Markets — A market for every question'}</title>
|
||||||
|
|
||||||
<meta
|
<meta
|
||||||
property="og:title"
|
property="og:title"
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { Page } from 'web/components/page'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { trackCallback } from 'web/lib/service/analytics'
|
import { trackCallback } from 'web/lib/service/analytics'
|
||||||
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
||||||
|
import { Button } from 'web/components/button'
|
||||||
|
|
||||||
export const getServerSideProps = redirectIfLoggedOut('/')
|
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">
|
<Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md">
|
||||||
<Title className="!mt-0" text="Get Mana" />
|
<Title className="!mt-0" text="Get Mana" />
|
||||||
<img
|
<img
|
||||||
className="mb-6 block -scale-x-100 self-center"
|
className="mb-6 block self-center"
|
||||||
src="/stylized-crane-black.png"
|
src="/welcome/manipurple.png"
|
||||||
width={200}
|
width={200}
|
||||||
height={200}
|
height={200}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mb-6 text-gray-500">
|
<div className="mb-6 text-gray-500">
|
||||||
Buy mana (M$) to trade in your favorite markets. <br /> (Not
|
Buy mana (M$) to trade in your favorite markets. <br />{' '}
|
||||||
redeemable for cash.)
|
<i>Not redeemable for cash.</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-2 text-sm text-gray-500">Amount</div>
|
<div className="mb-2 text-sm text-gray-500">Amount</div>
|
||||||
|
@ -63,13 +64,15 @@ export default function AddFundsPage() {
|
||||||
method="POST"
|
method="POST"
|
||||||
className="mt-8"
|
className="mt-8"
|
||||||
>
|
>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
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 })}
|
onClick={trackCallback('checkout', { amount: amountSelected })}
|
||||||
>
|
>
|
||||||
Checkout
|
Checkout
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { ExternalLinkIcon } from '@heroicons/react/outline'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { Button } from 'web/components/button'
|
import { Button } from 'web/components/button'
|
||||||
import { AddFundsModal } from 'web/components/add-funds-modal'
|
import { AddFundsModal } from 'web/components/add-funds-modal'
|
||||||
|
import ShortToggle from 'web/components/widgets/short-toggle'
|
||||||
|
|
||||||
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
|
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
|
||||||
return { props: { auth: await getUserAndPrivateUser(creds.uid) } }
|
return { props: { auth: await getUserAndPrivateUser(creds.uid) } }
|
||||||
|
@ -50,6 +51,7 @@ type NewQuestionParams = {
|
||||||
description: string
|
description: string
|
||||||
closeTime: string
|
closeTime: string
|
||||||
outcomeType: string
|
outcomeType: string
|
||||||
|
visibility: string
|
||||||
// Params for PSEUDO_NUMERIC outcomeType
|
// Params for PSEUDO_NUMERIC outcomeType
|
||||||
min?: string
|
min?: string
|
||||||
max?: string
|
max?: string
|
||||||
|
@ -136,7 +138,9 @@ export function NewContract(props: {
|
||||||
const [maxString, setMaxString] = useState(params?.max ?? '')
|
const [maxString, setMaxString] = useState(params?.max ?? '')
|
||||||
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
||||||
const [initialValueString, setInitialValueString] = useState(initValue)
|
const [initialValueString, setInitialValueString] = useState(initValue)
|
||||||
|
const [visibility, setVisibility] = useState<visibility>(
|
||||||
|
(params?.visibility as visibility) ?? 'public'
|
||||||
|
)
|
||||||
// for multiple choice, init to 3 empty answers
|
// for multiple choice, init to 3 empty answers
|
||||||
const [answers, setAnswers] = useState(['', '', ''])
|
const [answers, setAnswers] = useState(['', '', ''])
|
||||||
|
|
||||||
|
@ -168,7 +172,6 @@ export function NewContract(props: {
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
const [showGroupSelector, setShowGroupSelector] = useState(true)
|
const [showGroupSelector, setShowGroupSelector] = useState(true)
|
||||||
const [visibility, setVisibility] = useState<visibility>('public')
|
|
||||||
|
|
||||||
const [fundsModalOpen, setFundsModalOpen] = useState(false)
|
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">
|
<Row className="form-control my-2 items-center gap-2 text-sm">
|
||||||
<span>Display this market on homepage</span>
|
<span>Display this market on homepage</span>
|
||||||
<input
|
<ShortToggle
|
||||||
type="checkbox"
|
on={visibility === 'public'}
|
||||||
checked={visibility === 'public'}
|
setOn={(on) => setVisibility(on ? 'public' : 'unlisted')}
|
||||||
disabled={isSubmitting}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onChange={(e) =>
|
|
||||||
setVisibility(e.target.checked ? 'public' : 'unlisted')
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ export default function CreateDateDocPage() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const title = `${user?.name}'s Date Doc`
|
const title = `${user?.name}'s Date Doc`
|
||||||
|
const subtitle = 'Manifold dating docs'
|
||||||
const [birthday, setBirthday] = useState<undefined | string>(undefined)
|
const [birthday, setBirthday] = useState<undefined | string>(undefined)
|
||||||
const [question, setQuestion] = useState(
|
const [question, setQuestion] = useState(
|
||||||
'Will I find a partner in the next 3 months?'
|
'Will I find a partner in the next 3 months?'
|
||||||
|
@ -46,6 +47,7 @@ export default function CreateDateDocPage() {
|
||||||
'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug'
|
'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug'
|
||||||
> & { question: string } = {
|
> & { question: string } = {
|
||||||
title,
|
title,
|
||||||
|
subtitle,
|
||||||
content: editor.getJSON(),
|
content: editor.getJSON(),
|
||||||
bounty: 0,
|
bounty: 0,
|
||||||
birthday: birthdayTime,
|
birthday: birthdayTime,
|
||||||
|
|
|
@ -42,11 +42,7 @@ import { SelectMarketsModal } from 'web/components/contract-select-modal'
|
||||||
import { BETTORS } from 'common/user'
|
import { BETTORS } from 'common/user'
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { Tabs } from 'web/components/layout/tabs'
|
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 { 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 const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
|
@ -183,16 +179,6 @@ export default function GroupPage(props: {
|
||||||
</Col>
|
</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 = (
|
const overviewPage = (
|
||||||
<>
|
<>
|
||||||
<GroupOverview
|
<GroupOverview
|
||||||
|
@ -249,10 +235,6 @@ export default function GroupPage(props: {
|
||||||
title: 'Leaderboards',
|
title: 'Leaderboards',
|
||||||
content: leaderboardTab,
|
content: leaderboardTab,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Posts',
|
|
||||||
content: postsPage,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
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 }) {
|
function AddContractButton(props: { group: Group; user: User }) {
|
||||||
const { group, user } = props
|
const { group, user } = props
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
|
@ -21,7 +21,6 @@ import { Group } from 'common/group'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { usePrivateUser, useUser } from 'web/hooks/use-user'
|
import { usePrivateUser, useUser } from 'web/hooks/use-user'
|
||||||
import {
|
import {
|
||||||
useMemberGroupIds,
|
|
||||||
useMemberGroupsSubscription,
|
useMemberGroupsSubscription,
|
||||||
useTrendingGroups,
|
useTrendingGroups,
|
||||||
} from 'web/hooks/use-group'
|
} from 'web/hooks/use-group'
|
||||||
|
@ -80,6 +79,7 @@ export default function Home() {
|
||||||
const dailyTrendingContracts = useContractsByDailyScoreNotBetOn(user?.id, 6)
|
const dailyTrendingContracts = useContractsByDailyScoreNotBetOn(user?.id, 6)
|
||||||
|
|
||||||
const groups = useMemberGroupsSubscription(user)
|
const groups = useMemberGroupsSubscription(user)
|
||||||
|
const trendingGroups = useTrendingGroups()
|
||||||
const groupContracts = useContractsByDailyScoreGroups(
|
const groupContracts = useContractsByDailyScoreGroups(
|
||||||
groups?.map((g) => g.slug)
|
groups?.map((g) => g.slug)
|
||||||
)
|
)
|
||||||
|
@ -113,17 +113,27 @@ export default function Home() {
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{renderSections(user, sections, {
|
{renderSections(sections, {
|
||||||
score: trendingContracts,
|
score: trendingContracts,
|
||||||
newest: newContracts,
|
newest: newContracts,
|
||||||
'daily-trending': dailyTrendingContracts,
|
'daily-trending': dailyTrendingContracts,
|
||||||
'daily-movers': dailyMovers,
|
'daily-movers': dailyMovers,
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<TrendingGroupsSection user={user} />
|
{groups && groupContracts && trendingGroups.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<TrendingGroupsSection
|
||||||
|
className="mb-4"
|
||||||
|
user={user}
|
||||||
|
myGroups={groups}
|
||||||
|
trendingGroups={trendingGroups}
|
||||||
|
/>
|
||||||
{renderGroupSections(user, groups, groupContracts)}
|
{renderGroupSections(user, groups, groupContracts)}
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<LoadingIndicator />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
@ -171,7 +181,6 @@ export const getHomeItems = (sections: string[]) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSections(
|
function renderSections(
|
||||||
user: User,
|
|
||||||
sections: { id: string; label: string }[],
|
sections: { id: string; label: string }[],
|
||||||
sectionContracts: {
|
sectionContracts: {
|
||||||
'daily-movers': CPMMBinaryContract[]
|
'daily-movers': CPMMBinaryContract[]
|
||||||
|
@ -192,7 +201,7 @@ function renderSections(
|
||||||
}
|
}
|
||||||
if (id === 'daily-trending') {
|
if (id === 'daily-trending') {
|
||||||
return (
|
return (
|
||||||
<ContractsSection
|
<SearchSection
|
||||||
key={id}
|
key={id}
|
||||||
label={label}
|
label={label}
|
||||||
contracts={contracts}
|
contracts={contracts}
|
||||||
|
@ -202,11 +211,11 @@ function renderSections(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ContractsSection
|
<SearchSection
|
||||||
key={id}
|
key={id}
|
||||||
label={label}
|
label={label}
|
||||||
contracts={contracts}
|
contracts={contracts}
|
||||||
sort={id === 'daily-trending' ? 'daily-score' : (id as Sort)}
|
sort={id as Sort}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -216,13 +225,9 @@ function renderSections(
|
||||||
|
|
||||||
function renderGroupSections(
|
function renderGroupSections(
|
||||||
user: User,
|
user: User,
|
||||||
groups: Group[] | undefined,
|
groups: Group[],
|
||||||
groupContracts: Dictionary<CPMMBinaryContract[]> | undefined
|
groupContracts: Dictionary<CPMMBinaryContract[]>
|
||||||
) {
|
) {
|
||||||
if (!groups || !groupContracts) {
|
|
||||||
return <LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredGroups = groups.filter((g) => groupContracts[g.slug])
|
const filteredGroups = groups.filter((g) => groupContracts[g.slug])
|
||||||
const orderedGroups = sortBy(filteredGroups, (g) =>
|
const orderedGroups = sortBy(filteredGroups, (g) =>
|
||||||
// Sort by sum of top two daily scores.
|
// Sort by sum of top two daily scores.
|
||||||
|
@ -285,7 +290,7 @@ function SectionHeader(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContractsSection(props: {
|
function SearchSection(props: {
|
||||||
label: string
|
label: string
|
||||||
contracts: CPMMBinaryContract[]
|
contracts: CPMMBinaryContract[]
|
||||||
sort: Sort
|
sort: Sort
|
||||||
|
@ -406,15 +411,16 @@ function DailyStats(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TrendingGroupsSection(props: {
|
export function TrendingGroupsSection(props: {
|
||||||
user: User | null | undefined
|
user: User
|
||||||
|
myGroups: Group[]
|
||||||
|
trendingGroups: Group[]
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { user, className } = props
|
const { user, myGroups, trendingGroups, className } = props
|
||||||
const memberGroupIds = useMemberGroupIds(user) || []
|
|
||||||
|
|
||||||
const groups = useTrendingGroups().filter(
|
const myGroupIds = new Set(myGroups.map((g) => g.id))
|
||||||
(g) => !memberGroupIds.includes(g.id)
|
|
||||||
)
|
const groups = trendingGroups.filter((g) => !myGroupIds.has(g.id))
|
||||||
const count = 20
|
const count = 20
|
||||||
const chosenGroups = groups.slice(0, count)
|
const chosenGroups = groups.slice(0, count)
|
||||||
|
|
||||||
|
@ -433,10 +439,9 @@ export function TrendingGroupsSection(props: {
|
||||||
<PillButton
|
<PillButton
|
||||||
className="flex flex-row items-center gap-1"
|
className="flex flex-row items-center gap-1"
|
||||||
key={g.id}
|
key={g.id}
|
||||||
selected={memberGroupIds.includes(g.id)}
|
selected={myGroupIds.has(g.id)}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
if (!user) return
|
if (myGroupIds.has(g.id)) leaveGroup(g, user.id)
|
||||||
if (memberGroupIds.includes(g.id)) leaveGroup(g, user?.id)
|
|
||||||
else {
|
else {
|
||||||
const homeSections = (user.homeSections ?? [])
|
const homeSections = (user.homeSections ?? [])
|
||||||
.filter((id) => id !== g.id)
|
.filter((id) => id !== g.id)
|
||||||
|
|
|
@ -44,8 +44,8 @@ export default function LabsPage() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabCard
|
<LabCard
|
||||||
title="💌 Dating docs"
|
title="💌 Dating"
|
||||||
description="Browse dating docs or create your own"
|
description="Browse dating profiles and bet on relationships"
|
||||||
href="/date-docs"
|
href="/date-docs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
|
import { CPMMBinaryContract } from 'common/contract'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { Spacer } from 'web/components/layout/spacer'
|
import { Spacer } from 'web/components/layout/spacer'
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
|
import { SEO } from 'web/components/SEO'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import {
|
import {
|
||||||
StateElectionMarket,
|
StateElectionMarket,
|
||||||
StateElectionMap,
|
StateElectionMap,
|
||||||
} from 'web/components/usa-map/state-election-map'
|
} from 'web/components/usa-map/state-election-map'
|
||||||
|
import { getContractFromSlug } from 'web/lib/firebase/contracts'
|
||||||
|
|
||||||
const senateMidterms: StateElectionMarket[] = [
|
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 (
|
return (
|
||||||
<Page className="">
|
<Page className="">
|
||||||
<Col className="items-center justify-center">
|
<Col className="items-center justify-center">
|
||||||
<Title text="2022 US Midterm Elections" className="mt-2" />
|
<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>
|
<div className="mt-2 text-2xl">Senate</div>
|
||||||
<StateElectionMap markets={senateMidterms} />
|
<StateElectionMap
|
||||||
|
markets={senateMidterms}
|
||||||
|
contracts={senateContracts}
|
||||||
|
/>
|
||||||
<iframe
|
<iframe
|
||||||
src="https://manifold.markets/TomShlomi/will-the-gop-control-the-us-senate"
|
src="https://manifold.markets/TomShlomi/will-the-gop-control-the-us-senate"
|
||||||
title="Will the Democrats control the Senate after the Midterms?"
|
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
width={800}
|
className="mt-8 flex h-96 w-full"
|
||||||
height={400}
|
|
||||||
className="mt-8"
|
|
||||||
></iframe>
|
></iframe>
|
||||||
<Spacer h={8} />
|
<Spacer h={8} />
|
||||||
|
|
||||||
<div className="mt-8 text-2xl">Governors</div>
|
<div className="mt-8 text-2xl">Governors</div>
|
||||||
<StateElectionMap markets={governorMidterms} />
|
<StateElectionMap
|
||||||
|
markets={governorMidterms}
|
||||||
|
contracts={governorContracts}
|
||||||
|
/>
|
||||||
<iframe
|
<iframe
|
||||||
src="https://manifold.markets/ManifoldMarkets/democrats-go-down-at-least-one-gove"
|
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"
|
frameBorder="0"
|
||||||
width={800}
|
className="mt-8 flex h-96 w-full"
|
||||||
height={400}
|
|
||||||
className="mt-8"
|
|
||||||
></iframe>
|
></iframe>
|
||||||
<Spacer h={8} />
|
<Spacer h={8} />
|
||||||
|
|
||||||
<div className="mt-8 text-2xl">House</div>
|
<div className="mt-8 text-2xl">House</div>
|
||||||
<iframe
|
<iframe
|
||||||
src="https://manifold.markets/BoltonBailey/will-democrats-maintain-control-of"
|
src="https://manifold.markets/BoltonBailey/will-democrats-maintain-control-of"
|
||||||
title="Will the Democrats control the House after the Midterms?"
|
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
width={800}
|
className="mt-8 flex h-96 w-full"
|
||||||
height={400}
|
|
||||||
className="mt-8"
|
|
||||||
></iframe>
|
></iframe>
|
||||||
<Spacer h={8} />
|
<Spacer h={8} />
|
||||||
|
|
||||||
<div className="mt-8 text-2xl">Related markets</div>
|
<div className="mt-8 text-2xl">Related markets</div>
|
||||||
<iframe
|
<iframe
|
||||||
src="https://manifold.markets/BoltonBailey/balance-of-power-in-us-congress-aft"
|
src="https://manifold.markets/BoltonBailey/balance-of-power-in-us-congress-aft"
|
||||||
title="Balance of Power in US Congress after 2022 Midterms"
|
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
width={800}
|
className="mt-8 flex h-96 w-full"
|
||||||
height={400}
|
|
||||||
className="mt-8"
|
|
||||||
></iframe>
|
></iframe>
|
||||||
<iframe
|
<iframe
|
||||||
src="https://manifold.markets/SG/will-a-democrat-win-the-2024-us-pre"
|
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"
|
frameBorder="0"
|
||||||
width={800}
|
className="mt-8 flex h-96 w-full"
|
||||||
height={400}
|
|
||||||
className="mt-8"
|
|
||||||
></iframe>
|
></iframe>
|
||||||
<iframe
|
<iframe
|
||||||
src="https://manifold.markets/Ibozz91/will-the-2022-alaska-house-general"
|
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?"
|
title="Will the 2022 Alaska House General Nonspecial Election result in a Condorcet failure?"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
width={800}
|
className="mt-8 flex h-96 w-full"
|
||||||
height={400}
|
|
||||||
className="mt-8"
|
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|
||||||
<iframe
|
<iframe
|
||||||
src="https://manifold.markets/NathanpmYoung/how-many-supreme-court-justices-wil-1e597c3853ad"
|
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?"
|
title="Will the 2022 Alaska House General Nonspecial Election result in a Condorcet failure?"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
width={800}
|
className="mt-8 flex h-96 w-full"
|
||||||
height={400}
|
|
||||||
className="mt-8"
|
|
||||||
></iframe>
|
></iframe>
|
||||||
</Col>
|
</Col>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { formatMoney } from 'common/util/format'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
import {
|
import {
|
||||||
BETTING_STREAK_BONUS_AMOUNT,
|
BETTING_STREAK_BONUS_AMOUNT,
|
||||||
|
BETTING_STREAK_BONUS_MAX,
|
||||||
UNIQUE_BETTOR_BONUS_AMOUNT,
|
UNIQUE_BETTOR_BONUS_AMOUNT,
|
||||||
} from 'common/economy'
|
} from 'common/economy'
|
||||||
import { groupBy, sum, uniqBy } from 'lodash'
|
import { groupBy, sum, uniqBy } from 'lodash'
|
||||||
|
@ -440,7 +441,8 @@ function IncomeNotificationItem(props: {
|
||||||
} else if (sourceType === 'tip') {
|
} else if (sourceType === 'tip') {
|
||||||
reasonText = !simple ? `tipped you on` : `in tips on`
|
reasonText = !simple ? `tipped you on` : `in tips on`
|
||||||
} else if (sourceType === 'betting_streak_bonus') {
|
} 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 reasonText = 'for your'
|
||||||
} else if (sourceType === 'loan' && sourceText) {
|
} else if (sourceType === 'loan' && sourceText) {
|
||||||
reasonText = `of your invested predictions returned as a`
|
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 { useUser } from 'web/hooks/use-user'
|
||||||
import { usePost } from 'web/hooks/use-post'
|
import { usePost } from 'web/hooks/use-post'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
|
import { Subtitle } from 'web/components/subtitle'
|
||||||
|
|
||||||
export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||||
const { slugs } = props.params
|
const { slugs } = props.params
|
||||||
|
@ -75,7 +76,11 @@ export default function PostPage(props: {
|
||||||
url={'/post/' + post.slug}
|
url={'/post/' + post.slug}
|
||||||
/>
|
/>
|
||||||
<div className="mx-auto w-full max-w-3xl ">
|
<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>
|
<Row>
|
||||||
<Col className="flex-1 px-2">
|
<Col className="flex-1 px-2">
|
||||||
<div className={'inline-flex'}>
|
<div className={'inline-flex'}>
|
||||||
|
@ -202,11 +207,11 @@ export function RichEditPost(props: { post: Post }) {
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<Col>
|
||||||
<div className="relative">
|
<Content content={post.content} />
|
||||||
<div className="absolute top-0 right-0 z-10 space-x-2">
|
<Row className="place-content-end">
|
||||||
<Button
|
<Button
|
||||||
color="gray"
|
color="gray-white"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditing(true)
|
setEditing(true)
|
||||||
|
@ -214,13 +219,8 @@ export function RichEditPost(props: { post: Post }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PencilIcon className="inline h-4 w-4" />
|
<PencilIcon className="inline h-4 w-4" />
|
||||||
Edit
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Row>
|
||||||
|
</Col>
|
||||||
<Content content={post.content} />
|
|
||||||
<Spacer h={2} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,7 +215,6 @@ export default function ProfilePage(props: {
|
||||||
}}
|
}}
|
||||||
submitBtn={{
|
submitBtn={{
|
||||||
label: 'Update key',
|
label: 'Update key',
|
||||||
className: 'btn-primary',
|
|
||||||
}}
|
}}
|
||||||
onSubmitWithSuccess={async () => {
|
onSubmitWithSuccess={async () => {
|
||||||
updateApiKey()
|
updateApiKey()
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getServerSideSitemap, ISitemapField } from 'next-sitemap'
|
||||||
import { listAllContracts } from 'web/lib/firebase/contracts'
|
import { listAllContracts } from 'web/lib/firebase/contracts'
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
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))
|
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}`,
|
loc: `https://manifold.markets/${market.creatorUsername}/${market.slug}`,
|
||||||
changefreq: market.volume24Hours > 10 ? 'hourly' : 'daily',
|
changefreq: market.volume24Hours > 10 ? 'hourly' : 'daily',
|
||||||
priority: score(market.popularityScore ?? 0),
|
priority: score(market.popularityScore ?? 0),
|
||||||
lastmod: market.lastUpdatedTime,
|
lastmod: new Date(market.lastUpdatedTime ?? 0).toISOString(),
|
||||||
})) as ISitemapField[]
|
})) as ISitemapField[]
|
||||||
|
|
||||||
return await getServerSideSitemap(ctx, fields)
|
return await getServerSideSitemap(ctx, fields)
|
||||||
|
|
|
@ -103,7 +103,7 @@ export function CustomAnalytics(props: Stats) {
|
||||||
title: 'Daily (7d avg)',
|
title: 'Daily (7d avg)',
|
||||||
content: (
|
content: (
|
||||||
<DailyChart
|
<DailyChart
|
||||||
dailyValues={dailyActiveUsersWeeklyAvg}
|
dailyValues={dailyActiveUsersWeeklyAvg.map(Math.round)}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { SiteLink } from 'web/components/site-link'
|
||||||
import { Carousel } from 'web/components/carousel'
|
import { Carousel } from 'web/components/carousel'
|
||||||
import { usePagination } from 'web/hooks/use-pagination'
|
import { usePagination } from 'web/hooks/use-pagination'
|
||||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||||
|
import { Title } from 'web/components/title'
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
|
@ -48,7 +49,7 @@ const Salem = {
|
||||||
title: 'CSPI/Salem Forecasting Tournament',
|
title: 'CSPI/Salem Forecasting Tournament',
|
||||||
blurb: 'Top 5 traders qualify for a UT Austin research fellowship.',
|
blurb: 'Top 5 traders qualify for a UT Austin research fellowship.',
|
||||||
url: 'https://salemcenter.manifold.markets/',
|
url: 'https://salemcenter.manifold.markets/',
|
||||||
award: '$25,000',
|
award: 'US$25,000',
|
||||||
endTime: toDate('Jul 31, 2023'),
|
endTime: toDate('Jul 31, 2023'),
|
||||||
contractIds: [],
|
contractIds: [],
|
||||||
images: [
|
images: [
|
||||||
|
@ -76,35 +77,36 @@ const Salem = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tourneys: Tourney[] = [
|
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',
|
title: 'Clearer Thinking Regrant Project',
|
||||||
blurb: 'Which projects will Clearer Thinking give a grant to?',
|
blurb: 'Which projects will Clearer Thinking give a grant to?',
|
||||||
award: '$13,000',
|
award: 'US$13,000',
|
||||||
endTime: toDate('Sep 30, 2022'),
|
endTime: toDate('Sep 30, 2022'),
|
||||||
groupId: 'fhksfIgqyWf7OxsV9nkM',
|
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',
|
title: 'Cause Exploration Prizes',
|
||||||
blurb: 'How many points will each NFL player score this season?',
|
blurb:
|
||||||
award: '$2,500',
|
'Which new charity ideas will Open Philanthropy find most promising?',
|
||||||
endTime: toDate('Jan 6, 2023'),
|
award: 'M$100k',
|
||||||
groupId: 'SxGRqXRpV3RAQKudbcNb',
|
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
|
// Tournaments without awards get featured below
|
||||||
|
@ -158,16 +160,31 @@ export async function getStaticProps() {
|
||||||
export default function TournamentPage(props: { sections: SectionInfo[] }) {
|
export default function TournamentPage(props: { sections: SectionInfo[] }) {
|
||||||
const { sections } = props
|
const { sections } = props
|
||||||
|
|
||||||
|
const description = `Win real prizes (including cash!) by participating in forecasting
|
||||||
|
tournaments on current events, sports, science, and more.`
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<SEO
|
<SEO title="Tournaments" description={description} />
|
||||||
title="Tournaments"
|
|
||||||
description="Win money by predicting in forecasting tournaments on current events, sports, science, and more"
|
|
||||||
/>
|
|
||||||
<Col className="m-4 gap-10 sm:mx-10 sm:gap-24 xl:w-[125%]">
|
<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(
|
{sections.map(
|
||||||
({ tourney, slug, numPeople }) =>
|
({ tourney, slug, numPeople }) =>
|
||||||
tourney.award && (
|
tourney.award &&
|
||||||
|
(tourney.endTime ?? 0) > Date.now() && (
|
||||||
<div key={slug}>
|
<div key={slug}>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
url={groupPath(slug, 'about')}
|
url={groupPath(slug, 'about')}
|
||||||
|
@ -181,16 +198,39 @@ export default function TournamentPage(props: { sections: SectionInfo[] }) {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<div>
|
|
||||||
<SectionHeader
|
{/* Title break */}
|
||||||
url={Salem.url}
|
<div className="relative">
|
||||||
title={Salem.title}
|
<div
|
||||||
award={Salem.award}
|
className="absolute inset-0 flex items-center"
|
||||||
endTime={Salem.endTime}
|
aria-hidden="true"
|
||||||
/>
|
>
|
||||||
<span className="text-gray-500">{Salem.blurb}</span>
|
<div className="w-full border-t border-gray-300" />
|
||||||
<ImageCarousel url={Salem.url} images={Salem.images} />
|
|
||||||
</div>
|
</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 */}
|
{/* Title break */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
import { Editor } from '@tiptap/core'
|
import { Editor } from '@tiptap/core'
|
||||||
|
import { sum } from 'lodash'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { PostComment } from 'common/comment'
|
import { PostComment } from 'common/comment'
|
||||||
import { Post } from 'common/post'
|
import { Post } from 'common/post'
|
||||||
|
@ -109,6 +110,7 @@ export function PostComment(props: {
|
||||||
const { text, content, userUsername, userName, userAvatarUrl, createdTime } =
|
const { text, content, userUsername, userName, userAvatarUrl, createdTime } =
|
||||||
comment
|
comment
|
||||||
|
|
||||||
|
const me = useUser()
|
||||||
const [highlighted, setHighlighted] = useState(false)
|
const [highlighted, setHighlighted] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -162,7 +164,11 @@ export function PostComment(props: {
|
||||||
Reply
|
Reply
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Tipper comment={comment} tips={tips ?? {}} />
|
<Tipper
|
||||||
|
comment={comment}
|
||||||
|
myTip={me ? tips?.[me.id] ?? 0 : 0}
|
||||||
|
totalTip={sum(Object.values(tips ?? {}))}
|
||||||
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<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</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/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/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/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/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/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/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>
|
</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: {
|
colors: {
|
||||||
'red-25': '#FDF7F6',
|
'red-25': '#FDF7F6',
|
||||||
'greyscale-1': '#FBFBFF',
|
'greyscale-1': '#FBFBFF',
|
||||||
|
'greyscale-1.5': '#F4F4FB',
|
||||||
'greyscale-2': '#E7E7F4',
|
'greyscale-2': '#E7E7F4',
|
||||||
'greyscale-3': '#D8D8EB',
|
'greyscale-3': '#D8D8EB',
|
||||||
'greyscale-4': '#B1B1C7',
|
'greyscale-4': '#B1B1C7',
|
||||||
|
|
174
yarn.lock
174
yarn.lock
|
@ -1310,7 +1310,14 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
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"
|
version "7.18.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
|
||||||
integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==
|
integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==
|
||||||
|
@ -2351,6 +2358,19 @@
|
||||||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.6.1.tgz#d822792e589aac005462491dd62f86095e0c3bef"
|
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.6.1.tgz#d822792e589aac005462491dd62f86095e0c3bef"
|
||||||
integrity sha512-gMd6uIs1U4Oz718Z5gFoV0o/vD43/4zvbyiJN9Dt7PK9Ubxn+TmJwTmYwyNJc5KxxU1t0CmgTNgwZX9+4NjCnQ==
|
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":
|
"@heroicons/react@1.0.6":
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.6.tgz#35dd26987228b39ef2316db3b1245c42eb19e324"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/hogan.js/-/hogan.js-3.0.1.tgz#64c54407b30da359763e14877f5702b8ae85d61c"
|
||||||
integrity sha512-D03i/2OY7kGyMq9wdQ7oD8roE49z/ZCZThe/nbahtvuqCNZY9T2MfedOWyeBdbEpY2W8Gnh/dyJLdFtUCOkYbg==
|
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"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||||
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
||||||
|
@ -3567,30 +3587,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||||
|
|
||||||
"@types/react-beautiful-dnd@13.1.2":
|
"@types/react-dom@18.0.6":
|
||||||
version "13.1.2"
|
version "18.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz#510405abb09f493afdfd898bf83995dc6385c130"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.6.tgz#36652900024842b74607a17786b6662dd1e103a1"
|
||||||
integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==
|
integrity sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@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@*":
|
"@types/react-router-config@*":
|
||||||
version "5.0.6"
|
version "5.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.6.tgz#87c5c57e72d241db900d9734512c50ccec062451"
|
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/history" "^4.7.11"
|
||||||
"@types/react" "*"
|
"@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"
|
version "17.0.43"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55"
|
||||||
integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==
|
integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==
|
||||||
|
@ -3626,6 +3629,15 @@
|
||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
csstype "^3.0.2"
|
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":
|
"@types/retry@0.12.0":
|
||||||
version "0.12.0"
|
version "0.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
||||||
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
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":
|
"@types/ws@^8.5.1":
|
||||||
version "8.5.3"
|
version "8.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
|
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"
|
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
|
||||||
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
|
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
|
||||||
|
|
||||||
css-box-model@^1.2.0:
|
css-box-model@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
|
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
|
||||||
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
|
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
|
||||||
|
@ -8683,10 +8700,10 @@ memfs@^3.1.2, memfs@^3.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
fs-monkey "1.0.3"
|
fs-monkey "1.0.3"
|
||||||
|
|
||||||
memoize-one@^5.1.1:
|
memoize-one@^6.0.0:
|
||||||
version "5.2.1"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
|
||||||
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
|
||||||
|
|
||||||
merge-descriptors@1.0.1:
|
merge-descriptors@1.0.1:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
|
||||||
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
|
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
|
||||||
|
|
||||||
raf-schd@^4.0.2:
|
raf-schd@^4.0.3:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
|
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
|
||||||
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
|
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
|
||||||
|
@ -10465,19 +10482,6 @@ react-base16-styling@^0.6.0:
|
||||||
lodash.flow "^3.3.0"
|
lodash.flow "^3.3.0"
|
||||||
pure-color "^1.2.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:
|
react-confetti@6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.0.1.tgz#d4f57b5a021dd908a6243b8f63b6009b00818d10"
|
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"
|
strip-ansi "^6.0.1"
|
||||||
text-table "^0.2.0"
|
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"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||||
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
|
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"
|
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
|
||||||
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
|
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
|
||||||
|
|
||||||
react-expanding-textarea@2.3.5:
|
react-expanding-textarea@2.3.6:
|
||||||
version "2.3.5"
|
version "2.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/react-expanding-textarea/-/react-expanding-textarea-2.3.5.tgz#310c28ab242c724e042589ac3ea400dd68ad1488"
|
resolved "https://registry.yarnpkg.com/react-expanding-textarea/-/react-expanding-textarea-2.3.6.tgz#daa50e5110dd71ca79e1df8e056b0b2eb0e8a84a"
|
||||||
integrity sha512-mPdtg3CxSgZFcsRLf80jueBWy1Zlh9AKy76S7dGXUKinvo4EypMavZa2iC0hEnLxY0tcwWR+n/K8B21TkttpUw==
|
integrity sha512-LjkyZv1LilMlt+6/yYn9F1FlcK8iQU96myeq6PwU/a7IaQMkSwndSB1SAhMKgxSrUR71VbqsqnAHQ741WbAU/Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-shallow-equal "^1.0.0"
|
fast-shallow-equal "^1.0.0"
|
||||||
react-with-forwarded-ref "^0.3.3"
|
react-with-forwarded-ref "^0.3.5"
|
||||||
tslib "^2.0.3"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
react-fast-compare@^3.2.0:
|
react-fast-compare@^3.2.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
react-is@^17.0.2:
|
react-is@^18.0.0:
|
||||||
version "17.0.2"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||||
|
|
||||||
react-json-view@^1.21.3:
|
react-json-view@^1.21.3:
|
||||||
version "1.21.3"
|
version "1.21.3"
|
||||||
|
@ -10626,17 +10638,17 @@ react-query@3.39.0:
|
||||||
broadcast-channel "^3.4.1"
|
broadcast-channel "^3.4.1"
|
||||||
match-sorter "^6.0.2"
|
match-sorter "^6.0.2"
|
||||||
|
|
||||||
react-redux@^7.2.0:
|
react-redux@^8.0.2:
|
||||||
version "7.2.8"
|
version "8.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de"
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.4.tgz#80c31dffa8af9526967c4267022ae1525ff0e36a"
|
||||||
integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==
|
integrity sha512-yMfQ7mX6bWuicz2fids6cR1YT59VTuT8MKyyE310wJQlINKENCeT1UcPdEiX6znI5tF8zXyJ/VYvDgeGuaaNwQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.15.4"
|
"@babel/runtime" "^7.12.1"
|
||||||
"@types/react-redux" "^7.1.20"
|
"@types/hoist-non-react-statics" "^3.3.1"
|
||||||
|
"@types/use-sync-external-store" "^0.0.3"
|
||||||
hoist-non-react-statics "^3.3.2"
|
hoist-non-react-statics "^3.3.2"
|
||||||
loose-envify "^1.4.0"
|
react-is "^18.0.0"
|
||||||
prop-types "^15.7.2"
|
use-sync-external-store "^1.0.0"
|
||||||
react-is "^17.0.2"
|
|
||||||
|
|
||||||
react-router-config@^5.1.1:
|
react-router-config@^5.1.1:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
|
@ -10690,14 +10702,21 @@ react-twitter-embed@4.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
scriptjs "^2.5.9"
|
scriptjs "^2.5.9"
|
||||||
|
|
||||||
react-with-forwarded-ref@^0.3.3:
|
react-with-forwarded-ref@^0.3.5:
|
||||||
version "0.3.4"
|
version "0.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/react-with-forwarded-ref/-/react-with-forwarded-ref-0.3.4.tgz#b1e884ea081ec3c5dd578f37889159797454c0a5"
|
resolved "https://registry.yarnpkg.com/react-with-forwarded-ref/-/react-with-forwarded-ref-0.3.5.tgz#7d0bae2a9996fc91493f40ab179b8c54d29cfab9"
|
||||||
integrity sha512-SRq/uTdTh+02JDwYzEEhY2aNNWl/CP2EKP2nQtXzhJw06w6PgYnJt2ObrebvFJu6+wGzX3vDHU3H/ux9hxyZUQ==
|
integrity sha512-BJK4q0Nvqg4AFwc+LV+PZZb2nxS1ZqQlS9hY14TdIyg7Lapzirk/V/TtbYjPFSsm/fGm0wC4tsgI1IDhxSVVSQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.0.3"
|
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"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||||
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
|
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
|
||||||
|
@ -10789,7 +10808,7 @@ recursive-readdir@^2.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimatch "3.0.4"
|
minimatch "3.0.4"
|
||||||
|
|
||||||
redux@^4.0.0, redux@^4.0.4:
|
redux@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
|
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
|
||||||
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
|
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
|
||||||
|
@ -11169,6 +11188,13 @@ scheduler@^0.20.2:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
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:
|
schema-utils@2.7.0:
|
||||||
version "2.7.0"
|
version "2.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
|
||||||
|
@ -12394,12 +12420,12 @@ use-latest@^1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
use-isomorphic-layout-effect "^1.1.1"
|
use-isomorphic-layout-effect "^1.1.1"
|
||||||
|
|
||||||
use-memo-one@^1.1.1:
|
use-memo-one@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
|
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99"
|
||||||
integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
|
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"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
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==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
|
Loading…
Reference in New Issue
Block a user