Merge branch 'main' into inga/profile

This commit is contained in:
ingawei 2022-09-26 16:51:16 -05:00 committed by GitHub
commit e7a552b8ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 4157 additions and 1277 deletions

View File

@ -21,6 +21,25 @@ const computeInvestmentValue = (
})
}
export const computeInvestmentValueCustomProb = (
bets: Bet[],
contract: Contract,
p: number
) => {
return sumBy(bets, (bet) => {
if (!contract || contract.isResolved) return 0
if (bet.sale || bet.isSold) return 0
const { outcome, shares } = bet
const betP = outcome === 'YES' ? p : 1 - p
const payout = betP * shares
const value = payout - (bet.loanAmount ?? 0)
if (isNaN(value)) return 0
return value
})
}
const computeTotalPool = (userContracts: Contract[], startTime = 0) => {
const periodFilteredContracts = userContracts.filter(
(contract) => contract.createdTime >= startTime

View File

@ -57,6 +57,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
uniqueBettorIds?: string[]
uniqueBettorCount?: number
popularityScore?: number
dailyScore?: number
followerCount?: number
featuredOnHomeRank?: number
likedByUserIds?: string[]

View File

@ -10,6 +10,7 @@ export type Group = {
totalContracts: number
totalMembers: number
aboutPostId?: string
postIds: string[]
chatDisabled?: boolean
mostRecentContractAddedTime?: number
cachedLeaderboard?: {

View File

@ -168,7 +168,7 @@ export const getPayoutsMultiOutcome = (
const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal
const profit = winnings - amount
const payout = amount + (1 - DPM_FEES) * Math.max(0, profit)
const payout = amount + (1 - DPM_FEES) * profit
return { userId, profit, payout }
})

View File

@ -57,6 +57,7 @@ export type PrivateUser = {
email?: string
weeklyTrendingEmailSent?: boolean
weeklyPortfolioUpdateEmailSent?: boolean
manaBonusEmailSent?: boolean
initialDeviceToken?: string
initialIpAddress?: string

View File

@ -1,3 +1,6 @@
export const MINUTE_MS = 60 * 1000
export const HOUR_MS = 60 * MINUTE_MS
export const DAY_MS = 24 * HOUR_MS
export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))

View File

@ -582,6 +582,13 @@ $ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: applicat
"initialProb":25}'
```
### `POST /v0/market/[marketId]/close`
Closes a market on behalf of the authorized user.
- `closeTime`: Optional. Milliseconds since the epoch to close the market at. If not provided, the market will be closed immediately. Cannot provide close time in past.
### `POST /v0/market/[marketId]/resolve`
Resolves a market on behalf of the authorized user.

View File

@ -8,9 +8,8 @@ A list of community-created projects built on, or related to, Manifold Markets.
## Sites using Manifold
- [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government
- [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold
- [WagerWith.me](https://www.wagerwith.me/) — Bet with your friends, with full Manifold integration to bet with M$.
- [Alignment Markets](https://alignmentmarkets.com/) - Bet on the progress of benchmarks in ML safety!
## API / Dev
@ -28,6 +27,7 @@ A list of community-created projects built on, or related to, Manifold Markets.
- [mana](https://github.com/AnnikaCodes/mana) - A Discord bot for Manifold by [@arae](https://manifold.markets/arae)
## Writeups
- [Information Markets, Decision Markets, Attention Markets, Action Markets](https://astralcodexten.substack.com/p/information-markets-decision-markets) by Scott Alexander
- [Mismatched Monetary Motivation in Manifold Markets](https://kevin.zielnicki.com/2022/02/17/manifold/) by Kevin Zielnicki
- [Introducing the Salem/CSPI Forecasting Tournament](https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting) by Richard Hanania
@ -38,3 +38,10 @@ A list of community-created projects built on, or related to, Manifold Markets.
- Folded origami and doodles by [@hamnox](https://manifold.markets/hamnox) ![](https://i.imgur.com/nVGY4pL.png)
- Laser-cut Foldy by [@wasabipesto](https://manifold.markets/wasabipesto) ![](https://i.imgur.com/g9S6v3P.jpg)
## Alumni
_These projects are no longer active, but were really really cool!_
- [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold
- [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government

View File

@ -4,11 +4,7 @@
### Do I have to pay real money in order to participate?
Nope! Each account starts with a free M$1000. If you invest it wisely, you can increase your total without ever needing to put any real money into the site.
### What is the name for the currency Manifold uses, represented by M$?
Manifold Dollars, or mana for short.
Nope! Each account starts with a free 1000 mana (or M$1000 for short). If you invest it wisely, you can increase your total without ever needing to put any real money into the site.
### Can M$ be sold for real money?

View File

@ -40,7 +40,6 @@
"mailgun-js": "0.22.0",
"module-alias": "2.2.2",
"node-fetch": "2",
"react-masonry-css": "1.0.16",
"stripe": "8.194.0",
"zod": "3.17.2"
},
@ -48,7 +47,8 @@
"@types/mailgun-js": "0.22.12",
"@types/module-alias": "2.0.1",
"@types/node-fetch": "2.6.2",
"firebase-functions-test": "0.3.3"
"firebase-functions-test": "0.3.3",
"puppeteer": "18.0.5"
},
"private": true
}

View File

@ -0,0 +1,58 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { Contract } from '../../common/contract'
import { getUser } from './utils'
import { isAdmin, isManifoldId } from '../../common/envs/constants'
import { APIError, newEndpoint, validate } from './api'
const bodySchema = z.object({
contractId: z.string(),
closeTime: z.number().int().nonnegative().optional(),
})
export const closemarket = newEndpoint({}, async (req, auth) => {
const { contractId, closeTime } = validate(bodySchema, req.body)
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await contractDoc.get()
if (!contractSnap.exists)
throw new APIError(404, 'No contract exists with the provided ID')
const contract = contractSnap.data() as Contract
const { creatorId } = contract
const firebaseUser = await admin.auth().getUser(auth.uid)
if (
creatorId !== auth.uid &&
!isManifoldId(auth.uid) &&
!isAdmin(firebaseUser.email)
)
throw new APIError(403, 'User is not creator of contract')
const now = Date.now()
if (!closeTime && contract.closeTime && contract.closeTime < now)
throw new APIError(400, 'Contract already closed')
if (closeTime && closeTime < now)
throw new APIError(
400,
'Close time must be in the future. ' +
'Alternatively, do not provide a close time to close immediately.'
)
const creator = await getUser(creatorId)
if (!creator) throw new APIError(500, 'Creator not found')
const updatedContract = {
...contract,
closeTime: closeTime ? closeTime : now,
}
await contractDoc.update(updatedContract)
console.log('contract ', contractId, 'closed')
return updatedContract
})
const firestore = admin.firestore()

View File

@ -7,6 +7,7 @@ import { getNewMultiBetInfo } from '../../common/new-bet'
import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
import { getValues } from './utils'
import { APIError, newEndpoint, validate } from './api'
import { addUserToContractFollowers } from './follow-market'
const bodySchema = z.object({
contractId: z.string().max(MAX_ANSWER_LENGTH),
@ -96,6 +97,8 @@ export const createanswer = newEndpoint(opts, async (req, auth) => {
return answer
})
await addUserToContractFollowers(contractId, auth.uid)
return answer
})

View File

@ -61,6 +61,7 @@ export const creategroup = newEndpoint({}, async (req, auth) => {
anyoneCanJoin,
totalContracts: 0,
totalMembers: memberIds.length,
postIds: [],
}
await groupRef.create(group)

View File

@ -34,11 +34,12 @@ const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
const postSchema = z.object({
title: z.string().min(1).max(MAX_POST_TITLE_LENGTH),
content: contentSchema,
groupId: z.string().optional(),
})
export const createpost = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore()
const { title, content } = validate(postSchema, req.body)
const { title, content, groupId } = validate(postSchema, req.body)
const creator = await getUser(auth.uid)
if (!creator)
@ -60,6 +61,18 @@ export const createpost = newEndpoint({}, async (req, auth) => {
}
await postRef.create(post)
if (groupId) {
const groupRef = firestore.collection('groups').doc(groupId)
const group = await groupRef.get()
if (group.exists) {
const groupData = group.data()
if (groupData) {
const postIds = groupData.postIds ?? []
postIds.push(postRef.id)
await groupRef.update({ postIds })
}
}
}
return { status: 'success', post }
})

View File

@ -0,0 +1,411 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Weekly Portfolio Update on Manifold</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family:"Readex Pro", Helvetica, sans-serif;
}
table { margin: 0 auto; }
table,
td {
border-collapse: collapse;
mso-table-lspace: 0;
mso-table-rspace: 0;
}
th {color:#000000; font-size:17px;}
th, td {padding: 10px; }
td{ font-size: 17px}
th, td { vertical-align: center; text-align: left }
a { vertical-align: center; text-align: left}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
p.change{
margin: 0; vertical-align: middle;font-size:16px;display: inline; padding: 2px; border-radius: 5px; width: 20px; text-align: right;
}
p.prob{
font-size: 22px;display: inline; vertical-align: middle; font-weight: bold; width: 50px;
}
a.question{
font-size: 18px;display: inline; vertical-align: middle;
}
td.question{
vertical-align: middle; padding-bottom: 15px; text-align: left;
}
td.probs{
text-align: right; padding-left: 10px; min-width: 115px
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix {
width: 100% !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width: 480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing: normal; background-color: #f4f4f4">
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center"
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:550px;">
<a href="https://manifold.markets" target="_blank">
<img alt="banner logo" height="auto"
src="https://manifold.markets/logo-banner.png"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
title="" width="550">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 20px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align: top; margin-bottom: 30px" width="100%">
<tbody>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
</span>Hi {{name}},</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 0px;"
data-testid="4XoHRGw1Y">
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
We ran the numbers and here's how you did this past week!
</span>
</p>
</div>
</td>
</tr>
<!--/ show 5 columns with headers titled: Investment value, 7-day change, current balance, tips received, and markets made/-->
<tr>
<tr>
<th style='font-size: 22px; text-align: center'>
Profit
</th>
</tr>
<tr>
<td style='padding-bottom: 30px; text-align: center'>
<p class='change' style='font-size: 24px; padding:4px; {{profit_style}}'>
{{profit}}
</p>
</td>
</tr>
<td align="center"
style="font-size:0px;padding:10px 20px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px; ">
<tbody>
<tr>
<th style='width: 170px'>
🔥 Prediction streak
</th>
<td>
{{prediction_streak}}
</td>
</tr>
<tr>
<th>
💸 Tips received
</th>
<td>
{{tips_received}}
</td>
</tr>
<tr>
<th>
📈 Markets traded
</th>
<td>
{{markets_traded}}
</td>
</tr>
<tr>
<th>
❓ Markets created
</th>
<td>
{{markets_created}}
</td>
</tr>
<tr>
<th style='width: 55px'>
🥳 Traders attracted
</th>
<td>
{{unique_bettors}}
</td>
</tr>
</tbody>
</table>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 0 0 20px 0;
text-align: center;
">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 20px 0px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
width="100%">
<tbody>
<tr>
<td style="vertical-align: top; padding: 0">
<table border="0" cellpadding="0" cellspacing="0"
role="presentation" width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
">
<div style="
font-family: Ubuntu, Helvetica, Arial,
sans-serif;
font-size: 11px;
line-height: 22px;
text-align: center;
color: #000000;
">
<p style="margin: 10px 0">
This e-mail has been sent to
{{name}},
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,510 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Weekly Portfolio Update on Manifold</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family:"Readex Pro", Helvetica, sans-serif;
}
table { margin: 0 auto; }
table,
td {
border-collapse: collapse;
mso-table-lspace: 0;
mso-table-rspace: 0;
}
th {color:#000000; font-size:17px;}
th, td {padding: 10px; }
td{ font-size: 17px}
th, td { vertical-align: center; text-align: left }
a { vertical-align: center; text-align: left}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
p.change{
margin: 0; vertical-align: middle;font-size:16px;display: inline; padding: 2px; border-radius: 5px; width: 20px; text-align: right;
}
p.prob{
font-size: 22px;display: inline; vertical-align: middle; font-weight: bold; width: 50px;
}
a.question{
font-size: 18px;display: inline; vertical-align: middle;
}
td.question{
vertical-align: middle; padding-bottom: 15px; text-align: left;
}
td.probs{
text-align: right; padding-left: 10px; min-width: 115px
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix {
width: 100% !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width: 480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing: normal; background-color: #f4f4f4">
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center"
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:550px;">
<a href="https://manifold.markets" target="_blank">
<img alt="banner logo" height="auto"
src="https://manifold.markets/logo-banner.png"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
title="" width="550">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 20px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align: top" width="100%">
<tbody>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
</span>Hi {{name}},</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 0px;"
data-testid="4XoHRGw1Y">
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
We ran the numbers and here's how you did this past week!
</span>
</p>
</div>
</td>
</tr>
<!--/ show 5 columns with headers titled: Investment value, 7-day change, current balance, tips received, and markets made/-->
<tr>
<tr>
<th style='font-size: 22px; text-align: center'>
Profit
</th>
</tr>
<tr>
<td style='padding-bottom: 30px; text-align: center'>
<p class='change' style='font-size: 24px; padding:4px; {{profit_style}}'>
{{profit}}
</p>
</td>
</tr>
<td align="center"
style="font-size:0px;padding:10px 20px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px; ">
<tbody>
<tr>
<th style='width: 170px'>
🔥 Prediction streak
</th>
<td>
{{prediction_streak}}
</td>
</tr>
<tr>
<th>
💸 Tips received
</th>
<td>
{{tips_received}}
</td>
</tr>
<tr>
<th>
📈 Markets traded
</th>
<td>
{{markets_traded}}
</td>
</tr>
<tr>
<th>
❓ Markets created
</th>
<td>
{{markets_created}}
</td>
</tr>
<tr>
<th style='width: 55px'>
🥳 Traders attracted
</th>
<td>
{{unique_bettors}}
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:20px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 20px; margin-bottom: 20px;"
data-testid="4XoHRGw1Y">
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
And here's some of the biggest changes in your portfolio:
</span>
</p>
</div>
</td>
<tr>
<td
style="font-size:0; padding-left:10px;padding-top:10px;padding-bottom:0;word-break:break-word;">
<table role="presentation">
<tbody>
<tr>
<td class='question'>
<a class='question' href='{{question1Url}}'>
{{question1Title}}
<!-- Will the US economy recover from the pandemic?-->
</a>
</td>
<td class='probs'>
<p class='prob'>
{{question1Prob}}
<!-- 9.9%-->
<p class='change' style='{{question1ChangeStyle}}'>
{{question1Change}}
<!-- +17%-->
</p>
</p>
</td>
</tr><tr>
<td class='question'>
<a class='question' href='{{question2Url}}'>
{{question2Title}}
<!-- Will the US economy recover from the pandemic? blah blah blah-->
</a>
</td>
<td class='probs'>
<p class='prob'>
{{question2Prob}}
<!-- 99.9%-->
<p class='change' style='{{question2ChangeStyle}}'>
{{question2Change}}
<!-- +7%-->
</p>
</p>
</td>
</tr><tr>
<!-- <td style="{{investment_value_style}}">-->
<td class='question'>
<a class='question' href='{{question3Url}}'>
{{question3Title}}
<!-- Will the US economy recover from the pandemic?-->
</a>
</td>
<td class='probs'>
<p class='prob'>
{{question3Prob}}
<!-- 99.9%-->
<p class='change' style='{{question3ChangeStyle}}'>
{{question3Change}}
<!-- +17%-->
</p>
</p>
</td>
</tr><tr>
<!-- <td style="{{investment_value_style}}">-->
<td class='question'>
<a class='question' href='{{question4Url}}'>
{{question4Title}}
<!-- Will the US economy recover from the pandemic?-->
</a>
</td>
<td class='probs'>
<p class='prob'>
{{question4Prob}}
<!-- 99.9%-->
<p class='change' style='{{question4ChangeStyle}}'>
{{question4Change}}
<!-- +17%-->
</p>
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 0 0 20px 0;
text-align: center;
">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 20px 0px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
width="100%">
<tbody>
<tr>
<td style="vertical-align: top; padding: 0">
<table border="0" cellpadding="0" cellspacing="0"
role="presentation" width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
">
<div style="
font-family: Ubuntu, Helvetica, Arial,
sans-serif;
font-size: 11px;
line-height: 22px;
text-align: center;
color: #000000;
">
<p style="margin: 10px 0">
This e-mail has been sent to
{{name}},
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -12,14 +12,15 @@ import { getValueFromBucket } from '../../common/calculate-dpm'
import { formatNumericProbability } from '../../common/pseudo-numeric'
import { sendTemplateEmail, sendTextEmail } from './send-email'
import { getUser } from './utils'
import { contractUrl, getUser } from './utils'
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
import { notification_reason_types } from '../../common/notification'
import { Dictionary } from 'lodash'
import { getNotificationDestinationsForUser } from '../../common/user-notification-preferences'
import {
getNotificationDestinationsForUser,
notification_preference,
} from '../../common/user-notification-preferences'
PerContractInvestmentsData,
OverallPerformanceData,
} from 'functions/src/weekly-portfolio-emails'
export const sendMarketResolutionEmail = async (
reason: notification_reason_types,
@ -152,9 +153,10 @@ export const sendWelcomeEmail = async (
const { name } = user
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings&section=${
'onboarding_flow' as notification_preference
}`
const { unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
'onboarding_flow'
)
return await sendTemplateEmail(
privateUser.email,
@ -220,9 +222,11 @@ export const sendOneWeekBonusEmail = async (
const { name } = user
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings&section=${
'onboarding_flow' as notification_preference
}`
const { unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
'onboarding_flow'
)
return await sendTemplateEmail(
privateUser.email,
'Manifold Markets one week anniversary gift',
@ -252,10 +256,10 @@ export const sendCreatorGuideEmail = async (
const { name } = user
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings&section=${
'onboarding_flow' as notification_preference
}`
const { unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
'onboarding_flow'
)
return await sendTemplateEmail(
privateUser.email,
'Create your own prediction market',
@ -286,10 +290,10 @@ export const sendThankYouEmail = async (
const { name } = user
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings&section=${
'thank_you_for_purchases' as notification_preference
}`
const { unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
'thank_you_for_purchases'
)
return await sendTemplateEmail(
privateUser.email,
@ -469,9 +473,10 @@ export const sendInterestingMarketsEmail = async (
)
return
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings&section=${
'trending_markets' as notification_preference
}`
const { unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
'trending_markets'
)
const { name } = user
const firstName = name.split(' ')[0]
@ -507,10 +512,6 @@ export const sendInterestingMarketsEmail = async (
)
}
function contractUrl(contract: Contract) {
return `https://manifold.markets/${contract.creatorUsername}/${contract.slug}`
}
function imageSourceUrl(contract: Contract) {
return buildCardUrl(getOpenGraphProps(contract))
}
@ -612,3 +613,47 @@ export const sendNewUniqueBettorsEmail = async (
}
)
}
export const sendWeeklyPortfolioUpdateEmail = async (
user: User,
privateUser: PrivateUser,
investments: PerContractInvestmentsData[],
overallPerformance: OverallPerformanceData
) => {
if (
!privateUser ||
!privateUser.email ||
!privateUser.notificationPreferences.profit_loss_updates.includes('email')
)
return
const { unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
'profit_loss_updates'
)
const { name } = user
const firstName = name.split(' ')[0]
const templateData: Record<string, string> = {
name: firstName,
unsubscribeUrl,
...overallPerformance,
}
investments.forEach((investment, i) => {
templateData[`question${i + 1}Title`] = investment.questionTitle
templateData[`question${i + 1}Url`] = investment.questionUrl
templateData[`question${i + 1}Prob`] = investment.questionProb
templateData[`question${i + 1}Change`] = investment.questionChange
templateData[`question${i + 1}ChangeStyle`] = investment.questionChangeStyle
})
await sendTemplateEmail(
privateUser.email,
// 'iansphilips@gmail.com',
`Here's your weekly portfolio update!`,
investments.length === 0
? 'portfolio-update-no-movers'
: 'portfolio-update',
templateData
)
}

View File

@ -30,6 +30,7 @@ export * from './reset-betting-streaks'
export * from './reset-weekly-emails-flag'
export * from './on-update-contract-follow'
export * from './on-update-like'
export * from './weekly-portfolio-emails'
// v2
export * from './health'
@ -50,6 +51,7 @@ export * from './resolve-market'
export * from './unsubscribe'
export * from './stripe'
export * from './mana-bonus-email'
export * from './close-market'
import { health } from './health'
import { transact } from './transact'
@ -66,6 +68,7 @@ import { addliquidity } from './add-liquidity'
import { withdrawliquidity } from './withdraw-liquidity'
import { creategroup } from './create-group'
import { resolvemarket } from './resolve-market'
import { closemarket } from './close-market'
import { unsubscribe } from './unsubscribe'
import { stripewebhook, createcheckoutsession } from './stripe'
import { getcurrentuser } from './get-current-user'
@ -91,6 +94,7 @@ const addLiquidityFunction = toCloudFunction(addliquidity)
const withdrawLiquidityFunction = toCloudFunction(withdrawliquidity)
const createGroupFunction = toCloudFunction(creategroup)
const resolveMarketFunction = toCloudFunction(resolvemarket)
const closeMarketFunction = toCloudFunction(closemarket)
const unsubscribeFunction = toCloudFunction(unsubscribe)
const stripeWebhookFunction = toCloudFunction(stripewebhook)
const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
@ -115,11 +119,12 @@ export {
withdrawLiquidityFunction as withdrawliquidity,
createGroupFunction as creategroup,
resolveMarketFunction as resolvemarket,
closeMarketFunction as closemarket,
unsubscribeFunction as unsubscribe,
stripeWebhookFunction as stripewebhook,
createCheckoutSessionFunction as createcheckoutsession,
getCurrentUserFunction as getcurrentuser,
acceptChallenge as acceptchallenge,
createPostFunction as createpost,
saveTwitchCredentials as savetwitchcredentials
saveTwitchCredentials as savetwitchcredentials,
}

View File

@ -17,6 +17,7 @@ export const resetWeeklyEmailsFlag = functions
privateUsers.map(async (user) => {
return firestore.collection('private-users').doc(user.id).update({
weeklyTrendingEmailSent: false,
weeklyPortfolioEmailSent: false,
})
})
)

View File

@ -1,12 +1,15 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { Bet } from 'common/bet'
import { uniq } from 'lodash'
import { Contract } from 'common/contract'
import { Bet } from '../../common/bet'
import { Contract } from '../../common/contract'
import { log } from './utils'
import { removeUndefinedProps } from '../../common/util/object'
import { DAY_MS, HOUR_MS } from '../../common/util/time'
export const scoreContracts = functions.pubsub
.schedule('every 1 hours')
export const scoreContracts = functions
.runWith({ memory: '4GB', timeoutSeconds: 540 })
.pubsub.schedule('every 1 hours')
.onRun(async () => {
await scoreContractsInternal()
})
@ -14,11 +17,12 @@ const firestore = admin.firestore()
async function scoreContractsInternal() {
const now = Date.now()
const lastHour = now - 60 * 60 * 1000
const last3Days = now - 1000 * 60 * 60 * 24 * 3
const hourAgo = now - HOUR_MS
const dayAgo = now - DAY_MS
const threeDaysAgo = now - DAY_MS * 3
const activeContractsSnap = await firestore
.collection('contracts')
.where('lastUpdatedTime', '>', lastHour)
.where('lastUpdatedTime', '>', hourAgo)
.get()
const activeContracts = activeContractsSnap.docs.map(
(doc) => doc.data() as Contract
@ -39,16 +43,33 @@ async function scoreContractsInternal() {
for (const contract of contracts) {
const bets = await firestore
.collection(`contracts/${contract.id}/bets`)
.where('createdTime', '>', last3Days)
.where('createdTime', '>', threeDaysAgo)
.get()
const bettors = bets.docs
.map((doc) => doc.data() as Bet)
.map((bet) => bet.userId)
const score = uniq(bettors).length
if (contract.popularityScore !== score)
const popularityScore = uniq(bettors).length
const wasCreatedToday = contract.createdTime > dayAgo
let dailyScore: number | undefined
if (
contract.outcomeType === 'BINARY' &&
contract.mechanism === 'cpmm-1' &&
!wasCreatedToday
) {
const percentChange = Math.abs(contract.probChanges.day)
dailyScore = popularityScore * percentChange
}
if (
contract.popularityScore !== popularityScore ||
contract.dailyScore !== dailyScore
) {
await firestore
.collection('contracts')
.doc(contract.id)
.update({ popularityScore: score })
.update(removeUndefinedProps({ popularityScore, dailyScore }))
}
}
}

View File

@ -0,0 +1,115 @@
// Run with `npx ts-node src/scripts/contest/create-markets.ts`
import { data } from './criticism-and-red-teaming'
// Dev API key for Cause Exploration Prizes (@CEP)
// const API_KEY = '188f014c-0ba2-4c35-9e6d-88252e281dbf'
// DEV API key for Criticism and Red Teaming (@CARTBot)
const API_KEY = '6ff1f78a-32fe-43b2-b31b-9e3c78c5f18c'
type CEPSubmission = {
title: string
author?: string
link: string
}
// Use the API to create a new market for this Cause Exploration Prize submission
async function postMarket(submission: CEPSubmission) {
const { title, author } = submission
const response = await fetch('https://dev.manifold.markets/api/v0/market', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Key ${API_KEY}`,
},
body: JSON.stringify({
outcomeType: 'BINARY',
question: `"${title}" by ${author ?? 'anonymous'}`,
description: makeDescription(submission),
closeTime: Date.parse('2022-09-30').valueOf(),
initialProb: 10,
// Super secret options:
// groupId: 'y2hcaGybXT1UfobK3XTx', // [DEV] CEP Tournament
// groupId: 'cMcpBQ2p452jEcJD2SFw', // [PROD] Predict CEP
groupId: 'h3MhjYbSSG6HbxY8ZTwE', // [DEV] CART
// groupId: 'K86LmEmidMKdyCHdHNv4', // [PROD] CART
visibility: 'unlisted',
// TODO: Increase liquidity?
}),
})
const data = await response.json()
console.log('Created market:', data.slug)
}
async function postAll() {
for (const submission of data.slice(0, 3)) {
await postMarket(submission)
}
}
postAll()
/* Example curl request:
$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \
-H 'Authorization: Key {...}'
--data-raw '{"outcomeType":"BINARY", \
"question":"Is there life on Mars?", \
"description":"I'm not going to type some long ass example description.", \
"closeTime":1700000000000, \
"initialProb":25}'
*/
function makeDescription(submission: CEPSubmission) {
const { title, author, link } = submission
return {
content: [
{
content: [
{ text: `Will ${author ?? 'anonymous'}'s post "`, type: 'text' },
{
marks: [
{
attrs: {
target: '_blank',
href: link,
class:
'no-underline !text-indigo-700 z-10 break-words hover:underline hover:decoration-indigo-400 hover:decoration-2',
},
type: 'link',
},
],
type: 'text',
text: title,
},
{ text: '" win any prize in the ', type: 'text' },
{
text: 'EA Criticism and Red Teaming Contest',
type: 'text',
marks: [
{
attrs: {
target: '_blank',
class:
'no-underline !text-indigo-700 z-10 break-words hover:underline hover:decoration-indigo-400 hover:decoration-2',
href: 'https://forum.effectivealtruism.org/posts/8hvmvrgcxJJ2pYR4X/announcing-a-contest-ea-criticism-and-red-teaming',
},
type: 'link',
},
],
},
{ text: '?', type: 'text' },
],
type: 'paragraph',
},
{ type: 'paragraph' },
{
type: 'iframe',
attrs: {
allowfullscreen: true,
src: link,
frameborder: 0,
},
},
],
type: 'doc',
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
// Run with `npx ts-node src/scripts/contest/scrape-ea.ts`
import * as fs from 'fs'
import * as puppeteer from 'puppeteer'
export function scrapeEA(contestLink: string, fileName: string) {
;(async () => {
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
await page.goto(contestLink)
let loadMoreButton = await page.$('.LoadMore-root')
while (loadMoreButton) {
await loadMoreButton.click()
await page.waitForNetworkIdle()
loadMoreButton = await page.$('.LoadMore-root')
}
/* Run javascript inside the page */
const data = await page.evaluate(() => {
const list = []
const items = document.querySelectorAll('.PostsItem2-root')
for (const item of items) {
const link =
'https://forum.effectivealtruism.org' +
item?.querySelector('a')?.getAttribute('href')
// Replace '&amp;' with '&'
const clean = (str: string | undefined) => str?.replace(/&amp;/g, '&')
list.push({
title: clean(item?.querySelector('a>span>span')?.innerHTML),
author: item?.querySelector('a.UsersNameDisplay-userName')?.innerHTML,
link: link,
})
}
return list
})
fs.writeFileSync(
`./src/scripts/contest/${fileName}.ts`,
`export const data = ${JSON.stringify(data, null, 2)}`
)
console.log(data)
await browser.close()
})()
}
scrapeEA(
'https://forum.effectivealtruism.org/topics/criticism-and-red-teaming-contest',
'criticism-and-red-teaming'
)

View File

@ -41,6 +41,7 @@ const createGroup = async (
anyoneCanJoin: true,
totalContracts: contracts.length,
totalMembers: 1,
postIds: [],
}
await groupRef.create(group)
// create a GroupMemberDoc for the creator

View File

@ -28,6 +28,7 @@ import { stripewebhook, createcheckoutsession } from './stripe'
import { getcurrentuser } from './get-current-user'
import { createpost } from './create-post'
import { savetwitchcredentials } from './save-twitch-credentials'
import { testscheduledfunction } from './test-scheduled-function'
type Middleware = (req: Request, res: Response, next: NextFunction) => void
const app = express()
@ -69,6 +70,7 @@ addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
addJsonEndpointRoute('/savetwitchcredentials', savetwitchcredentials)
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
addEndpointRoute('/createpost', createpost)
addEndpointRoute('/testscheduledfunction', testscheduledfunction)
app.listen(PORT)
console.log(`Serving functions on port ${PORT}.`)

View File

@ -0,0 +1,17 @@
import { APIError, newEndpoint } from './api'
import { sendPortfolioUpdateEmailsToAllUsers } from './weekly-portfolio-emails'
import { isProd } from './utils'
// Function for testing scheduled functions locally
export const testscheduledfunction = newEndpoint(
{ method: 'GET', memory: '4GiB' },
async (_req) => {
if (isProd())
throw new APIError(400, 'This function is only available in dev mode')
// Replace your function here
await sendPortfolioUpdateEmailsToAllUsers()
return { success: true }
}
)

View File

@ -170,3 +170,7 @@ export const chargeUser = (
export const getContractPath = (contract: Contract) => {
return `/${contract.creatorUsername}/${contract.slug}`
}
export function contractUrl(contract: Contract) {
return `https://manifold.markets/${contract.creatorUsername}/${contract.slug}`
}

View File

@ -0,0 +1,280 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { Contract, CPMMContract } from '../../common/contract'
import {
getAllPrivateUsers,
getPrivateUser,
getUser,
getValue,
getValues,
isProd,
log,
} from './utils'
import { filterDefined } from '../../common/util/array'
import { DAY_MS } from '../../common/util/time'
import { partition, sortBy, sum, uniq } from 'lodash'
import { Bet } from '../../common/bet'
import { computeInvestmentValueCustomProb } from '../../common/calculate-metrics'
import { sendWeeklyPortfolioUpdateEmail } from './emails'
import { contractUrl } from './utils'
import { Txn } from '../../common/txn'
import { formatMoney } from '../../common/util/format'
// TODO: reset weeklyPortfolioUpdateEmailSent to false for all users at the start of each week
export const weeklyPortfolioUpdateEmails = functions
.runWith({ secrets: ['MAILGUN_KEY'], memory: '4GB' })
// every minute on Friday for an hour at 12pm PT (UTC -07:00)
.pubsub.schedule('* 19 * * 5')
.timeZone('Etc/UTC')
.onRun(async () => {
await sendPortfolioUpdateEmailsToAllUsers()
})
const firestore = admin.firestore()
export async function sendPortfolioUpdateEmailsToAllUsers() {
const privateUsers = isProd()
? // ian & stephen's ids
// ? filterDefined([
// await getPrivateUser('AJwLWoo3xue32XIiAVrL5SyR1WB2'),
// await getPrivateUser('tlmGNz9kjXc2EteizMORes4qvWl2'),
// ])
await getAllPrivateUsers()
: filterDefined([await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2')])
// get all users that haven't unsubscribed from weekly emails
const privateUsersToSendEmailsTo = privateUsers
.filter((user) => {
return isProd()
? user.notificationPreferences.profit_loss_updates.includes('email') &&
!user.weeklyPortfolioUpdateEmailSent
: true
})
// Send emails in batches
.slice(0, 200)
log(
'Sending weekly portfolio emails to',
privateUsersToSendEmailsTo.length,
'users'
)
const usersBets: { [userId: string]: Bet[] } = {}
// get all bets made by each user
await Promise.all(
privateUsersToSendEmailsTo.map(async (user) => {
return getValues<Bet>(
firestore.collectionGroup('bets').where('userId', '==', user.id)
).then((bets) => {
usersBets[user.id] = bets
})
})
)
const usersToContractsCreated: { [userId: string]: Contract[] } = {}
// Get all contracts created by each user
await Promise.all(
privateUsersToSendEmailsTo.map(async (user) => {
return getValues<Contract>(
firestore
.collection('contracts')
.where('creatorId', '==', user.id)
.where('createdTime', '>', Date.now() - 7 * DAY_MS)
).then((contracts) => {
usersToContractsCreated[user.id] = contracts
})
})
)
// Get all txns the users received over the past week
const usersToTxnsReceived: { [userId: string]: Txn[] } = {}
await Promise.all(
privateUsersToSendEmailsTo.map(async (user) => {
return getValues<Txn>(
firestore
.collection(`txns`)
.where('toId', '==', user.id)
.where('createdTime', '>', Date.now() - 7 * DAY_MS)
).then((txn) => {
usersToTxnsReceived[user.id] = txn
})
})
)
// Get a flat map of all the bets that users made to get the contracts they bet on
const contractsUsersBetOn = filterDefined(
await Promise.all(
uniq(
Object.values(usersBets).flatMap((bets) =>
bets.map((bet) => bet.contractId)
)
).map((contractId) =>
getValue<Contract>(firestore.collection('contracts').doc(contractId))
)
)
)
log('Found', contractsUsersBetOn.length, 'contracts')
let count = 0
await Promise.all(
privateUsersToSendEmailsTo.map(async (privateUser) => {
const user = await getUser(privateUser.id)
if (!user) return
const userBets = usersBets[privateUser.id] as Bet[]
const contractsUserBetOn = contractsUsersBetOn.filter((contract) =>
userBets.some((bet) => bet.contractId === contract.id)
)
const contractsBetOnInLastWeek = uniq(
userBets
.filter((bet) => bet.createdTime > Date.now() - 7 * DAY_MS)
.map((bet) => bet.contractId)
)
const totalTips = sum(
usersToTxnsReceived[privateUser.id]
.filter((txn) => txn.category === 'TIP')
.map((txn) => txn.amount)
)
const greenBg = 'rgba(0,160,0,0.2)'
const redBg = 'rgba(160,0,0,0.2)'
const clearBg = 'rgba(255,255,255,0)'
const roundedProfit =
Math.round(user.profitCached.weekly) === 0
? 0
: Math.floor(user.profitCached.weekly)
const performanceData = {
profit: formatMoney(user.profitCached.weekly),
profit_style: `background-color: ${
roundedProfit > 0 ? greenBg : roundedProfit === 0 ? clearBg : redBg
}`,
markets_created:
usersToContractsCreated[privateUser.id].length.toString(),
tips_received: formatMoney(totalTips),
unique_bettors: usersToTxnsReceived[privateUser.id]
.filter((txn) => txn.category === 'UNIQUE_BETTOR_BONUS')
.length.toString(),
markets_traded: contractsBetOnInLastWeek.length.toString(),
prediction_streak:
(user.currentBettingStreak?.toString() ?? '0') + ' days',
// More options: bonuses, tips given,
} as OverallPerformanceData
const investmentValueDifferences = sortBy(
filterDefined(
contractsUserBetOn.map((contract) => {
const cpmmContract = contract as CPMMContract
if (cpmmContract === undefined || cpmmContract.prob === undefined)
return
const bets = userBets.filter(
(bet) => bet.contractId === contract.id
)
const marketProbabilityAWeekAgo =
cpmmContract.prob - cpmmContract.probChanges.week
const currentMarketProbability = cpmmContract.resolutionProbability
? cpmmContract.resolutionProbability
: cpmmContract.prob
const betsValueAWeekAgo = computeInvestmentValueCustomProb(
bets.filter((b) => b.createdTime < Date.now() - 7 * DAY_MS),
contract,
marketProbabilityAWeekAgo
)
const currentBetsValue = computeInvestmentValueCustomProb(
bets,
contract,
currentMarketProbability
)
const marketChange =
currentMarketProbability - marketProbabilityAWeekAgo
return {
currentValue: currentBetsValue,
pastValue: betsValueAWeekAgo,
difference: currentBetsValue - betsValueAWeekAgo,
contractSlug: contract.slug,
marketProbAWeekAgo: marketProbabilityAWeekAgo,
questionTitle: contract.question,
questionUrl: contractUrl(contract),
questionProb: cpmmContract.resolution
? cpmmContract.resolution
: Math.round(cpmmContract.prob * 100) + '%',
questionChange:
(marketChange > 0 ? '+' : '') +
Math.round(marketChange * 100) +
'%',
questionChangeStyle: `color: ${
currentMarketProbability > marketProbabilityAWeekAgo
? 'rgba(0,160,0,1)'
: '#a80000'
};`,
} as PerContractInvestmentsData
})
),
(differences) => Math.abs(differences.difference)
).reverse()
log(
'Found',
investmentValueDifferences.length,
'investment differences for user',
privateUser.id
)
const [winningInvestments, losingInvestments] = partition(
investmentValueDifferences.filter(
(diff) =>
diff.pastValue > 0.01 &&
Math.abs(diff.difference / diff.pastValue) > 0.01 // difference is greater than 1%
),
(investmentsData: PerContractInvestmentsData) => {
return investmentsData.difference > 0
}
)
// pick 3 winning investments and 3 losing investments
const topInvestments = winningInvestments.slice(0, 2)
const worstInvestments = losingInvestments.slice(0, 2)
// if no bets in the last week ANd no market movers AND no markets created, don't send email
if (
contractsBetOnInLastWeek.length === 0 &&
topInvestments.length === 0 &&
worstInvestments.length === 0 &&
usersToContractsCreated[privateUser.id].length === 0
) {
log('No bets in last week, no market movers, no markets created')
await firestore.collection('private-users').doc(privateUser.id).update({
weeklyPortfolioUpdateEmailSent: true,
})
return
}
await sendWeeklyPortfolioUpdateEmail(
user,
privateUser,
topInvestments.concat(worstInvestments) as PerContractInvestmentsData[],
performanceData
)
await firestore.collection('private-users').doc(privateUser.id).update({
weeklyPortfolioUpdateEmailSent: true,
})
log('Sent weekly portfolio update email to', privateUser.email)
count++
log('sent out emails to user count:', count)
})
)
}
export type PerContractInvestmentsData = {
questionTitle: string
questionUrl: string
questionProb: string
questionChange: string
questionChangeStyle: string
currentValue: number
pastValue: number
difference: number
}
export type OverallPerformanceData = {
profit: string
prediction_streak: string
markets_traded: string
profit_style: string
tips_received: string
markets_created: string
unique_bettors: string
}

View File

@ -30,10 +30,10 @@ export function AddFundsButton(props: { className?: string }) {
<div className="modal">
<div className="modal-box">
<div className="mb-6 text-xl">Get Manifold Dollars</div>
<div className="mb-6 text-xl">Get Mana</div>
<div className="mb-6 text-gray-500">
Use Manifold Dollars to trade in your favorite markets. <br /> (Not
Buy mana (M$) to trade in your favorite markets. <br /> (Not
redeemable for cash.)
</div>

View File

@ -5,7 +5,7 @@ import { formatMoney } from 'common/util/format'
import { Col } from './layout/col'
import { SiteLink } from './site-link'
import { ENV_CONFIG } from 'common/envs/constants'
import { useWindowSize } from 'web/hooks/use-window-size'
import { useIsMobile } from 'web/hooks/use-is-mobile'
export function AmountInput(props: {
amount: number | undefined
@ -34,8 +34,7 @@ export function AmountInput(props: {
const isInvalid = !str || isNaN(amount)
onChange(isInvalid ? undefined : amount)
}
const { width } = useWindowSize()
const isMobile = (width ?? 0) < 768
const isMobile = useIsMobile(768)
return (
<Col className={className}>
<label className="input-group mb-4">

View File

@ -17,7 +17,7 @@ import { setCookie } from 'web/lib/util/cookie'
// Either we haven't looked up the logged in user yet (undefined), or we know
// the user is not logged in (null), or we know the user is logged in.
type AuthUser = undefined | null | UserAndPrivateUser
export type AuthUser = undefined | null | UserAndPrivateUser
const TEN_YEARS_SECS = 60 * 60 * 24 * 365 * 10
const CACHED_USER_KEY = 'CACHED_USER_KEY_V2'

View File

@ -40,7 +40,7 @@ export function Avatar(props: {
style={{ maxWidth: `${s * 0.25}rem` }}
src={avatarUrl}
onClick={onClick}
alt={username}
alt={`${username ?? 'Unknown user'} avatar`}
onError={() => {
// If the image doesn't load, clear the avatarUrl to show the default
// Mostly for localhost, when getting a 403 from googleusercontent

View File

@ -601,18 +601,24 @@ function BetRow(props: {
const isNumeric = outcomeType === 'NUMERIC'
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
const saleAmount = saleBet?.sale?.amount
// calculateSaleAmount is very slow right now so that's why we memoized this
const payout = useMemo(() => {
const saleBetAmount = saleBet?.sale?.amount
if (saleBetAmount) {
return saleBetAmount
} else if (contract.isResolved) {
return resolvedPayout(contract, bet)
} else {
return calculateSaleAmount(contract, bet, unfilledBets)
}
}, [contract, bet, saleBet, unfilledBets])
const saleDisplay = isAnte ? (
'ANTE'
) : saleAmount !== undefined ? (
<>{formatMoney(saleAmount)} (sold)</>
) : saleBet ? (
<>{formatMoney(payout)} (sold)</>
) : (
formatMoney(
isResolved
? resolvedPayout(contract, bet)
: calculateSaleAmount(contract, bet, unfilledBets)
)
formatMoney(payout)
)
const payoutIfChosenDisplay =

View File

@ -11,7 +11,7 @@ import { Row } from './layout/row'
import { LoadingIndicator } from './loading-indicator'
export function CommentInput(props: {
replyToUser?: { id: string; username: string }
replyTo?: { id: string; username: string }
// Reply to a free response answer
parentAnswerOutcome?: string
// Reply to another comment
@ -19,7 +19,7 @@ export function CommentInput(props: {
onSubmitComment?: (editor: Editor) => void
className?: string
}) {
const { parentAnswerOutcome, parentCommentId, replyToUser, onSubmitComment } =
const { parentAnswerOutcome, parentCommentId, replyTo, onSubmitComment } =
props
const user = useUser()
@ -55,7 +55,7 @@ export function CommentInput(props: {
<CommentInputTextArea
editor={editor}
upload={upload}
replyToUser={replyToUser}
replyTo={replyTo}
user={user}
submitComment={submitComment}
isSubmitting={isSubmitting}
@ -67,14 +67,13 @@ export function CommentInput(props: {
export function CommentInputTextArea(props: {
user: User | undefined | null
replyToUser?: { id: string; username: string }
replyTo?: { id: string; username: string }
editor: Editor | null
upload: Parameters<typeof TextEditor>[0]['upload']
submitComment: () => void
isSubmitting: boolean
}) {
const { user, editor, upload, submitComment, isSubmitting, replyToUser } =
props
const { user, editor, upload, submitComment, isSubmitting, replyTo } = props
useEffect(() => {
editor?.setEditable(!isSubmitting)
}, [isSubmitting, editor])
@ -108,12 +107,12 @@ export function CommentInputTextArea(props: {
},
})
// insert at mention and focus
if (replyToUser) {
if (replyTo) {
editor
.chain()
.setContent({
type: 'mention',
attrs: { label: replyToUser.username, id: replyToUser.id },
attrs: { label: replyTo.username, id: replyTo.id },
})
.insertContent(' ')
.focus()
@ -127,7 +126,7 @@ export function CommentInputTextArea(props: {
<TextEditor editor={editor} upload={upload}>
{user && !isSubmitting && (
<button
className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300"
className="btn btn-ghost btn-sm disabled:bg-inherit! px-2 disabled:text-gray-300"
disabled={!editor || editor.isEmpty}
onClick={submit}
>

View File

@ -48,18 +48,17 @@ import { Title } from './title'
export const SORTS = [
{ label: 'Newest', value: 'newest' },
{ label: 'Trending', value: 'score' },
{ label: `Most traded`, value: 'most-traded' },
{ label: 'Daily trending', value: 'daily-score' },
{ label: '24h volume', value: '24-hour-vol' },
{ label: '24h change', value: 'prob-change-day' },
{ label: 'Last updated', value: 'last-updated' },
{ label: 'Subsidy', value: 'liquidity' },
{ label: 'Close date', value: 'close-date' },
{ label: 'Closing soon', value: 'close-date' },
{ label: 'Resolve date', value: 'resolve-date' },
{ label: 'Highest %', value: 'prob-descending' },
{ label: 'Lowest %', value: 'prob-ascending' },
] as const
export type Sort = typeof SORTS[number]['value']
export const PROB_SORTS = ['prob-descending', 'prob-ascending']
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
@ -90,11 +89,13 @@ export function ContractSearch(props: {
hideGroupLink?: boolean
hideQuickBet?: boolean
noLinkAvatar?: boolean
showProbChange?: boolean
}
headerClassName?: string
persistPrefix?: string
useQueryUrlParam?: boolean
isWholePage?: boolean
includeProbSorts?: boolean
noControls?: boolean
maxResults?: number
renderContracts?: (
@ -117,6 +118,7 @@ export function ContractSearch(props: {
headerClassName,
persistPrefix,
useQueryUrlParam,
includeProbSorts,
isWholePage,
noControls,
maxResults,
@ -130,6 +132,7 @@ export function ContractSearch(props: {
numPages: 1,
pages: [] as Contract[][],
showTime: null as ShowTime | null,
showProbChange: false,
},
!persistPrefix
? undefined
@ -183,8 +186,9 @@ export function ContractSearch(props: {
const newPage = results.hits as any as Contract[]
const showTime =
sort === 'close-date' || sort === 'resolve-date' ? sort : null
const showProbChange = sort === 'daily-score'
const pages = freshQuery ? [newPage] : [...state.pages, newPage]
setState({ numPages: results.nbPages, pages, showTime })
setState({ numPages: results.nbPages, pages, showTime, showProbChange })
if (freshQuery && isWholePage) window.scrollTo(0, 0)
}
}
@ -202,6 +206,12 @@ export function ContractSearch(props: {
}, 100)
).current
const updatedCardUIOptions = useMemo(() => {
if (cardUIOptions?.showProbChange === undefined && state.showProbChange)
return { ...cardUIOptions, showProbChange: true }
return cardUIOptions
}, [cardUIOptions, state.showProbChange])
const contracts = state.pages
.flat()
.filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id))
@ -223,6 +233,7 @@ export function ContractSearch(props: {
persistPrefix={persistPrefix}
hideOrderSelector={hideOrderSelector}
useQueryUrlParam={useQueryUrlParam}
includeProbSorts={includeProbSorts}
user={user}
onSearchParametersChanged={onSearchParametersChanged}
noControls={noControls}
@ -241,7 +252,7 @@ export function ContractSearch(props: {
showTime={state.showTime ?? undefined}
onContractClick={onContractClick}
highlightOptions={highlightOptions}
cardUIOptions={cardUIOptions}
cardUIOptions={updatedCardUIOptions}
/>
)}
</Col>
@ -256,6 +267,7 @@ function ContractSearchControls(props: {
additionalFilter?: AdditionalFilter
persistPrefix?: string
hideOrderSelector?: boolean
includeProbSorts?: boolean
onSearchParametersChanged: (params: SearchParameters) => void
useQueryUrlParam?: boolean
user?: User | null
@ -275,6 +287,7 @@ function ContractSearchControls(props: {
user,
noControls,
autoFocus,
includeProbSorts,
} = props
const router = useRouter()
@ -443,6 +456,7 @@ function ContractSearchControls(props: {
selectSort={selectSort}
sort={sort}
className={'flex flex-row gap-2'}
includeProbSorts={includeProbSorts}
/>
)}
{isMobile && (
@ -456,6 +470,7 @@ function ContractSearchControls(props: {
selectSort={selectSort}
sort={sort}
className={'flex flex-col gap-4'}
includeProbSorts={includeProbSorts}
/>
}
/>
@ -510,6 +525,7 @@ export function SearchFilters(props: {
selectSort: (newSort: Sort) => void
sort: string
className?: string
includeProbSorts?: boolean
}) {
const {
filter,
@ -518,7 +534,13 @@ export function SearchFilters(props: {
selectSort,
sort,
className,
includeProbSorts,
} = props
const sorts = includeProbSorts
? SORTS
: SORTS.filter((sort) => !PROB_SORTS.includes(sort.value))
return (
<div className={className}>
<select
@ -537,7 +559,7 @@ export function SearchFilters(props: {
value={sort}
onChange={(e) => selectSort(e.target.value as Sort)}
>
{SORTS.map((option) => (
{sorts.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>

View File

@ -7,6 +7,7 @@ import { Col } from '../layout/col'
import {
BinaryContract,
Contract,
CPMMBinaryContract,
FreeResponseContract,
MultipleChoiceContract,
NumericContract,
@ -32,6 +33,8 @@ import { track } from '@amplitude/analytics-browser'
import { trackCallback } from 'web/lib/service/analytics'
import { getMappedValue } from 'common/pseudo-numeric'
import { Tooltip } from '../tooltip'
import { SiteLink } from '../site-link'
import { ProbChange } from './prob-change-table'
export function ContractCard(props: {
contract: Contract
@ -379,3 +382,34 @@ export function PseudoNumericResolutionOrExpectation(props: {
</Col>
)
}
export function ContractCardProbChange(props: {
contract: CPMMBinaryContract
noLinkAvatar?: boolean
className?: string
}) {
const { contract, noLinkAvatar, className } = props
return (
<Col
className={clsx(
className,
'mb-4 rounded-lg bg-white shadow hover:bg-gray-100 hover:shadow-lg'
)}
>
<AvatarDetails
contract={contract}
className={'px-6 pt-4'}
noLink={noLinkAvatar}
/>
<Row className={clsx('items-start justify-between gap-4 ', className)}>
<SiteLink
className="pl-6 pr-0 pt-2 pb-4 font-semibold text-indigo-700"
href={contractPath(contract)}
>
<span className="line-clamp-3">{contract.question}</span>
</SiteLink>
<ProbChange className="py-2 pr-4" contract={contract} />
</Row>
</Col>
)
}

View File

@ -3,9 +3,8 @@ import { resolvedPayout } from 'common/calculate'
import { Contract } from 'common/contract'
import { formatMoney } from 'common/util/format'
import { groupBy, mapValues, sumBy, sortBy, keyBy } from 'lodash'
import { useState, useMemo, useEffect } from 'react'
import { memo } from 'react'
import { useComments } from 'web/hooks/use-comments'
import { listUsers, User } from 'web/lib/firebase/users'
import { FeedBet } from '../feed/feed-bets'
import { FeedComment } from '../feed/feed-comments'
import { Spacer } from '../layout/spacer'
@ -13,59 +12,48 @@ import { Leaderboard } from '../leaderboard'
import { Title } from '../title'
import { BETTORS } from 'common/user'
export function ContractLeaderboard(props: {
export const ContractLeaderboard = memo(function ContractLeaderboard(props: {
contract: Contract
bets: Bet[]
}) {
const { contract, bets } = props
const [users, setUsers] = useState<User[]>()
const { userProfits, top5Ids } = useMemo(() => {
// Create a map of userIds to total profits (including sales)
const openBets = bets.filter((bet) => !bet.isSold && !bet.sale)
const betsByUser = groupBy(openBets, 'userId')
const userProfits = mapValues(betsByUser, (bets) =>
sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount)
)
// Find the 5 users with the most profits
const top5Ids = Object.entries(userProfits)
.sort(([_i1, p1], [_i2, p2]) => p2 - p1)
.filter(([, p]) => p > 0)
.slice(0, 5)
.map(([id]) => id)
return { userProfits, top5Ids }
}, [contract, bets])
useEffect(() => {
if (top5Ids.length > 0) {
listUsers(top5Ids).then((users) => {
const sortedUsers = sortBy(users, (user) => -userProfits[user.id])
setUsers(sortedUsers)
})
const userProfits = mapValues(betsByUser, (bets) => {
return {
name: bets[0].userName,
username: bets[0].userUsername,
avatarUrl: bets[0].userAvatarUrl,
total: sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount),
}
}, [userProfits, top5Ids])
})
// Find the 5 users with the most profits
const top5 = Object.values(userProfits)
.sort((p1, p2) => p2.total - p1.total)
.filter((p) => p.total > 0)
.slice(0, 5)
return users && users.length > 0 ? (
return top5 && top5.length > 0 ? (
<Leaderboard
title={`🏅 Top ${BETTORS}`}
users={users || []}
entries={top5 || []}
columns={[
{
header: 'Total profit',
renderCell: (user) => formatMoney(userProfits[user.id] || 0),
renderCell: (entry) => formatMoney(entry.total),
},
]}
className="mt-12 max-w-sm"
/>
) : null
}
})
export function ContractTopTrades(props: { contract: Contract; bets: Bet[] }) {
const { contract, bets } = props
// todo: this stuff should be calced in DB at resolve time
const comments = useComments(contract.id)
const commentsById = keyBy(comments, 'id')
const betsById = keyBy(bets, 'id')
// If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit
@ -86,29 +74,23 @@ export function ContractTopTrades(props: { contract: Contract; bets: Bet[] }) {
const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id
const topBettor = betsById[topBetId]?.userName
// And also the commentId of the comment with the highest profit
const topCommentId = sortBy(
comments,
(c) => c.betId && -profitById[c.betId]
)[0]?.id
// And also the comment with the highest profit
const topComment = sortBy(comments, (c) => c.betId && -profitById[c.betId])[0]
return (
<div className="mt-12 max-w-sm">
{topCommentId && profitById[topCommentId] > 0 && (
{topComment && profitById[topComment.id] > 0 && (
<>
<Title text="💬 Proven correct" className="!mt-0" />
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
<FeedComment
contract={contract}
comment={commentsById[topCommentId]}
/>
<FeedComment contract={contract} comment={topComment} />
</div>
<Spacer h={16} />
</>
)}
{/* If they're the same, only show the comment; otherwise show both */}
{topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && (
{topBettor && topBetId !== topComment?.betId && profitById[topBetId] > 0 && (
<>
<Title text="💸 Best bet" className="!mt-0" />
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">

View File

@ -49,6 +49,7 @@ export function ContractTabs(props: { contract: Contract; bets: Bet[] }) {
return (
<Tabs
className="mb-4"
currentPageForAnalytics={'contract'}
tabs={[
{

View File

@ -2,7 +2,7 @@ import { Contract } from 'web/lib/firebase/contracts'
import { User } from 'web/lib/firebase/users'
import { Col } from '../layout/col'
import { SiteLink } from '../site-link'
import { ContractCard } from './contract-card'
import { ContractCard, ContractCardProbChange } from './contract-card'
import { ShowTime } from './contract-details'
import { ContractSearch } from '../contract-search'
import { useCallback } from 'react'
@ -10,6 +10,7 @@ import clsx from 'clsx'
import { LoadingIndicator } from '../loading-indicator'
import { VisibilityObserver } from '../visibility-observer'
import Masonry from 'react-masonry-css'
import { CPMMBinaryContract } from 'common/contract'
export type ContractHighlightOptions = {
contractIds?: string[]
@ -25,6 +26,7 @@ export function ContractsGrid(props: {
hideQuickBet?: boolean
hideGroupLink?: boolean
noLinkAvatar?: boolean
showProbChange?: boolean
}
highlightOptions?: ContractHighlightOptions
trackingPostfix?: string
@ -39,7 +41,8 @@ export function ContractsGrid(props: {
highlightOptions,
trackingPostfix,
} = props
const { hideQuickBet, hideGroupLink, noLinkAvatar } = cardUIOptions || {}
const { hideQuickBet, hideGroupLink, noLinkAvatar, showProbChange } =
cardUIOptions || {}
const { contractIds, highlightClassName } = highlightOptions || {}
const onVisibilityUpdated = useCallback(
(visible) => {
@ -73,7 +76,13 @@ export function ContractsGrid(props: {
className="-ml-4 flex w-auto"
columnClassName="pl-4 bg-clip-padding"
>
{contracts.map((contract) => (
{contracts.map((contract) =>
showProbChange && contract.mechanism === 'cpmm-1' ? (
<ContractCardProbChange
key={contract.id}
contract={contract as CPMMBinaryContract}
/>
) : (
<ContractCard
contract={contract}
key={contract.id}
@ -90,7 +99,8 @@ export function ContractsGrid(props: {
contractIds?.includes(contract.id) && highlightClassName
)}
/>
))}
)
)}
</Masonry>
{loadMore && (
<VisibilityObserver

View File

@ -1,6 +1,4 @@
import clsx from 'clsx'
import { ShareIcon } from '@heroicons/react/outline'
import { Row } from '../layout/row'
import { Contract } from 'web/lib/firebase/contracts'
import React, { useState } from 'react'
@ -10,7 +8,7 @@ import { ShareModal } from './share-modal'
import { FollowMarketButton } from 'web/components/follow-market-button'
import { LikeMarketButton } from 'web/components/contract/like-market-button'
import { ContractInfoDialog } from 'web/components/contract/contract-info-dialog'
import { Col } from 'web/components/layout/col'
import { Tooltip } from '../tooltip'
export function ExtraContractActionsRow(props: { contract: Contract }) {
const { contract } = props
@ -23,17 +21,14 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
{user?.id !== contract.creatorId && (
<LikeMarketButton contract={contract} user={user} />
)}
<Tooltip text="Share" placement="bottom" noTap noFade>
<Button
size="sm"
color="gray-white"
className={'flex'}
onClick={() => {
setShareOpen(true)
}}
onClick={() => setShareOpen(true)}
>
<Row>
<ShareIcon className={clsx('h-5 w-5')} aria-hidden="true" />
</Row>
<ShareIcon className="h-5 w-5" aria-hidden />
<ShareModal
isOpen={isShareOpen}
setOpen={setShareOpen}
@ -41,9 +36,8 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
user={user}
/>
</Button>
<Col className={'justify-center'}>
</Tooltip>
<ContractInfoDialog contract={contract} />
</Col>
</Row>
)
}

View File

@ -13,6 +13,7 @@ import { Col } from 'web/components/layout/col'
import { firebaseLogin } from 'web/lib/firebase/users'
import { useMarketTipTxns } from 'web/hooks/use-tip-txns'
import { sum } from 'lodash'
import { Tooltip } from '../tooltip'
export function LikeMarketButton(props: {
contract: Contract
@ -37,6 +38,12 @@ export function LikeMarketButton(props: {
}
return (
<Tooltip
text={`Tip ${formatMoney(LIKE_TIP_AMOUNT)}`}
placement="bottom"
noTap
noFade
>
<Button
size={'sm'}
className={'max-w-xs self-center'}
@ -69,5 +76,6 @@ export function LikeMarketButton(props: {
)}
</Col>
</Button>
</Tooltip>
)
}

View File

@ -1,4 +1,5 @@
import clsx from 'clsx'
import { partition } from 'lodash'
import { contractPath } from 'web/lib/firebase/contracts'
import { CPMMContract } from 'common/contract'
import { formatPercent } from 'common/util/format'
@ -8,16 +9,17 @@ import { Row } from '../layout/row'
import { LoadingIndicator } from '../loading-indicator'
export function ProbChangeTable(props: {
changes:
| { positiveChanges: CPMMContract[]; negativeChanges: CPMMContract[] }
| undefined
changes: CPMMContract[] | undefined
full?: boolean
}) {
const { changes, full } = props
if (!changes) return <LoadingIndicator />
const { positiveChanges, negativeChanges } = changes
const [positiveChanges, negativeChanges] = partition(
changes,
(c) => c.probChanges.day > 0
)
const threshold = 0.01
const positiveAboveThreshold = positiveChanges.filter(
@ -53,10 +55,18 @@ export function ProbChangeTable(props: {
)
}
function ProbChangeRow(props: { contract: CPMMContract }) {
const { contract } = props
export function ProbChangeRow(props: {
contract: CPMMContract
className?: string
}) {
const { contract, className } = props
return (
<Row className="items-center justify-between gap-4 hover:bg-gray-100">
<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)}

View File

@ -21,6 +21,7 @@ import { CreateChallengeModal } from 'web/components/challenges/create-challenge
import { useState } from 'react'
import { CHALLENGES_ENABLED } from 'common/challenge'
import ChallengeIcon from 'web/lib/icons/challenge-icon'
import { QRCode } from '../qr-code'
export function ShareModal(props: {
contract: Contract
@ -54,6 +55,12 @@ export function ShareModal(props: {
</SiteLink>{' '}
if a new user signs up using the link!
</p>
<QRCode
url={shareUrl}
className="self-center"
width={150}
height={150}
/>
<Button
size="2xl"
color="indigo"

View File

@ -0,0 +1,94 @@
import { useState } from 'react'
import { Spacer } from 'web/components/layout/spacer'
import { Title } from 'web/components/title'
import Textarea from 'react-expanding-textarea'
import { TextEditor, useTextEditor } from 'web/components/editor'
import { createPost } from 'web/lib/firebase/api'
import clsx from 'clsx'
import Router from 'next/router'
import { MAX_POST_TITLE_LENGTH } from 'common/post'
import { postPath } from 'web/lib/firebase/posts'
import { Group } from 'common/group'
export function CreatePost(props: { group?: Group }) {
const [title, setTitle] = useState('')
const [error, setError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const { group } = props
const { editor, upload } = useTextEditor({
disabled: isSubmitting,
})
const isValid = editor && title.length > 0 && editor.isEmpty === false
async function savePost(title: string) {
if (!editor) return
const newPost = {
title: title,
content: editor.getJSON(),
groupId: group?.id,
}
const result = await createPost(newPost).catch((e) => {
console.log(e)
setError('There was an error creating the post, please try again')
return e
})
if (result.post) {
await Router.push(postPath(result.post.slug))
}
}
return (
<div className="mx-auto w-full max-w-3xl">
<div className="rounded-lg px-6 py-4 sm:py-0">
<Title className="!mt-0" text="Create a post" />
<form>
<div className="form-control w-full">
<label className="label">
<span className="mb-1">
Title<span className={'text-red-700'}> *</span>
</span>
</label>
<Textarea
placeholder="e.g. Elon Mania Post"
className="input input-bordered resize-none"
autoFocus
maxLength={MAX_POST_TITLE_LENGTH}
value={title}
onChange={(e) => setTitle(e.target.value || '')}
/>
<Spacer h={6} />
<label className="label">
<span className="mb-1">
Content<span className={'text-red-700'}> *</span>
</span>
</label>
<TextEditor editor={editor} upload={upload} />
<Spacer h={6} />
<button
type="submit"
className={clsx(
'btn btn-primary normal-case',
isSubmitting && 'loading disabled'
)}
disabled={isSubmitting || !isValid || upload.isLoading}
onClick={async () => {
setIsSubmitting(true)
await savePost(title)
setIsSubmitting(false)
}}
>
{isSubmitting ? 'Creating...' : 'Create a post'}
</button>
{error !== '' && <div className="text-red-700">{error}</div>}
</div>
</form>
</div>
</div>
)
}

View File

@ -1,7 +1,7 @@
import { Answer } from 'common/answer'
import { FreeResponseContract } from 'common/contract'
import { ContractComment } from 'common/comment'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row'
import { Avatar } from 'web/components/avatar'
@ -10,11 +10,10 @@ import clsx from 'clsx'
import {
ContractCommentInput,
FeedComment,
ReplyTo,
} from 'web/components/feed/feed-comments'
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
import { useRouter } from 'next/router'
import { User } from 'common/user'
import { useEvent } from 'web/hooks/use-event'
import { CommentTipMap } from 'web/hooks/use-tip-txns'
import { UserLink } from 'web/components/user-link'
@ -27,32 +26,17 @@ export function FeedAnswerCommentGroup(props: {
const { answer, contract, answerComments, tips } = props
const { username, avatarUrl, name, text } = answer
const [replyToUser, setReplyToUser] =
useState<Pick<User, 'id' | 'username'>>()
const [showReply, setShowReply] = useState(false)
const [highlighted, setHighlighted] = useState(false)
const [replyTo, setReplyTo] = useState<ReplyTo>()
const router = useRouter()
const answerElementId = `answer-${answer.id}`
const scrollAndOpenReplyInput = useEvent(
(comment?: ContractComment, answer?: Answer) => {
setReplyToUser(
comment
? { id: comment.userId, username: comment.userUsername }
: answer
? { id: answer.userId, username: answer.username }
: undefined
)
setShowReply(true)
}
)
const highlighted = router.asPath.endsWith(`#${answerElementId}`)
const answerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (router.asPath.endsWith(`#${answerElementId}`)) {
setHighlighted(true)
if (highlighted && answerRef.current != null) {
answerRef.current.scrollIntoView(true)
}
}, [answerElementId, router.asPath])
}, [highlighted])
return (
<Col className="relative flex-1 items-stretch gap-3">
@ -61,6 +45,7 @@ export function FeedAnswerCommentGroup(props: {
'gap-3 space-x-3 pt-4 transition-all duration-1000',
highlighted ? `-m-2 my-3 rounded bg-indigo-500/[0.2] p-2` : ''
)}
ref={answerRef}
id={answerElementId}
>
<Avatar username={username} avatarUrl={avatarUrl} />
@ -83,7 +68,9 @@ export function FeedAnswerCommentGroup(props: {
<div className="sm:hidden">
<button
className="text-xs font-bold text-gray-500 hover:underline"
onClick={() => scrollAndOpenReplyInput(undefined, answer)}
onClick={() =>
setReplyTo({ id: answer.id, username: answer.username })
}
>
Reply
</button>
@ -92,7 +79,9 @@ export function FeedAnswerCommentGroup(props: {
<div className="justify-initial hidden sm:block">
<button
className="text-xs font-bold text-gray-500 hover:underline"
onClick={() => scrollAndOpenReplyInput(undefined, answer)}
onClick={() =>
setReplyTo({ id: answer.id, username: answer.username })
}
>
Reply
</button>
@ -107,11 +96,13 @@ export function FeedAnswerCommentGroup(props: {
contract={contract}
comment={comment}
tips={tips[comment.id] ?? {}}
onReplyClick={scrollAndOpenReplyInput}
onReplyClick={() =>
setReplyTo({ id: comment.id, username: comment.userUsername })
}
/>
))}
</Col>
{showReply && (
{replyTo && (
<div className="relative ml-7">
<span
className="absolute -left-1 -ml-[1px] mt-[1.25rem] h-2 w-0.5 rotate-90 bg-gray-200"
@ -120,8 +111,8 @@ export function FeedAnswerCommentGroup(props: {
<ContractCommentInput
contract={contract}
parentAnswerOutcome={answer.number.toString()}
replyToUser={replyToUser}
onSubmitComment={() => setShowReply(false)}
replyTo={replyTo}
onSubmitComment={() => setReplyTo(undefined)}
/>
</div>
)}

View File

@ -1,6 +1,6 @@
import { ContractComment } from 'common/comment'
import { Contract } from 'common/contract'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useUser } from 'web/hooks/use-user'
import { formatMoney } from 'common/util/format'
import { useRouter } from 'next/router'
@ -20,6 +20,8 @@ import { Editor } from '@tiptap/react'
import { UserLink } from 'web/components/user-link'
import { CommentInput } from '../comment-input'
export type ReplyTo = { id: string; username: string }
export function FeedCommentThread(props: {
contract: Contract
threadComments: ContractComment[]
@ -27,13 +29,7 @@ export function FeedCommentThread(props: {
parentComment: ContractComment
}) {
const { contract, threadComments, tips, parentComment } = props
const [showReply, setShowReply] = useState(false)
const [replyTo, setReplyTo] = useState<{ id: string; username: string }>()
function scrollAndOpenReplyInput(comment: ContractComment) {
setReplyTo({ id: comment.userId, username: comment.userUsername })
setShowReply(true)
}
const [replyTo, setReplyTo] = useState<ReplyTo>()
return (
<Col className="relative w-full items-stretch gap-3 pb-4">
@ -48,10 +44,12 @@ export function FeedCommentThread(props: {
contract={contract}
comment={comment}
tips={tips[comment.id] ?? {}}
onReplyClick={scrollAndOpenReplyInput}
onReplyClick={() =>
setReplyTo({ id: comment.id, username: comment.userUsername })
}
/>
))}
{showReply && (
{replyTo && (
<Col className="-pb-2 relative ml-6">
<span
className="absolute -left-1 -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200"
@ -60,10 +58,8 @@ export function FeedCommentThread(props: {
<ContractCommentInput
contract={contract}
parentCommentId={parentComment.id}
replyToUser={replyTo}
onSubmitComment={() => {
setShowReply(false)
}}
replyTo={replyTo}
onSubmitComment={() => setReplyTo(undefined)}
/>
</Col>
)}
@ -76,7 +72,7 @@ export function FeedComment(props: {
comment: ContractComment
tips?: CommentTips
indent?: boolean
onReplyClick?: (comment: ContractComment) => void
onReplyClick?: () => void
}) {
const { contract, comment, tips, indent, onReplyClick } = props
const {
@ -98,16 +94,19 @@ export function FeedComment(props: {
money = formatMoney(Math.abs(comment.betAmount))
}
const [highlighted, setHighlighted] = useState(false)
const router = useRouter()
const highlighted = router.asPath.endsWith(`#${comment.id}`)
const commentRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (router.asPath.endsWith(`#${comment.id}`)) {
setHighlighted(true)
if (highlighted && commentRef.current != null) {
commentRef.current.scrollIntoView(true)
}
}, [comment.id, router.asPath])
}, [highlighted])
return (
<Row
ref={commentRef}
id={comment.id}
className={clsx(
'relative',
@ -174,7 +173,7 @@ export function FeedComment(props: {
{onReplyClick && (
<button
className="font-bold hover:underline"
onClick={() => onReplyClick(comment)}
onClick={onReplyClick}
>
Reply
</button>
@ -204,7 +203,7 @@ export function ContractCommentInput(props: {
contract: Contract
className?: string
parentAnswerOutcome?: string | undefined
replyToUser?: { id: string; username: string }
replyTo?: ReplyTo
parentCommentId?: string
onSubmitComment?: () => void
}) {
@ -226,7 +225,7 @@ export function ContractCommentInput(props: {
return (
<CommentInput
replyToUser={props.replyToUser}
replyTo={props.replyTo}
parentAnswerOutcome={props.parentAnswerOutcome}
parentCommentId={props.parentCommentId}
onSubmitComment={onSubmitComment}

View File

@ -14,6 +14,7 @@ import { track } from 'web/lib/service/analytics'
import { WatchMarketModal } from 'web/components/contract/watch-market-modal'
import { useState } from 'react'
import { Col } from 'web/components/layout/col'
import { Tooltip } from './tooltip'
export const FollowMarketButton = (props: {
contract: Contract
@ -23,7 +24,15 @@ export const FollowMarketButton = (props: {
const followers = useContractFollows(contract.id)
const [open, setOpen] = useState(false)
const watching = followers?.includes(user?.id ?? 'nope')
return (
<Tooltip
text={watching ? 'Unfollow' : 'Follow'}
placement="bottom"
noTap
noFade
>
<Button
size={'sm'}
color={'gray-white'}
@ -54,7 +63,7 @@ export const FollowMarketButton = (props: {
}
}}
>
{followers?.includes(user?.id ?? 'nope') ? (
{watching ? (
<Col className={'items-center gap-x-2 sm:flex-row'}>
<EyeOffIcon
className={clsx('h-5 w-5 sm:h-6 sm:w-6')}
@ -79,5 +88,6 @@ export const FollowMarketButton = (props: {
} a question!`}
/>
</Button>
</Tooltip>
)
}

View File

@ -117,6 +117,7 @@ function FollowsDialog(props: {
<div className="p-2 pb-1 text-xl">{user.name}</div>
<div className="p-2 pt-0 text-sm text-gray-500">@{user.username}</div>
<Tabs
className="mb-4"
tabs={[
{
title: 'Following',

View File

@ -16,29 +16,26 @@ import { usePost } from 'web/hooks/use-post'
export function GroupAboutPost(props: {
group: Group
isEditable: boolean
post: Post
post: Post | null
}) {
const { group, isEditable } = props
const post = usePost(group.aboutPostId) ?? props.post
return (
<div className="rounded-md bg-white p-4 ">
{isEditable ? (
<RichEditGroupAboutPost group={group} post={post} />
) : (
<Content content={post.content} />
)}
{isEditable && <RichEditGroupAboutPost group={group} post={post} />}
{!isEditable && post && <Content content={post.content} />}
</div>
)
}
function RichEditGroupAboutPost(props: { group: Group; post: Post }) {
function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {
const { group, post } = props
const [editing, setEditing] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({
defaultValue: post.content,
defaultValue: post?.content,
disabled: isSubmitting,
})
@ -49,7 +46,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post }) {
content: editor.getJSON(),
}
if (group.aboutPostId == null) {
if (post == null) {
const result = await createPost(newPost).catch((e) => {
console.error(e)
return e
@ -65,6 +62,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post }) {
}
async function deleteGroupAboutPost() {
if (post == null) return
await deletePost(post)
await deleteFieldFromGroup(group, 'aboutPostId')
}
@ -91,7 +89,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post }) {
</>
) : (
<>
{group.aboutPostId == null ? (
{post == null ? (
<div className="text-center text-gray-500">
<p className="text-sm">
No post has been added yet.

View File

@ -23,6 +23,7 @@ export function LandingPagePanel(props: { hotContracts: Contract[] }) {
height={250}
width={250}
className="self-center"
alt="Manifold logo"
src="/flappy-logo.gif"
/>
<div className="m-4 max-w-[550px] self-center">

View File

@ -32,7 +32,7 @@ export function ControlledTabs(props: TabProps & { activeIndex: number }) {
return (
<>
<nav
className={clsx('mb-4 space-x-8 border-b border-gray-200', className)}
className={clsx('space-x-8 border-b border-gray-200', className)}
aria-label="Tabs"
>
{tabs.map((tab, i) => (

View File

@ -1,28 +1,33 @@
import clsx from 'clsx'
import { User } from 'common/user'
import { Avatar } from './avatar'
import { Row } from './layout/row'
import { SiteLink } from './site-link'
import { Title } from './title'
export function Leaderboard(props: {
interface LeaderboardEntry {
username: string
name: string
avatarUrl?: string
}
export function Leaderboard<T extends LeaderboardEntry>(props: {
title: string
users: User[]
entries: T[]
columns: {
header: string
renderCell: (user: User) => any
renderCell: (entry: T) => any
}[]
className?: string
maxToShow?: number
}) {
// TODO: Ideally, highlight your own entry on the leaderboard
const { title, columns, className } = props
const maxToShow = props.maxToShow ?? props.users.length
const users = props.users.slice(0, maxToShow)
const maxToShow = props.maxToShow ?? props.entries.length
const entries = props.entries.slice(0, maxToShow)
return (
<div className={clsx('w-full px-1', className)}>
<Title text={title} className="!mt-0" />
{users.length === 0 ? (
{entries.length === 0 ? (
<div className="ml-2 text-gray-500">None yet</div>
) : (
<div className="overflow-x-auto">
@ -37,19 +42,19 @@ export function Leaderboard(props: {
</tr>
</thead>
<tbody>
{users.map((user, index) => (
<tr key={user.id}>
{entries.map((entry, index) => (
<tr key={index}>
<td>{index + 1}</td>
<td className="max-w-[190px]">
<SiteLink className="relative" href={`/${user.username}`}>
<SiteLink className="relative" href={`/${entry.username}`}>
<Row className="items-center gap-4">
<Avatar avatarUrl={user.avatarUrl} size={8} />
<div className="truncate">{user.name}</div>
<Avatar avatarUrl={entry.avatarUrl} size={8} />
<div className="truncate">{entry.name}</div>
</Row>
</SiteLink>
</td>
{columns.map((column) => (
<td key={column.header}>{column.renderCell(user)}</td>
<td key={column.header}>{column.renderCell(entry)}</td>
))}
</tr>
))}

View File

@ -1,94 +0,0 @@
import { ClipboardIcon, HomeIcon } from '@heroicons/react/outline'
import { Item } from './sidebar-item'
import clsx from 'clsx'
import { trackCallback } from 'web/lib/service/analytics'
import TrophyIcon from 'web/lib/icons/trophy-icon'
import { useUser } from 'web/hooks/use-user'
import NotificationsIcon from '../notifications-icon'
import router from 'next/router'
import { userProfileItem } from './bottom-nav-bar'
const mobileGroupNavigation = [
{ name: 'Markets', key: 'markets', icon: HomeIcon },
{ name: 'Leaderboard', key: 'leaderboards', icon: TrophyIcon },
{ name: 'About', key: 'about', icon: ClipboardIcon },
]
const mobileGeneralNavigation = [
{
name: 'Notifications',
key: 'notifications',
icon: NotificationsIcon,
href: '/notifications',
},
]
export function GroupNavBar(props: {
currentPage: string
onClick: (key: string) => void
}) {
const { currentPage } = props
const user = useUser()
return (
<nav className="z-20 flex justify-between border-t-2 bg-white text-xs text-gray-700 lg:hidden">
{mobileGroupNavigation.map((item) => (
<NavBarItem
key={item.name}
item={item}
currentPage={currentPage}
onClick={props.onClick}
/>
))}
{mobileGeneralNavigation.map((item) => (
<NavBarItem
key={item.name}
item={item}
currentPage={currentPage}
onClick={() => {
router.push(item.href)
}}
/>
))}
{user && (
<NavBarItem
key={'profile'}
currentPage={currentPage}
onClick={() => {
router.push(`/${user.username}?tab=trades`)
}}
item={userProfileItem(user)}
/>
)}
</nav>
)
}
function NavBarItem(props: {
item: Item
currentPage: string
onClick: (key: string) => void
}) {
const { item, currentPage } = props
const track = trackCallback(
`group navbar: ${item.trackingEventName ?? item.name}`
)
return (
<button onClick={() => props.onClick(item.key ?? '#')}>
<a
className={clsx(
'block w-full py-1 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700',
currentPage === item.key && 'bg-gray-200 text-indigo-700'
)}
onClick={track}
>
{item.icon && <item.icon className="my-1 mx-auto h-6 w-6" />}
{item.name}
</a>
</button>
)
}

View File

@ -1,82 +0,0 @@
import { ClipboardIcon, HomeIcon } from '@heroicons/react/outline'
import clsx from 'clsx'
import { useUser } from 'web/hooks/use-user'
import { ManifoldLogo } from './manifold-logo'
import { ProfileSummary } from './profile-menu'
import React from 'react'
import TrophyIcon from 'web/lib/icons/trophy-icon'
import { SignInButton } from '../sign-in-button'
import NotificationsIcon from '../notifications-icon'
import { SidebarItem } from './sidebar-item'
import { buildArray } from 'common/util/array'
import { User } from 'common/user'
import { Row } from '../layout/row'
import { Spacer } from '../layout/spacer'
const groupNavigation = [
{ name: 'Markets', key: 'markets', icon: HomeIcon },
{ name: 'About', key: 'about', icon: ClipboardIcon },
{ name: 'Leaderboard', key: 'leaderboards', icon: TrophyIcon },
]
const generalNavigation = (user?: User | null) =>
buildArray(
user && {
name: 'Notifications',
href: `/notifications`,
key: 'notifications',
icon: NotificationsIcon,
}
)
export function GroupSidebar(props: {
groupName: string
className?: string
onClick: (key: string) => void
joinOrAddQuestionsButton: React.ReactNode
currentKey: string
}) {
const { className, groupName, currentKey } = props
const user = useUser()
return (
<nav
aria-label="Group Sidebar"
className={clsx('flex max-h-[100vh] flex-col', className)}
>
<ManifoldLogo className="pt-6" twoLine />
<Row className="pl-2 text-xl text-indigo-700 sm:mt-3">{groupName}</Row>
<div className=" min-h-0 shrink flex-col items-stretch gap-1 pt-6 lg:flex ">
{user ? (
<ProfileSummary user={user} />
) : (
<SignInButton className="mb-4" />
)}
</div>
{/* Desktop navigation */}
{groupNavigation.map((item) => (
<SidebarItem
key={item.key}
item={item}
currentPage={currentKey}
onClick={props.onClick}
/>
))}
{generalNavigation(user).map((item) => (
<SidebarItem
key={item.key}
item={item}
currentPage={currentKey}
onClick={props.onClick}
/>
))}
<Spacer h={2} />
{props.joinOrAddQuestionsButton}
</nav>
)
}

View File

@ -26,9 +26,14 @@ import TrophyIcon from 'web/lib/icons/trophy-icon'
import { SignInButton } from '../sign-in-button'
import { SidebarItem } from './sidebar-item'
import { MoreButton } from './more-button'
import { Row } from '../layout/row'
import { Spacer } from '../layout/spacer'
export default function Sidebar(props: { className?: string }) {
const { className } = props
export default function Sidebar(props: {
className?: string
logoSubheading?: string
}) {
const { className, logoSubheading } = props
const router = useRouter()
const currentPage = router.pathname
@ -51,7 +56,13 @@ export default function Sidebar(props: { className?: string }) {
aria-label="Sidebar"
className={clsx('flex max-h-[100vh] flex-col', className)}
>
<ManifoldLogo className="py-6" twoLine />
<ManifoldLogo className="pt-6" twoLine />
{logoSubheading && (
<Row className="pl-2 text-2xl text-indigo-700 sm:mt-3">
{logoSubheading}
</Row>
)}
<Spacer h={6} />
{!user && <SignInButton className="mb-4" />}

View File

@ -99,8 +99,6 @@ const useIsTwitch = (user: User | null | undefined) => {
const isTwitch = router.pathname === '/twitch'
useEffect(() => {
console.log('twich?', isTwitch)
if (isTwitch && user?.shouldShowWelcome) {
updateUser(user.id, { ['shouldShowWelcome']: false })
}

View File

@ -9,8 +9,15 @@ export function Page(props: {
className?: string
rightSidebarClassName?: string
children?: ReactNode
logoSubheading?: string
}) {
const { children, rightSidebar, className, rightSidebarClassName } = props
const {
children,
rightSidebar,
className,
rightSidebarClassName,
logoSubheading,
} = props
const bottomBarPadding = 'pb-[58px] lg:pb-0 '
return (
@ -23,7 +30,10 @@ export function Page(props: {
)}
>
<Toaster />
<Sidebar className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:flex" />
<Sidebar
logoSubheading={logoSubheading}
className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:flex"
/>
<main
className={clsx(
'lg:col-span-8 lg:pt-6',

View File

@ -68,6 +68,7 @@ function ReferralsDialog(props: {
<div className="p-2 pb-1 text-xl">{user.name}</div>
<div className="p-2 pt-0 text-sm text-gray-500">@{user.username}</div>
<Tabs
className="mb-4"
tabs={[
{
title: 'Referrals',

View File

@ -9,12 +9,13 @@ import {
getUserBetContractsQuery,
listAllContracts,
trendingContractsQuery,
getContractsQuery,
} from 'web/lib/firebase/contracts'
import { QueryClient, useQueryClient } from 'react-query'
import { MINUTE_MS } from 'common/util/time'
import { QueryClient, useQuery, useQueryClient } from 'react-query'
import { MINUTE_MS, sleep } from 'common/util/time'
import { query, limit } from 'firebase/firestore'
import { Sort } from 'web/components/contract-search'
import { dailyScoreIndex } from 'web/lib/service/algolia'
import { CPMMBinaryContract } from 'common/contract'
import { zipObject } from 'lodash'
export const useContracts = () => {
const [contracts, setContracts] = useState<Contract[] | undefined>()
@ -26,6 +27,29 @@ export const useContracts = () => {
return contracts
}
export const useContractsByDailyScoreGroups = (
groupSlugs: string[] | undefined
) => {
const facetFilters = ['isResolved:false']
const { data } = useQuery(['daily-score', groupSlugs], () =>
Promise.all(
(groupSlugs ?? []).map((slug) =>
dailyScoreIndex.search<CPMMBinaryContract>('', {
facetFilters: [...facetFilters, `groupLinks.slug:${slug}`],
})
)
)
)
if (!groupSlugs || !data || data.length !== groupSlugs.length)
return undefined
return zipObject(
groupSlugs,
data.map((d) => d.hits.filter((c) => c.dailyScore))
)
}
const q = new QueryClient()
export const getCachedContracts = async () =>
q.fetchQuery(['contracts'], () => listAllContracts(1000), {
@ -40,19 +64,6 @@ export const useTrendingContracts = (maxContracts: number) => {
return result.data
}
export const useContractsQuery = (
sort: Sort,
maxContracts: number,
filters: { groupSlug?: string } = {},
visibility?: 'public'
) => {
const result = useFirestoreQueryData(
['contracts-query', sort, maxContracts, filters],
getContractsQuery(sort, maxContracts, filters, visibility)
)
return result.data
}
export const useInactiveContracts = () => {
const [contracts, setContracts] = useState<Contract[] | undefined>()
@ -75,7 +86,7 @@ export const usePrefetchUserBetContracts = (userId: string) => {
const queryClient = useQueryClient()
return queryClient.prefetchQuery(
['contracts', 'bets', userId],
() => getUserBetContracts(userId),
() => sleep(1000).then(() => getUserBetContracts(userId)),
{ staleTime: 5 * MINUTE_MS }
)
}

View File

@ -104,7 +104,7 @@ export const useMemberGroupIds = (user: User | null | undefined) => {
}
export function useMemberGroupsSubscription(user: User | null | undefined) {
const cachedGroups = useMemberGroups(user?.id) ?? []
const cachedGroups = useMemberGroups(user?.id)
const [groups, setGroups] = useState(cachedGroups)
const userId = user?.id

View File

@ -1,7 +1,13 @@
import { useWindowSize } from 'web/hooks/use-window-size'
import { useEffect, useState } from 'react'
// matches talwind sm breakpoint
export function useIsMobile() {
const { width } = useWindowSize()
return (width ?? 0) < 640
export function useIsMobile(threshold?: number) {
const [isMobile, setIsMobile] = useState<boolean>()
useEffect(() => {
// 640 matches tailwind sm breakpoint
const onResize = () => setIsMobile(window.innerWidth < (threshold ?? 640))
onResize()
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [threshold])
return isMobile
}

View File

@ -1,6 +1,6 @@
import { useQueryClient } from 'react-query'
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
import { DAY_MS, HOUR_MS, MINUTE_MS } from 'common/util/time'
import { DAY_MS, HOUR_MS, MINUTE_MS, sleep } from 'common/util/time'
import {
getPortfolioHistory,
getPortfolioHistoryQuery,
@ -17,7 +17,7 @@ export const usePrefetchPortfolioHistory = (userId: string, period: Period) => {
const cutoff = getCutoff(period)
return queryClient.prefetchQuery(
['portfolio-history', userId, cutoff],
() => getPortfolioHistory(userId, cutoff),
() => sleep(1000).then(() => getPortfolioHistory(userId, cutoff)),
{ staleTime: 15 * MINUTE_MS }
)
}

View File

@ -11,3 +11,29 @@ export const usePost = (postId: string | undefined) => {
return post
}
export const usePosts = (postIds: string[]) => {
const [posts, setPosts] = useState<Post[]>([])
useEffect(() => {
if (postIds.length === 0) return
setPosts([])
const unsubscribes = postIds.map((postId) =>
listenForPost(postId, (post) => {
if (post) {
setPosts((posts) => [...posts, post])
}
})
)
return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe())
}
}, [postIds])
return posts
.filter(
(post, index, self) => index === self.findIndex((t) => t.id === post.id)
)
.sort((a, b) => b.createdTime - a.createdTime)
}

View File

@ -1,5 +1,4 @@
import { usePrefetchUserBetContracts } from './use-contracts'
import { usePrefetchPortfolioHistory } from './use-portfolio-history'
import { usePrefetchUserBets } from './use-user-bets'
export function usePrefetch(userId: string | undefined) {
@ -7,6 +6,5 @@ export function usePrefetch(userId: string | undefined) {
return Promise.all([
usePrefetchUserBets(maybeUserId),
usePrefetchUserBetContracts(maybeUserId),
usePrefetchPortfolioHistory(maybeUserId, 'weekly'),
])
}

View File

@ -1,75 +1,47 @@
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
import { CPMMContract } from 'common/contract'
import { MINUTE_MS } from 'common/util/time'
import { useQuery, useQueryClient } from 'react-query'
import { CPMMBinaryContract } from 'common/contract'
import { sortBy, uniqBy } from 'lodash'
import { useQuery } from 'react-query'
import {
getProbChangesNegative,
getProbChangesPositive,
} from 'web/lib/firebase/contracts'
import { getValues } from 'web/lib/firebase/utils'
import { getIndexName, searchClient } from 'web/lib/service/algolia'
probChangeAscendingIndex,
probChangeDescendingIndex,
} from 'web/lib/service/algolia'
export const useProbChangesAlgolia = (userId: string) => {
const { data: positiveData } = useQuery(['prob-change-day', userId], () =>
searchClient
.initIndex(getIndexName('prob-change-day'))
.search<CPMMContract>('', {
facetFilters: ['uniqueBettorIds:' + userId, 'isResolved:false'],
})
export const useProbChanges = (
filters: { bettorId?: string; groupSlugs?: string[] } = {}
) => {
const { bettorId, groupSlugs } = filters
const bettorFilter = bettorId ? `uniqueBettorIds:${bettorId}` : ''
const groupFilters = groupSlugs
? groupSlugs.map((slug) => `groupLinks.slug:${slug}`)
: []
const facetFilters = [
'isResolved:false',
'outcomeType:BINARY',
bettorFilter,
groupFilters,
]
const searchParams = {
facetFilters,
hitsPerPage: 50,
}
const { data: positiveChanges } = useQuery(
['prob-change-day', groupSlugs],
() => probChangeDescendingIndex.search<CPMMBinaryContract>('', searchParams)
)
const { data: negativeData } = useQuery(
['prob-change-day-ascending', userId],
() =>
searchClient
.initIndex(getIndexName('prob-change-day-ascending'))
.search<CPMMContract>('', {
facetFilters: ['uniqueBettorIds:' + userId, 'isResolved:false'],
})
const { data: negativeChanges } = useQuery(
['prob-change-day-ascending', groupSlugs],
() => probChangeAscendingIndex.search<CPMMBinaryContract>('', searchParams)
)
if (!positiveData || !negativeData) {
return undefined
}
if (!positiveChanges || !negativeChanges) return undefined
return {
positiveChanges: positiveData.hits
.filter((c) => c.probChanges && c.probChanges.day > 0)
.filter((c) => c.outcomeType === 'BINARY'),
negativeChanges: negativeData.hits
.filter((c) => c.probChanges && c.probChanges.day < 0)
.filter((c) => c.outcomeType === 'BINARY'),
}
}
const hits = uniqBy(
[...positiveChanges.hits, ...negativeChanges.hits],
(c) => c.id
).filter((c) => c.probChanges)
export const useProbChanges = (userId: string) => {
const { data: positiveChanges } = useFirestoreQueryData(
['prob-changes-day-positive', userId],
getProbChangesPositive(userId)
)
const { data: negativeChanges } = useFirestoreQueryData(
['prob-changes-day-negative', userId],
getProbChangesNegative(userId)
)
if (!positiveChanges || !negativeChanges) {
return undefined
}
return { positiveChanges, negativeChanges }
}
export const usePrefetchProbChanges = (userId: string | undefined) => {
const queryClient = useQueryClient()
if (userId) {
queryClient.prefetchQuery(
['prob-changes-day-positive', userId],
() => getValues(getProbChangesPositive(userId)),
{ staleTime: MINUTE_MS }
)
queryClient.prefetchQuery(
['prob-changes-day-negative', userId],
() => getValues(getProbChangesNegative(userId)),
{ staleTime: MINUTE_MS }
)
}
return sortBy(hits, (c) => Math.abs(c.probChanges.day)).reverse()
}

View File

@ -7,13 +7,13 @@ import {
getUserBetsQuery,
listenForUserContractBets,
} from 'web/lib/firebase/bets'
import { MINUTE_MS } from 'common/util/time'
import { MINUTE_MS, sleep } from 'common/util/time'
export const usePrefetchUserBets = (userId: string) => {
const queryClient = useQueryClient()
return queryClient.prefetchQuery(
['bets', userId],
() => getUserBets(userId),
() => sleep(1000).then(() => getUserBets(userId)),
{ staleTime: MINUTE_MS }
)
}

View File

@ -90,6 +90,10 @@ export function getCurrentUser(params: any) {
return call(getFunctionUrl('getcurrentuser'), 'GET', params)
}
export function createPost(params: { title: string; content: JSONContent }) {
export function createPost(params: {
title: string
content: JSONContent
groupId?: string
}) {
return call(getFunctionUrl('createpost'), 'POST', params)
}

View File

@ -16,7 +16,7 @@ import {
import { partition, sortBy, sum, uniqBy } from 'lodash'
import { coll, getValues, listenForValue, listenForValues } from './utils'
import { BinaryContract, Contract, CPMMContract } from 'common/contract'
import { BinaryContract, Contract } from 'common/contract'
import { chooseRandomSubset } from 'common/util/random'
import { formatMoney, formatPercent } from 'common/util/format'
import { DAY_MS } from 'common/util/time'
@ -24,7 +24,6 @@ import { Bet } from 'common/bet'
import { Comment } from 'common/comment'
import { ENV_CONFIG } from 'common/envs/constants'
import { getBinaryProb } from 'common/contract-details'
import { Sort } from 'web/components/contract-search'
export const contracts = coll<Contract>('contracts')
@ -321,51 +320,6 @@ export const getTopGroupContracts = async (
return await getValues<Contract>(creatorContractsQuery)
}
const sortToField = {
newest: 'createdTime',
score: 'popularityScore',
'most-traded': 'volume',
'24-hour-vol': 'volume24Hours',
'prob-change-day': 'probChanges.day',
'last-updated': 'lastUpdated',
liquidity: 'totalLiquidity',
'close-date': 'closeTime',
'resolve-date': 'resolutionTime',
'prob-descending': 'prob',
'prob-ascending': 'prob',
} as const
const sortToDirection = {
newest: 'desc',
score: 'desc',
'most-traded': 'desc',
'24-hour-vol': 'desc',
'prob-change-day': 'desc',
'last-updated': 'desc',
liquidity: 'desc',
'close-date': 'asc',
'resolve-date': 'desc',
'prob-ascending': 'asc',
'prob-descending': 'desc',
} as const
export const getContractsQuery = (
sort: Sort,
maxItems: number,
filters: { groupSlug?: string } = {},
visibility?: 'public'
) => {
const { groupSlug } = filters
return query(
contracts,
where('isResolved', '==', false),
...(visibility ? [where('visibility', '==', visibility)] : []),
...(groupSlug ? [where('groupSlugs', 'array-contains', groupSlug)] : []),
orderBy(sortToField[sort], sortToDirection[sort]),
limit(maxItems)
)
}
export const getRecommendedContracts = async (
contract: Contract,
excludeBettorId: string,
@ -426,21 +380,3 @@ export async function getRecentBetsAndComments(contract: Contract) {
recentComments,
}
}
export const getProbChangesPositive = (userId: string) =>
query(
contracts,
where('uniqueBettorIds', 'array-contains', userId),
where('probChanges.day', '>', 0),
orderBy('probChanges.day', 'desc'),
limit(10)
) as Query<CPMMContract>
export const getProbChangesNegative = (userId: string) =>
query(
contracts,
where('uniqueBettorIds', 'array-contains', userId),
where('probChanges.day', '<', 0),
orderBy('probChanges.day', 'asc'),
limit(10)
) as Query<CPMMContract>

View File

@ -43,6 +43,7 @@ export function groupPath(
| 'about'
| typeof GROUP_CHAT_SLUG
| 'leaderboards'
| 'posts'
) {
return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}`
}

View File

@ -39,3 +39,8 @@ export function listenForPost(
) {
return listenForValue(doc(posts, postId), setPost)
}
export async function listPosts(postIds?: string[]) {
if (postIds === undefined) return []
return Promise.all(postIds.map(getPost))
}

View File

@ -13,3 +13,13 @@ export const searchIndexName =
export const getIndexName = (sort: string) => {
return `${indexPrefix}contracts-${sort}`
}
export const probChangeDescendingIndex = searchClient.initIndex(
getIndexName('prob-change-day')
)
export const probChangeAscendingIndex = searchClient.initIndex(
getIndexName('prob-change-day-ascending')
)
export const dailyScoreIndex = searchClient.initIndex(
getIndexName('daily-score')
)

View File

@ -9,9 +9,6 @@ module.exports = {
reactStrictMode: true,
optimizeFonts: false,
experimental: {
images: {
allowFutureImage: true,
},
scrollRestoration: true,
externalDir: true,
modularizeImports: {

View File

@ -23,9 +23,9 @@
"@floating-ui/react-dom-interactions": "0.9.2",
"@headlessui/react": "1.6.1",
"@heroicons/react": "1.0.5",
"@nivo/core": "0.74.0",
"@nivo/line": "0.74.0",
"@nivo/tooltip": "0.74.0",
"@nivo/core": "0.80.0",
"@nivo/line": "0.80.0",
"@nivo/tooltip": "0.80.0",
"@react-query-firebase/firestore": "0.4.2",
"@tiptap/core": "2.0.0-beta.182",
"@tiptap/extension-character-count": "2.0.0-beta.31",
@ -46,7 +46,7 @@
"gridjs-react": "5.0.2",
"lodash": "4.17.21",
"nanoid": "^3.3.4",
"next": "12.2.5",
"next": "12.3.1",
"node-fetch": "3.2.4",
"prosemirror-state": "1.4.1",
"react": "17.0.2",
@ -58,6 +58,7 @@
"react-instantsearch-hooks-web": "6.24.1",
"react-query": "3.39.0",
"react-twitter-embed": "4.0.4",
"react-masonry-css": "1.0.16",
"string-similarity": "^4.0.4",
"tippy.js": "6.3.7"
},

View File

@ -4,7 +4,7 @@ import { useEffect } from 'react'
import Head from 'next/head'
import Script from 'next/script'
import { QueryClient, QueryClientProvider } from 'react-query'
import { AuthProvider } from 'web/components/auth-context'
import { AuthProvider, AuthUser } from 'web/components/auth-context'
import Welcome from 'web/components/onboarding/welcome'
function firstLine(msg: string) {
@ -24,7 +24,10 @@ function printBuildInfo() {
}
}
function MyApp({ Component, pageProps }: AppProps) {
// specially treated props that may be present in the server/static props
type ManifoldPageProps = { auth?: AuthUser }
function MyApp({ Component, pageProps }: AppProps<ManifoldPageProps>) {
useEffect(printBuildInfo, [])
return (
@ -78,7 +81,7 @@ function MyApp({ Component, pageProps }: AppProps) {
</Head>
<AuthProvider serverUser={pageProps.auth}>
<QueryClientProvider client={queryClient}>
<Welcome {...pageProps} />
<Welcome />
<Component {...pageProps} />
</QueryClientProvider>
</AuthProvider>

View File

@ -24,14 +24,14 @@ export default function AddFundsPage() {
return (
<Page>
<SEO
title="Get Manifold Dollars"
description="Get Manifold Dollars"
title="Get Mana"
description="Buy mana to trade in your favorite markets on Manifold"
url="/add-funds"
/>
<Col className="items-center">
<Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md">
<Title className="!mt-0" text="Get Manifold Dollars" />
<Title className="!mt-0" text="Get Mana" />
<img
className="mb-6 block -scale-x-100 self-center"
src="/stylized-crane-black.png"
@ -40,8 +40,8 @@ export default function AddFundsPage() {
/>
<div className="mb-6 text-gray-500">
Purchase Manifold Dollars to trade in your favorite markets. <br />{' '}
(Not redeemable for cash.)
Buy mana (M$) to trade in your favorite markets. <br /> (Not
redeemable for cash.)
</div>
<div className="mb-2 text-sm text-gray-500">Amount</div>

View File

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

View File

@ -92,7 +92,7 @@ export default function ChallengesListPage() {
tap the button above to create a new market & challenge in one.
</p>
<Tabs tabs={[...userTab, ...publicTab]} />
<Tabs className="mb-4" tabs={[...userTab, ...publicTab]} />
</Col>
</Page>
)

View File

@ -1,93 +0,0 @@
import { useState } from 'react'
import { Spacer } from 'web/components/layout/spacer'
import { Page } from 'web/components/page'
import { Title } from 'web/components/title'
import Textarea from 'react-expanding-textarea'
import { TextEditor, useTextEditor } from 'web/components/editor'
import { createPost } from 'web/lib/firebase/api'
import clsx from 'clsx'
import Router from 'next/router'
import { MAX_POST_TITLE_LENGTH } from 'common/post'
import { postPath } from 'web/lib/firebase/posts'
export default function CreatePost() {
const [title, setTitle] = useState('')
const [error, setError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({
disabled: isSubmitting,
})
const isValid = editor && title.length > 0 && editor.isEmpty === false
async function savePost(title: string) {
if (!editor) return
const newPost = {
title: title,
content: editor.getJSON(),
}
const result = await createPost(newPost).catch((e) => {
console.log(e)
setError('There was an error creating the post, please try again')
return e
})
if (result.post) {
await Router.push(postPath(result.post.slug))
}
}
return (
<Page>
<div className="mx-auto w-full max-w-3xl">
<div className="rounded-lg px-6 py-4 sm:py-0">
<Title className="!mt-0" text="Create a post" />
<form>
<div className="form-control w-full">
<label className="label">
<span className="mb-1">
Title<span className={'text-red-700'}> *</span>
</span>
</label>
<Textarea
placeholder="e.g. Elon Mania Post"
className="input input-bordered resize-none"
autoFocus
maxLength={MAX_POST_TITLE_LENGTH}
value={title}
onChange={(e) => setTitle(e.target.value || '')}
/>
<Spacer h={6} />
<label className="label">
<span className="mb-1">
Content<span className={'text-red-700'}> *</span>
</span>
</label>
<TextEditor editor={editor} upload={upload} />
<Spacer h={6} />
<button
type="submit"
className={clsx(
'btn btn-primary normal-case',
isSubmitting && 'loading disabled'
)}
disabled={isSubmitting || !isValid || upload.isLoading}
onClick={async () => {
setIsSubmitting(true)
await savePost(title)
setIsSubmitting(false)
}}
>
{isSubmitting ? 'Creating...' : 'Create a post'}
</button>
{error !== '' && <div className="text-red-700">{error}</div>}
</div>
</form>
</div>
</div>
</Page>
)
}

View File

@ -2,14 +2,17 @@ import { ProbChangeTable } from 'web/components/contract/prob-change-table'
import { Col } from 'web/components/layout/col'
import { Page } from 'web/components/page'
import { Title } from 'web/components/title'
import { useProbChangesAlgolia } from 'web/hooks/use-prob-changes'
import { useProbChanges } from 'web/hooks/use-prob-changes'
import { useTracking } from 'web/hooks/use-tracking'
import { useUser } from 'web/hooks/use-user'
export default function DailyMovers() {
const user = useUser()
const bettorId = user?.id ?? undefined
const changes = useProbChangesAlgolia(user?.id ?? '')
const changes = useProbChanges({ bettorId })?.filter(
(c) => Math.abs(c.probChanges.day) >= 0.01
)
useTracking('view daily movers')

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { toast, Toaster } from 'react-hot-toast'
import { toast } from 'react-hot-toast'
import { Group, GROUP_CHAT_SLUG } from 'common/group'
import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts'
@ -16,7 +16,7 @@ import {
import { Row } from 'web/components/layout/row'
import { firebaseLogin, getUser, User } from 'web/lib/firebase/users'
import { Col } from 'web/components/layout/col'
import { useUser } from 'web/hooks/use-user'
import { useUser, useUserById } from 'web/hooks/use-user'
import {
useGroup,
useGroupContractIds,
@ -42,17 +42,21 @@ import { GroupComment } from 'common/comment'
import { REFERRAL_AMOUNT } from 'common/economy'
import { UserLink } from 'web/components/user-link'
import { GroupAboutPost } from 'web/components/groups/group-about-post'
import { getPost } from 'web/lib/firebase/posts'
import { getPost, listPosts, postPath } from 'web/lib/firebase/posts'
import { Post } from 'common/post'
import { Spacer } from 'web/components/layout/spacer'
import { usePost } from 'web/hooks/use-post'
import { usePost, usePosts } from 'web/hooks/use-post'
import { useAdmin } from 'web/hooks/use-admin'
import { track } from '@amplitude/analytics-browser'
import { GroupNavBar } from 'web/components/nav/group-nav-bar'
import { ArrowLeftIcon } from '@heroicons/react/solid'
import { GroupSidebar } from 'web/components/nav/group-sidebar'
import { SelectMarketsModal } from 'web/components/contract-select-modal'
import { BETTORS } from 'common/user'
import { Page } from 'web/components/page'
import { Tabs } from 'web/components/layout/tabs'
import { Avatar } from 'web/components/avatar'
import { Title } from 'web/components/title'
import { fromNow } from 'web/lib/util/time'
import { CreatePost } from 'web/components/create-post'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
@ -70,7 +74,8 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
? 'all'
: 'open'
const aboutPost =
group && group.aboutPostId != null && (await getPost(group.aboutPostId))
group && group.aboutPostId != null ? await getPost(group.aboutPostId) : null
const messages = group && (await listAllCommentsOnGroup(group.id))
const cachedTopTraderIds =
@ -83,6 +88,9 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
const creator = await creatorPromise
const posts = ((group && (await listPosts(group.postIds))) ?? []).filter(
(p) => p != null
) as Post[]
return {
props: {
group,
@ -93,6 +101,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
messages,
aboutPost,
suggestedFilter,
posts,
},
revalidate: 60, // regenerate after a minute
@ -107,17 +116,19 @@ const groupSubpages = [
'markets',
'leaderboards',
'about',
'posts',
] as const
export default function GroupPage(props: {
group: Group | null
memberIds: string[]
creator: User
creator: User | null
topTraders: { user: User; score: number }[]
topCreators: { user: User; score: number }[]
messages: GroupComment[]
aboutPost: Post
aboutPost: Post | null
suggestedFilter: 'open' | 'all'
posts: Post[]
}) {
props = usePropz(props, getStaticPropz) ?? {
group: null,
@ -127,37 +138,43 @@ export default function GroupPage(props: {
topCreators: [],
messages: [],
suggestedFilter: 'open',
posts: [],
}
const { creator, topTraders, topCreators, suggestedFilter } = props
const { creator, topTraders, topCreators, suggestedFilter, posts } = props
const router = useRouter()
const { slugs } = router.query as { slugs: string[] }
const page = slugs?.[1] as typeof groupSubpages[number]
const tabIndex = ['markets', 'leaderboard', 'about', 'posts'].indexOf(
page ?? 'markets'
)
const group = useGroup(props.group?.id) ?? props.group
const aboutPost = usePost(props.aboutPost?.id) ?? props.aboutPost
let groupPosts = usePosts(group?.postIds ?? []) ?? posts
if (aboutPost != null) {
groupPosts = [aboutPost, ...groupPosts]
}
const user = useUser()
const isAdmin = useAdmin()
const memberIds = useMemberIds(group?.id ?? null) ?? props.memberIds
// Note: Keep in sync with sidebarPages
const [sidebarIndex, setSidebarIndex] = useState(
['markets', 'leaderboards', 'about'].indexOf(page ?? 'markets')
)
useSaveReferral(user, {
defaultReferrerUsername: creator.username,
defaultReferrerUsername: creator?.username,
groupId: group?.id,
})
if (group === null || !groupSubpages.includes(page) || slugs[2]) {
if (group === null || !groupSubpages.includes(page) || slugs[2] || !creator) {
return <Custom404 />
}
const isCreator = user && group && user.id === group.creatorId
const isMember = user && memberIds.includes(user.id)
const maxLeaderboardSize = 50
const leaderboardPage = (
const leaderboardTab = (
<Col>
<div className="mt-4 flex flex-col gap-8 px-4 md:flex-row">
<GroupLeaderboard
@ -176,7 +193,17 @@ export default function GroupPage(props: {
</Col>
)
const aboutPage = (
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 aboutTab = (
<Col>
{(group.aboutPostId != null || isCreator || isAdmin) && (
<GroupAboutPost
@ -196,10 +223,14 @@ export default function GroupPage(props: {
</Col>
)
const questionsPage = (
const questionsTab = (
<>
{/* align the divs to the right */}
<div className={' flex justify-end px-2 pb-2 sm:hidden'}>
<div className={'flex justify-end '}>
<div
className={
'flex items-end justify-self-end px-2 md:absolute md:top-0 md:pb-2'
}
>
<div>
<JoinOrAddQuestionsButtons
group={group}
@ -208,6 +239,7 @@ export default function GroupPage(props: {
/>
</div>
</div>
</div>
<ContractSearch
headerClassName="md:sticky"
user={user}
@ -215,92 +247,47 @@ export default function GroupPage(props: {
defaultFilter={suggestedFilter}
additionalFilter={{ groupSlug: group.slug }}
persistPrefix={`group-${group.slug}`}
includeProbSorts
/>
</>
)
const sidebarPages = [
const tabs = [
{
title: 'Markets',
content: questionsPage,
href: groupPath(group.slug, 'markets'),
key: 'markets',
content: questionsTab,
},
{
title: 'Leaderboards',
content: leaderboardPage,
href: groupPath(group.slug, 'leaderboards'),
key: 'leaderboards',
content: leaderboardTab,
},
{
title: 'About',
content: aboutPage,
href: groupPath(group.slug, 'about'),
key: 'about',
content: aboutTab,
},
{
title: 'Posts',
content: postsPage,
},
]
const pageContent = sidebarPages[sidebarIndex].content
const onSidebarClick = (key: string) => {
const index = sidebarPages.findIndex((t) => t.key === key)
setSidebarIndex(index)
// Append the page to the URL, e.g. /group/mexifold/markets
router.replace(
{ query: { ...router.query, slugs: [group.slug, key] } },
undefined,
{ shallow: true }
)
}
const joinOrAddQuestionsButton = (
<JoinOrAddQuestionsButtons
group={group}
user={user}
isMember={!!isMember}
/>
)
return (
<>
<TopGroupNavBar
group={group}
currentPage={sidebarPages[sidebarIndex].key}
onClick={onSidebarClick}
/>
<div>
<div
className={
'mx-auto w-full pb-[58px] lg:grid lg:grid-cols-12 lg:gap-x-2 lg:pb-0 xl:max-w-7xl xl:gap-x-8'
}
>
<Toaster />
<GroupSidebar
groupName={group.name}
className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:flex"
onClick={onSidebarClick}
joinOrAddQuestionsButton={joinOrAddQuestionsButton}
currentKey={sidebarPages[sidebarIndex].key}
/>
<Page logoSubheading={group.name}>
<SEO
title={group.name}
description={`Created by ${creator.name}. ${group.about}`}
url={groupPath(group.slug)}
/>
<main className={'px-2 pt-1 lg:col-span-8 lg:pt-6 xl:col-span-8'}>
{pageContent}
</main>
<TopGroupNavBar group={group} />
<div className={'relative p-2 pt-0 md:pt-2'}>
{/* TODO: Switching tabs should also update the group path */}
<Tabs className={'mb-2'} tabs={tabs} defaultIndex={tabIndex} />
</div>
</div>
</>
</Page>
)
}
export function TopGroupNavBar(props: {
group: Group
currentPage: string
onClick: (key: string) => void
}) {
export function TopGroupNavBar(props: { group: Group }) {
return (
<header className="sticky top-0 z-50 w-full border-b border-gray-200 md:hidden lg:col-span-12">
<div className="flex items-center bg-white px-4">
@ -317,7 +304,6 @@ export function TopGroupNavBar(props: {
</h1>
</div>
</div>
<GroupNavBar currentPage={props.currentPage} onClick={props.onClick} />
</header>
)
}
@ -330,11 +316,13 @@ function JoinOrAddQuestionsButtons(props: {
}) {
const { group, user, isMember } = props
return user && isMember ? (
<Row className={'w-full self-start pt-4'}>
<Row className={'mb-2 w-full self-start md:mt-2 '}>
<AddContractButton group={group} user={user} />
</Row>
) : group.anyoneCanJoin ? (
<div className="mb-2 md:mb-0">
<JoinGroupButton group={group} user={user} />
</div>
) : null
}
@ -451,7 +439,7 @@ function GroupLeaderboard(props: {
return (
<Leaderboard
className="max-w-xl"
users={topUsers.map((t) => t.user)}
entries={topUsers.map((t) => t.user)}
title={title}
columns={[
{ header, renderCell: (user) => formatMoney(scoresByUser[user.id]) },
@ -461,6 +449,84 @@ function GroupLeaderboard(props: {
)
}
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">
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
{posts.length === 0 && (
<div className="text-center text-gray-500">No posts yet</div>
)}
</div>
</div>
)
return showCreatePost ? createPost : postList
}
function PostCard(props: { post: Post }) {
const { post } = props
const creatorId = post.creatorId
const user = useUserById(creatorId)
if (!user) return <> </>
return (
<div className="py-1">
<Link href={postPath(post.slug)}>
<Row
className={
'relative gap-3 rounded-lg bg-white p-2 shadow-md hover:cursor-pointer hover:bg-gray-100'
}
>
<div className="flex-shrink-0">
<Avatar className="h-12 w-12" username={user?.username} />
</div>
<div className="">
<div className="text-sm text-gray-500">
<UserLink
className="text-neutral"
name={user?.name}
username={user?.username}
/>
<span className="mx-1"></span>
<span className="text-gray-500">{fromNow(post.createdTime)}</span>
</div>
<div className="text-lg font-medium text-gray-900">
{post.title}
</div>
</div>
</Row>
</Link>
</div>
)
}
function AddContractButton(props: { group: Group; user: User }) {
const { group, user } = props
const [open, setOpen] = useState(false)

View File

@ -99,8 +99,35 @@ export default function Groups(props: {
</div>
<Tabs
className="mb-4"
currentPageForAnalytics={'groups'}
tabs={[
{
title: 'All',
content: (
<Col>
<input
type="text"
onChange={(e) => debouncedQuery(e.target.value)}
placeholder="Search groups"
value={query}
className="input input-bordered mb-4 w-full"
/>
<div className="flex flex-wrap justify-center gap-4">
{matchesOrderedByMostContractAndMembers.map((group) => (
<GroupCard
key={group.id}
group={group}
creator={creatorsDict[group.creatorId]}
user={user}
isMember={memberGroupIds.includes(group.id)}
/>
))}
</div>
</Col>
),
},
...(user
? [
{
@ -135,32 +162,6 @@ export default function Groups(props: {
},
]
: []),
{
title: 'All',
content: (
<Col>
<input
type="text"
onChange={(e) => debouncedQuery(e.target.value)}
placeholder="Search groups"
value={query}
className="input input-bordered mb-4 w-full"
/>
<div className="flex flex-wrap justify-center gap-4">
{matchesOrderedByMostContractAndMembers.map((group) => (
<GroupCard
key={group.id}
group={group}
creator={creatorsDict[group.creatorId]}
user={user}
isMember={memberGroupIds.includes(group.id)}
/>
))}
</div>
</Col>
),
},
]}
/>
</Col>

View File

@ -28,7 +28,7 @@ export default function Home() {
}
const groups = useMemberGroupsSubscription(user)
const { sections } = getHomeItems(groups, homeSections)
const { sections } = getHomeItems(groups ?? [], homeSections)
return (
<Page>

View File

@ -8,6 +8,7 @@ import {
import { PlusCircleIcon, XCircleIcon } from '@heroicons/react/outline'
import clsx from 'clsx'
import { toast, Toaster } from 'react-hot-toast'
import { Dictionary } from 'lodash'
import { Page } from 'web/components/page'
import { Col } from 'web/components/layout/col'
@ -31,11 +32,10 @@ import { ProbChangeTable } from 'web/components/contract/prob-change-table'
import { groupPath, joinGroup, leaveGroup } from 'web/lib/firebase/groups'
import { usePortfolioHistory } from 'web/hooks/use-portfolio-history'
import { formatMoney } from 'common/util/format'
import { useProbChangesAlgolia } from 'web/hooks/use-prob-changes'
import { useProbChanges } from 'web/hooks/use-prob-changes'
import { ProfitBadge } from 'web/components/bets-list'
import { calculatePortfolioProfit } from 'common/calculate-metrics'
import { hasCompletedStreakToday } from 'web/components/profile/betting-streak-modal'
import { useContractsQuery } from 'web/hooks/use-contracts'
import { ContractsGrid } from 'web/components/contract/contracts-grid'
import { PillButton } from 'web/components/buttons/pill-button'
import { filterDefined } from 'common/util/array'
@ -43,6 +43,9 @@ import { updateUser } from 'web/lib/firebase/users'
import { isArray, keyBy } from 'lodash'
import { usePrefetch } from 'web/hooks/use-prefetch'
import { Title } from 'web/components/title'
import { CPMMBinaryContract } from 'common/contract'
import { useContractsByDailyScoreGroups } from 'web/hooks/use-contracts'
import { LoadingIndicator } from 'web/components/loading-indicator'
export default function Home() {
const user = useUser()
@ -52,22 +55,28 @@ export default function Home() {
useSaveReferral()
usePrefetch(user?.id)
useEffect(() => {
if (user === null) {
// Go to landing page if not logged in.
Router.push('/')
}
})
const groups = useMemberGroupsSubscription(user)
const { sections } = getHomeItems(groups, user?.homeSections ?? [])
const { sections } = getHomeItems(groups ?? [], user?.homeSections ?? [])
useEffect(() => {
if (
user &&
!user.homeSections &&
sections.length > 0 &&
groups.length > 0
) {
if (user && !user.homeSections && sections.length > 0 && groups) {
// Save initial home sections.
updateUser(user.id, { homeSections: sections.map((s) => s.id) })
}
}, [user, sections, groups])
const groupContracts = useContractsByDailyScoreGroups(
groups?.map((g) => g.slug)
)
return (
<Page>
<Toaster />
@ -81,9 +90,17 @@ export default function Home() {
<DailyStats user={user} />
</Row>
{sections.map((section) => renderSection(section, user, groups))}
{!user ? (
<LoadingIndicator />
) : (
<>
{sections.map((section) =>
renderSection(section, user, groups, groupContracts)
)}
<TrendingGroupsSection user={user} />
</>
)}
</Col>
<button
type="button"
@ -101,9 +118,9 @@ export default function Home() {
const HOME_SECTIONS = [
{ label: 'Daily movers', id: 'daily-movers' },
{ label: 'Daily trending', id: 'daily-trending' },
{ label: 'Trending', id: 'score' },
{ label: 'New', id: 'newest' },
{ label: 'Recently updated', id: 'recently-updated-for-you' },
]
export const getHomeItems = (groups: Group[], sections: string[]) => {
@ -122,6 +139,10 @@ export const getHomeItems = (groups: Group[], sections: string[]) => {
const sectionItems = filterDefined(sections.map((id) => itemsById[id]))
// Add new home section items to the top.
sectionItems.unshift(
...HOME_SECTIONS.filter((item) => !sectionItems.includes(item))
)
// Add unmentioned items to the end.
sectionItems.push(...items.filter((item) => !sectionItems.includes(item)))
@ -133,19 +154,20 @@ export const getHomeItems = (groups: Group[], sections: string[]) => {
function renderSection(
section: { id: string; label: string },
user: User | null | undefined,
groups: Group[]
user: User,
groups: Group[] | undefined,
groupContracts: Dictionary<CPMMBinaryContract[]> | undefined
) {
const { id, label } = section
if (id === 'daily-movers') {
return <DailyMoversSection key={id} userId={user?.id} />
return <DailyMoversSection key={id} userId={user.id} />
}
if (id === 'recently-updated-for-you')
if (id === 'daily-trending')
return (
<SearchSection
key={id}
label={label}
sort={'last-updated'}
sort={'daily-score'}
pill="personal"
user={user}
/>
@ -156,8 +178,23 @@ function renderSection(
<SearchSection key={id} label={label} sort={sort.value} user={user} />
)
if (groups && groupContracts) {
const group = groups.find((g) => g.id === id)
if (group) return <GroupSection key={id} group={group} user={user} />
if (group) {
const contracts = groupContracts[group.slug].filter(
(c) => Math.abs(c.probChanges.day) >= 0.01
)
if (contracts.length === 0) return null
return (
<GroupSection
key={id}
group={group}
user={user}
contracts={contracts}
/>
)
}
}
return null
}
@ -189,7 +226,7 @@ function SectionHeader(props: {
function SearchSection(props: {
label: string
user: User | null | undefined | undefined
user: User
sort: Sort
pill?: string
}) {
@ -207,7 +244,6 @@ function SearchSection(props: {
defaultPill={pill}
noControls
maxResults={6}
headerClassName="sticky"
persistPrefix={`home-${sort}`}
/>
</Col>
@ -216,11 +252,10 @@ function SearchSection(props: {
function GroupSection(props: {
group: Group
user: User | null | undefined | undefined
user: User
contracts: CPMMBinaryContract[]
}) {
const { group, user } = props
const contracts = useContractsQuery('score', 4, { groupSlug: group.slug })
const { group, user, contracts } = props
return (
<Col>
@ -228,7 +263,6 @@ function GroupSection(props: {
<Button
color="gray-white"
onClick={() => {
if (user) {
const homeSections = (user.homeSections ?? []).filter(
(id) => id !== group.id
)
@ -239,27 +273,26 @@ function GroupSection(props: {
success: `Unfollowed ${group.name}`,
error: "Couldn't unfollow group, try again?",
})
}
}}
>
<XCircleIcon className={'h-5 w-5 flex-shrink-0'} aria-hidden="true" />
</Button>
</SectionHeader>
<ContractsGrid contracts={contracts} />
<ContractsGrid
contracts={contracts.slice(0, 4)}
cardUIOptions={{ showProbChange: true }}
/>
</Col>
)
}
function DailyMoversSection(props: { userId: string | null | undefined }) {
const { userId } = props
const changes = useProbChangesAlgolia(userId ?? '')
if (changes) {
const { positiveChanges, negativeChanges } = changes
if (
!positiveChanges.find((c) => c.probChanges.day >= 0.01) ||
!negativeChanges.find((c) => c.probChanges.day <= -0.01)
const changes = useProbChanges({ bettorId: userId ?? undefined })?.filter(
(c) => Math.abs(c.probChanges.day) >= 0.01
)
if (changes && changes.length === 0) {
return null
}
@ -332,6 +365,10 @@ export function TrendingGroupsSection(props: {
const count = full ? 100 : 25
const chosenGroups = groups.slice(0, count)
if (chosenGroups.length === 0) {
return null
}
return (
<Col className={className}>
<SectionHeader label="Trending groups" href="/explore-groups">

View File

@ -81,7 +81,7 @@ export default function Leaderboards(_props: {
<Col className="mx-4 items-center gap-10 lg:flex-row">
<Leaderboard
title={`🏅 Top ${BETTORS}`}
users={topTraders}
entries={topTraders}
columns={[
{
header: 'Total profit',
@ -92,7 +92,7 @@ export default function Leaderboards(_props: {
<Leaderboard
title="🏅 Top creators"
users={topCreators}
entries={topCreators}
columns={[
{
header: 'Total bet',
@ -106,7 +106,7 @@ export default function Leaderboards(_props: {
<Col className="mx-4 my-10 items-center gap-10 lg:mx-0 lg:w-1/2 lg:flex-row">
<Leaderboard
title="🏅 Top followed"
users={topFollowed}
entries={topFollowed}
columns={[
{
header: 'Total followers',
@ -132,6 +132,7 @@ export default function Leaderboards(_props: {
/>
<Title text={'Leaderboards'} className={'hidden md:block'} />
<Tabs
className="mb-4"
currentPageForAnalytics={'leaderboards'}
defaultIndex={1}
tabs={[

View File

@ -971,6 +971,8 @@ function ContractResolvedNotification(props: {
const { sourceText, data } = notification
const { userInvestment, userPayout } = (data as ContractResolutionData) ?? {}
const subtitle = 'resolved the market'
const profitable = userPayout >= userInvestment
const ROI = (userPayout - userInvestment) / userInvestment
const resolutionDescription = () => {
if (!sourceText) return <div />
@ -1002,23 +1004,21 @@ function ContractResolvedNotification(props: {
const description =
userInvestment && userPayout !== undefined ? (
<Row className={'gap-1 '}>
Resolved: {resolutionDescription()}
Invested:
<>
Resolved: {resolutionDescription()} Invested:
<span className={'text-primary'}>{formatMoney(userInvestment)} </span>
Payout:
<span
className={clsx(
userPayout > 0 ? 'text-primary' : 'text-red-500',
'truncate'
profitable ? 'text-primary' : 'text-red-500',
'truncate text-ellipsis'
)}
>
{formatMoney(userPayout)}
{` (${userPayout > 0 ? '+' : ''}${Math.round(
((userPayout - userInvestment) / userInvestment) * 100
)}%)`}
{userPayout > 0 &&
` (${profitable ? '+' : ''}${Math.round(ROI * 100)}%)`}
</span>
</Row>
</>
) : (
<span>Resolved {resolutionDescription()}</span>
)
@ -1038,9 +1038,7 @@ function ContractResolvedNotification(props: {
highlighted={highlighted}
subtitle={subtitle}
>
<Row>
<span>{description}</span>
</Row>
<Row className={'line-clamp-2 space-x-1'}>{description}</Row>
</NotificationFrame>
)
}

View File

@ -26,6 +26,7 @@ export default function Analytics() {
return (
<Page>
<Tabs
className="mb-4"
currentPageForAnalytics={'stats'}
tabs={[
{
@ -89,6 +90,7 @@ export function CustomAnalytics(props: Stats) {
<Spacer h={4} />
<Tabs
className="mb-4"
defaultIndex={1}
tabs={[
{
@ -141,6 +143,7 @@ export function CustomAnalytics(props: Stats) {
period?
</p>
<Tabs
className="mb-4"
defaultIndex={1}
tabs={[
{
@ -198,6 +201,7 @@ export function CustomAnalytics(props: Stats) {
<Spacer h={4} />
<Tabs
className="mb-4"
defaultIndex={2}
tabs={[
{
@ -239,6 +243,7 @@ export function CustomAnalytics(props: Stats) {
<Title text="Daily activity" />
<Tabs
className="mb-4"
defaultIndex={0}
tabs={[
{
@ -293,6 +298,7 @@ export function CustomAnalytics(props: Stats) {
<Spacer h={4} />
<Tabs
className="mb-4"
defaultIndex={1}
tabs={[
{
@ -323,6 +329,7 @@ export function CustomAnalytics(props: Stats) {
<Title text="Ratio of Active Users" />
<Tabs
className="mb-4"
defaultIndex={1}
tabs={[
{
@ -367,6 +374,7 @@ export function CustomAnalytics(props: Stats) {
Sum of bet amounts. (Divided by 100 to be more readable.)
</p>
<Tabs
className="mb-4"
defaultIndex={1}
tabs={[
{

View File

@ -107,7 +107,14 @@ const tourneys: Tourney[] = [
groupId: 'SxGRqXRpV3RAQKudbcNb',
},
// Tournaments without awards get featured belows
// Tournaments without awards get featured below
{
title: 'Criticism and Red Teaming Contest',
blurb:
'Which criticisms of Effective Altruism have been the most valuable?',
endTime: toDate('Sep 30, 2022'),
groupId: 'K86LmEmidMKdyCHdHNv4',
},
{
title: 'SF 2022 Ballot',
blurb: 'Which ballot initiatives will pass this year in SF and CA?',

View File

@ -92,7 +92,7 @@ export function PostCommentInput(props: {
return (
<CommentInput
replyToUser={replyToUser}
replyTo={replyToUser}
parentCommentId={parentCommentId}
onSubmitComment={onSubmitComment}
/>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

View File

@ -15,9 +15,6 @@ module.exports = {
}
),
extend: {
backgroundImage: {
'world-trading': "url('/world-trading-background.webp')",
},
colors: {
'greyscale-1': '#FBFBFF',
'greyscale-2': '#E7E7F4',

569
yarn.lock
View File

@ -2476,10 +2476,10 @@
resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b"
integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==
"@next/env@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.5.tgz#d908c57b35262b94db3e431e869b72ac3e1ad3e3"
integrity sha512-vLPLV3cpPGjUPT3PjgRj7e3nio9t6USkuew3JE/jMeon/9Mvp1WyR18v3iwnCuX7eUAm1HmAbJHHLAbcu/EJcw==
"@next/env@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.1.tgz#18266bd92de3b4aa4037b1927aa59e6f11879260"
integrity sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg==
"@next/eslint-plugin-next@12.1.6":
version "12.1.6"
@ -2488,167 +2488,164 @@
dependencies:
glob "7.1.7"
"@next/swc-android-arm-eabi@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.5.tgz#903a5479ab4c2705d9c08d080907475f7bacf94d"
integrity sha512-cPWClKxGhgn2dLWnspW+7psl3MoLQUcNqJqOHk2BhNcou9ARDtC0IjQkKe5qcn9qg7I7U83Gp1yh2aesZfZJMA==
"@next/swc-android-arm-eabi@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz#b15ce8ad376102a3b8c0f3c017dde050a22bb1a3"
integrity sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ==
"@next/swc-android-arm64@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.5.tgz#2f9a98ec4166c7860510963b31bda1f57a77c792"
integrity sha512-vMj0efliXmC5b7p+wfcQCX0AfU8IypjkzT64GiKJD9PgiA3IILNiGJr1fw2lyUDHkjeWx/5HMlMEpLnTsQslwg==
"@next/swc-android-arm64@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz#85d205f568a790a137cb3c3f720d961a2436ac9c"
integrity sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q==
"@next/swc-darwin-arm64@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.5.tgz#31b1c3c659d54be546120c488a1e1bad21c24a1d"
integrity sha512-VOPWbO5EFr6snla/WcxUKtvzGVShfs302TEMOtzYyWni6f9zuOetijJvVh9CCTzInnXAZMtHyNhefijA4HMYLg==
"@next/swc-darwin-arm64@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.1.tgz#b105457d6760a7916b27e46c97cb1a40547114ae"
integrity sha512-hT/EBGNcu0ITiuWDYU9ur57Oa4LybD5DOQp4f22T6zLfpoBMfBibPtR8XktXmOyFHrL/6FC2p9ojdLZhWhvBHg==
"@next/swc-darwin-x64@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.5.tgz#2e44dd82b2b7fef88238d1bc4d3bead5884cedfd"
integrity sha512-5o8bTCgAmtYOgauO/Xd27vW52G2/m3i5PX7MUYePquxXAnX73AAtqA3WgPXBRitEB60plSKZgOTkcpqrsh546A==
"@next/swc-darwin-x64@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz#6947b39082271378896b095b6696a7791c6e32b1"
integrity sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA==
"@next/swc-freebsd-x64@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.5.tgz#e24e75d8c2581bfebc75e4f08f6ddbd116ce9dbd"
integrity sha512-yYUbyup1JnznMtEBRkK4LT56N0lfK5qNTzr6/DEyDw5TbFVwnuy2hhLBzwCBkScFVjpFdfiC6SQAX3FrAZzuuw==
"@next/swc-freebsd-x64@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz#2b6c36a4d84aae8b0ea0e0da9bafc696ae27085a"
integrity sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q==
"@next/swc-linux-arm-gnueabihf@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.5.tgz#46d8c514d834d2b5f67086013f0bd5e3081e10b9"
integrity sha512-2ZE2/G921Acks7UopJZVMgKLdm4vN4U0yuzvAMJ6KBavPzqESA2yHJlm85TV/K9gIjKhSk5BVtauIUntFRP8cg==
"@next/swc-linux-arm-gnueabihf@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz#6e421c44285cfedac1f4631d5de330dd60b86298"
integrity sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w==
"@next/swc-linux-arm64-gnu@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.5.tgz#91f725ac217d3a1f4f9f53b553615ba582fd3d9f"
integrity sha512-/I6+PWVlz2wkTdWqhlSYYJ1pWWgUVva6SgX353oqTh8njNQp1SdFQuWDqk8LnM6ulheVfSsgkDzxrDaAQZnzjQ==
"@next/swc-linux-arm64-gnu@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz#8863f08a81f422f910af126159d2cbb9552ef717"
integrity sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ==
"@next/swc-linux-arm64-musl@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.5.tgz#e627e8c867920995810250303cd9b8e963598383"
integrity sha512-LPQRelfX6asXyVr59p5sTpx5l+0yh2Vjp/R8Wi4X9pnqcayqT4CUJLiHqCvZuLin3IsFdisJL0rKHMoaZLRfmg==
"@next/swc-linux-arm64-musl@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz#0038f07cf0b259d70ae0c80890d826dfc775d9f3"
integrity sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg==
"@next/swc-linux-x64-gnu@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.5.tgz#83a5e224fbc4d119ef2e0f29d0d79c40cc43887e"
integrity sha512-0szyAo8jMCClkjNK0hknjhmAngUppoRekW6OAezbEYwHXN/VNtsXbfzgYOqjKWxEx3OoAzrT3jLwAF0HdX2MEw==
"@next/swc-linux-x64-gnu@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz#c66468f5e8181ffb096c537f0dbfb589baa6a9c1"
integrity sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA==
"@next/swc-linux-x64-musl@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.5.tgz#be700d48471baac1ec2e9539396625584a317e95"
integrity sha512-zg/Y6oBar1yVnW6Il1I/08/2ukWtOG6s3acdJdEyIdsCzyQi4RLxbbhkD/EGQyhqBvd3QrC6ZXQEXighQUAZ0g==
"@next/swc-linux-x64-musl@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz#c6269f3e96ac0395bc722ad97ce410ea5101d305"
integrity sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg==
"@next/swc-win32-arm64-msvc@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.5.tgz#a93e958133ad3310373fda33a79aa10af2a0aa97"
integrity sha512-3/90DRNSqeeSRMMEhj4gHHQlLhhKg5SCCoYfE3kBjGpE63EfnblYUqsszGGZ9ekpKL/R4/SGB40iCQr8tR5Jiw==
"@next/swc-win32-arm64-msvc@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz#83c639ee969cee36ce247c3abd1d9df97b5ecade"
integrity sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw==
"@next/swc-win32-ia32-msvc@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.5.tgz#4f5f7ba0a98ff89a883625d4af0125baed8b2e19"
integrity sha512-hGLc0ZRAwnaPL4ulwpp4D2RxmkHQLuI8CFOEEHdzZpS63/hMVzv81g8jzYA0UXbb9pus/iTc3VRbVbAM03SRrw==
"@next/swc-win32-ia32-msvc@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz#52995748b92aa8ad053440301bc2c0d9fbcf27c2"
integrity sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA==
"@next/swc-win32-x64-msvc@12.2.5":
version "12.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.5.tgz#20fed129b04a0d3f632c6d0de135345bb623b1e4"
integrity sha512-7h5/ahY7NeaO2xygqVrSG/Y8Vs4cdjxIjowTZ5W6CKoTKn7tmnuxlUc2h74x06FKmbhAd9agOjr/AOKyxYYm9Q==
"@next/swc-win32-x64-msvc@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz#27d71a95247a9eaee03d47adee7e3bd594514136"
integrity sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA==
"@nivo/annotations@0.74.0":
version "0.74.0"
resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.74.0.tgz#f4a3474fdf8812c3812c30e08d3277e209bec0f6"
integrity sha512-nxZLKDi9YEy2zZUsOtbYL/2oAgsxK5SVZ1P3Csll+cQ96uLU6sU7jmb67AwK0nDbYk7BD3sZf/O/A9r/MCK4Ow==
"@nivo/annotations@0.80.0":
version "0.80.0"
resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.80.0.tgz#127e4801fff7370dcfb9acfe1e335781dd65cfd5"
integrity sha512-bC9z0CLjU07LULTMWsqpjovRtHxP7n8oJjqBQBLmHOGB4IfiLbrryBfu9+aEZH3VN2jXHhdpWUz+HxeZzOzsLg==
dependencies:
"@nivo/colors" "0.74.0"
"@react-spring/web" "9.2.6"
"@nivo/colors" "0.80.0"
"@react-spring/web" "9.4.5"
lodash "^4.17.21"
"@nivo/axes@0.74.0":
version "0.74.0"
resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.74.0.tgz#cf7cf2277b7aca5449a040ddf3e0cf9891971199"
integrity sha512-27o1H+Br0AaeUTiRhy7OebqzYEWr1xznHOxd+Hn2Xz9kK1alGBiPgwXrkXV0Q9CtrsroQFnX2QR3JxRgOtC5fA==
"@nivo/axes@0.80.0":
version "0.80.0"
resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.80.0.tgz#22788855ddc45bb6a619dcd03d62d4bd8c0fc35f"
integrity sha512-AsUyaSHGwQVSEK8QXpsn8X+poZxvakLMYW7crKY1xTGPNw+SU4SSBohPVumm2jMH3fTSLNxLhAjWo71GBJXfdA==
dependencies:
"@nivo/scales" "0.74.0"
"@react-spring/web" "9.2.6"
"@nivo/scales" "0.80.0"
"@react-spring/web" "9.4.5"
d3-format "^1.4.4"
d3-time-format "^3.0.0"
"@nivo/colors@0.74.0":
version "0.74.0"
resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.74.0.tgz#29d1e7c6f3bcab4e872a168651b3a90cfba03a4f"
integrity sha512-5ClckmBm3x2XdJqHMylr6erY+scEL/twoGVfyXak/L+AIhL+Gf9PQxyxyfl3Lbtc3SPeAQe0ZAO1+VrmTn7qlA==
"@nivo/colors@0.80.0":
version "0.80.0"
resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.80.0.tgz#5b70b4979df246d9d0d69fb638bba9764dd88b52"
integrity sha512-T695Zr411FU4RPo7WDINOAn8f79DPP10SFJmDdEqELE+cbzYVTpXqLGZ7JMv88ko7EOf9qxLQgcBqY69rp9tHQ==
dependencies:
d3-color "^2.0.0"
d3-scale "^3.2.3"
d3-scale-chromatic "^2.0.0"
lodash "^4.17.21"
react-motion "^0.5.2"
"@nivo/core@0.74.0":
version "0.74.0"
resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.74.0.tgz#7634c78a36a8bd50a0c04c6b6f12b779a88ec2f4"
integrity sha512-LZ3kN1PiEW0KU4PTBgaHFO757amyKZkEL4mKdAzvyNQtpq5idB3OhC/sYrBxhJaLqYcX19MgNfhIel/0KygHAg==
"@nivo/core@0.80.0":
version "0.80.0"
resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.80.0.tgz#d180cb2622158eb7bc5f984131ff07984f12297e"
integrity sha512-6caih0RavXdWWSfde+rC2pk17WrX9YQlqK26BrxIdXzv3Ydzlh5SkrC7dR2TEvMGBhunzVeLOfiC2DWT1S8CFg==
dependencies:
"@nivo/recompose" "0.74.0"
"@react-spring/web" "9.2.6"
"@nivo/recompose" "0.80.0"
"@react-spring/web" "9.4.5"
d3-color "^2.0.0"
d3-format "^1.4.4"
d3-hierarchy "^1.1.8"
d3-interpolate "^2.0.1"
d3-scale "^3.2.3"
d3-scale-chromatic "^2.0.0"
d3-shape "^1.3.5"
d3-time-format "^3.0.0"
lodash "^4.17.21"
resize-observer-polyfill "^1.5.1"
"@nivo/legends@0.74.0":
version "0.74.0"
resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.74.0.tgz#8e5e04b2a3f980c2073a394d94c4d89fa8bc8724"
integrity sha512-Bfk392ngre1C8UaGoymwqK0acjjzuk0cglUSNsr0z8BAUQIVGUPthtfcxbq/yUYGJL/cxWky2QKxi9r3C0FbmA==
"@nivo/legends@0.80.0":
version "0.80.0"
resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.80.0.tgz#49edc54000075b4df055f86794a8c32810269d06"
integrity sha512-h0IUIPGygpbKIZZZWIxkkxOw4SO0rqPrqDrykjaoQz4CvL4HtLIUS3YRA4akKOVNZfS5agmImjzvIe0s3RvqlQ==
"@nivo/line@0.74.0":
version "0.74.0"
resolved "https://registry.yarnpkg.com/@nivo/line/-/line-0.74.0.tgz#f1f430d64a81d2fe1a5fd49e5cfaa61242066927"
integrity sha512-uJssLII1UTfxrZkPrkki054LFUpSKeqS35ttwK6VLvyqs5r3SrSXn223vDRNaaxuop5oT/L3APUJQwQDqUcj3w==
"@nivo/line@0.80.0":
version "0.80.0"
resolved "https://registry.yarnpkg.com/@nivo/line/-/line-0.80.0.tgz#ba541b0fcfd53b3a7ce865feb43c993b7cf4a7d4"
integrity sha512-6UAD/y74qq3DDRnVb+QUPvXYojxMtwXMipGSNvCGk8omv1QZNTaUrbV+eQacvn9yh//a0yZcWipnpq0tGJyJCA==
dependencies:
"@nivo/annotations" "0.74.0"
"@nivo/axes" "0.74.0"
"@nivo/colors" "0.74.0"
"@nivo/legends" "0.74.0"
"@nivo/scales" "0.74.0"
"@nivo/tooltip" "0.74.0"
"@nivo/voronoi" "0.74.0"
"@react-spring/web" "9.2.6"
"@nivo/annotations" "0.80.0"
"@nivo/axes" "0.80.0"
"@nivo/colors" "0.80.0"
"@nivo/legends" "0.80.0"
"@nivo/scales" "0.80.0"
"@nivo/tooltip" "0.80.0"
"@nivo/voronoi" "0.80.0"
"@react-spring/web" "9.4.5"
d3-shape "^1.3.5"
"@nivo/recompose@0.74.0":
version "0.74.0"
resolved "https://registry.yarnpkg.com/@nivo/recompose/-/recompose-0.74.0.tgz#057e8e1154073d7f4cb01aa8d165c3914b8bdb54"
integrity sha512-qC9gzGvDIxocrJoozDjqqffOwDpuEZijeMV59KExnztCwIpQbIYVBsDdpvL+tXfWausigSlnGILGfereXJTLUQ==
"@nivo/recompose@0.80.0":
version "0.80.0"
resolved "https://registry.yarnpkg.com/@nivo/recompose/-/recompose-0.80.0.tgz#572048aed793321a0bada1fd176b72df5a25282e"
integrity sha512-iL3g7j3nJGD9+mRDbwNwt/IXDXH6E29mhShY1I7SP91xrfusZV9pSFf4EzyYgruNJk/2iqMuaqn+e+TVFra44A==
dependencies:
react-lifecycles-compat "^3.0.4"
"@nivo/scales@0.74.0":
version "0.74.0"
resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.74.0.tgz#ede12b899da9e3aee7921ebce40f227e670a430d"
integrity sha512-5mER71NgZGdgs8X2PgilBpAWMMGtTXrUuYOBQWDKDMgtc83MU+mphhiYfLv5e6ViZyUB5ebfEkfeIgStLqrcEA==
"@nivo/scales@0.80.0":
version "0.80.0"
resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.80.0.tgz#39313fb97c8ae9633c2aa1e17adb57cb851e8a50"
integrity sha512-4y2pQdCg+f3n4TKXC2tYuq71veZM+xPRQbOTgGYJpuBvMc7pQsXF9T5z7ryeIG9hkpXkrlyjecU6XcAG7tLSNg==
dependencies:
d3-scale "^3.2.3"
d3-time "^1.0.11"
d3-time-format "^3.0.0"
lodash "^4.17.21"
"@nivo/tooltip@0.74.0":
version "0.74.0"
resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.74.0.tgz#60d94b0fecc2fc179ada3efa380e7e456982b4a5"
integrity sha512-h3PUgNFF5HUeQFfx19MWS1uGK8iUDymZNY+5PyaCWDFT+0/ldXBu8uw5WYRui2KwNdTym6F0E/aT7JKczDd85w==
"@nivo/tooltip@0.80.0":
version "0.80.0"
resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.80.0.tgz#07ebef47eb708a0612bd6297d5ad156bbec19d34"
integrity sha512-qGmrreRwnCsYjn/LAuwBtxBn/tvG8y+rwgd4gkANLBAoXd3bzJyvmkSe+QJPhUG64bq57ibDK+lO2pC48a3/fw==
dependencies:
"@react-spring/web" "9.2.6"
"@react-spring/web" "9.4.5"
"@nivo/voronoi@0.74.0":
version "0.74.0"
resolved "https://registry.yarnpkg.com/@nivo/voronoi/-/voronoi-0.74.0.tgz#4b427955ddabd86934a2bbb95a62ff53ee97c575"
integrity sha512-Q3267T1+Tlufn8LbmSYnO8x9gL+h/iwH2Uqc5CENHSZu2KPD0PB82vxpQnbDVhjadulI0rlrPA9fU3VY3q1zKg==
"@nivo/voronoi@0.80.0":
version "0.80.0"
resolved "https://registry.yarnpkg.com/@nivo/voronoi/-/voronoi-0.80.0.tgz#59cc7ed253dc1a5bbcf614a5ac37d2468d561599"
integrity sha512-zaJV3I3cRu1gHpsXCIEvp6GGlGY8P7D9CwAVCjYDGrz3W/+GKN0kA7qGyHTC97zVxJtfefxSPlP/GtOdxac+qw==
dependencies:
d3-delaunay "^5.3.0"
d3-scale "^3.2.3"
@ -2747,50 +2744,51 @@
resolved "https://registry.yarnpkg.com/@react-query-firebase/firestore/-/firestore-0.4.2.tgz#6ae52768715aa0a5c0d903dd4fd953ed417ba635"
integrity sha512-7eYp905+sfBRcBTdj7W7BAc3bI3V0D0kKca4/juOTnN4gyoNyaCNOCjLPY467dTq325hGs7BX0ol7Pw3JENdHA==
"@react-spring/animated@~9.2.6-beta.0":
version "9.2.6"
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.6.tgz#58f30fb75d8bfb7ccbc156cfd6b974a8f3dfd54e"
integrity sha512-xjL6nmixYNDvnpTs1FFMsMfSC0tURwPCU3b2jWNriYGLfwZ7c/TcyaEZA7yiNnmdFnuR3f3Z27AqIgaFC083Cw==
"@react-spring/animated@~9.4.5":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.4.5.tgz#dd9921c716a4f4a3ed29491e0c0c9f8ca0eb1a54"
integrity sha512-KWqrtvJSMx6Fj9nMJkhTwM9r6LIriExDRV6YHZV9HKQsaolUFppgkOXpC+rsL1JEtEvKv6EkLLmSqHTnuYjiIA==
dependencies:
"@react-spring/shared" "~9.2.6-beta.0"
"@react-spring/types" "~9.2.6-beta.0"
"@react-spring/shared" "~9.4.5"
"@react-spring/types" "~9.4.5"
"@react-spring/core@~9.2.6-beta.0":
version "9.2.6"
resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.2.6.tgz#ae22338fe55d070caf03abb4293b5519ba620d93"
integrity sha512-uPHUxmu+w6mHJrfQTMtmGJ8iZEwiVxz9kH7dRyk69bkZJt9z+w0Oj3UF4J3VcECZsbm3HRhN2ogXSAaqGjwhQw==
"@react-spring/core@~9.4.5":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.4.5.tgz#4616e1adc18dd10f5731f100ebdbe9518b89ba3c"
integrity sha512-83u3FzfQmGMJFwZLAJSwF24/ZJctwUkWtyPD7KYtNagrFeQKUH1I05ZuhmCmqW+2w1KDW1SFWQ43RawqfXKiiQ==
dependencies:
"@react-spring/animated" "~9.2.6-beta.0"
"@react-spring/shared" "~9.2.6-beta.0"
"@react-spring/types" "~9.2.6-beta.0"
"@react-spring/animated" "~9.4.5"
"@react-spring/rafz" "~9.4.5"
"@react-spring/shared" "~9.4.5"
"@react-spring/types" "~9.4.5"
"@react-spring/rafz@~9.2.6-beta.0":
version "9.2.6"
resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.2.6.tgz#d97484003875bf5fb5e6ec22dee97cc208363e48"
integrity sha512-62SivLKEpo7EfHPkxO5J3g9Cr9LF6+1A1RVOMJhkcpEYtbdbmma/d63Xp8qpMPEpk7uuWxaTb6jjyxW33pW3sg==
"@react-spring/rafz@~9.4.5":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.4.5.tgz#84f809f287f2a66bbfbc66195db340482f886bd7"
integrity sha512-swGsutMwvnoyTRxvqhfJBtGM8Ipx6ks0RkIpNX9F/U7XmyPvBMGd3GgX/mqxZUpdlsuI1zr/jiYw+GXZxAlLcQ==
"@react-spring/shared@~9.2.6-beta.0":
version "9.2.6"
resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.2.6.tgz#2c84e62cc0cfbbbbeb5546acd46c1f4b248bc562"
integrity sha512-Qrm9fopKG/RxZ3Rw+4euhrpnB3uXSyiON9skHbcBfmkkzagpkUR66MX1YLrhHw0UchcZuSDnXs0Lonzt1rpWag==
"@react-spring/shared@~9.4.5":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.4.5.tgz#4c3ad817bca547984fb1539204d752a412a6d829"
integrity sha512-JhMh3nFKsqyag0KM5IIM8BQANGscTdd0mMv3BXsUiMZrcjQTskyfnv5qxEeGWbJGGar52qr5kHuBHtCjQOzniA==
dependencies:
"@react-spring/rafz" "~9.2.6-beta.0"
"@react-spring/types" "~9.2.6-beta.0"
"@react-spring/rafz" "~9.4.5"
"@react-spring/types" "~9.4.5"
"@react-spring/types@~9.2.6-beta.0":
version "9.2.6"
resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.2.6.tgz#f60722fcf9f8492ae16d0bdc47f0ea3c2a16d2cf"
integrity sha512-l7mCw182DtDMnCI8CB9orgTAEoFZRtdQ6aS6YeEAqYcy3nQZPmPggIHH9DxyLw7n7vBPRSzu9gCvUMgXKpTflg==
"@react-spring/types@~9.4.5":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.4.5.tgz#9c71e5ff866b5484a7ef3db822bf6c10e77bdd8c"
integrity sha512-mpRIamoHwql0ogxEUh9yr4TP0xU5CWyZxVQeccGkHHF8kPMErtDXJlxyo0lj+telRF35XNihtPTWoflqtyARmg==
"@react-spring/web@9.2.6":
version "9.2.6"
resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.2.6.tgz#c4fba69e1b1b43bd1d6a62346530cfb07f2be09b"
integrity sha512-0HkRsEYR/CO3Uw46FWDWaF2wg2rUXcWE2R9AoZXthEYLUn5w9uE1mf2Jel7BxBxWGQ73owkqSQv+klA1Hb+ViQ==
"@react-spring/web@9.4.5":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.4.5.tgz#b92f05b87cdc0963a59ee149e677dcaff09f680e"
integrity sha512-NGAkOtKmOzDEctL7MzRlQGv24sRce++0xAY7KlcxmeVkR7LRSGkoXHaIfm9ObzxPMcPHQYQhf3+X9jepIFNHQA==
dependencies:
"@react-spring/animated" "~9.2.6-beta.0"
"@react-spring/core" "~9.2.6-beta.0"
"@react-spring/shared" "~9.2.6-beta.0"
"@react-spring/types" "~9.2.6-beta.0"
"@react-spring/animated" "~9.4.5"
"@react-spring/core" "~9.4.5"
"@react-spring/shared" "~9.4.5"
"@react-spring/types" "~9.4.5"
"@rushstack/eslint-patch@^1.1.3":
version "1.1.3"
@ -2933,10 +2931,10 @@
"@svgr/plugin-jsx" "^6.2.1"
"@svgr/plugin-svgo" "^6.2.0"
"@swc/helpers@0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.3.tgz#16593dfc248c53b699d4b5026040f88ddb497012"
integrity sha512-6JrF+fdUK2zbGpJIlN7G3v966PQjyx/dPt1T9km2wj+EUBqgrxCk3uX4Kct16MIm9gGxfKRcfax2hVf5jvlTzA==
"@swc/helpers@0.4.11":
version "0.4.11"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.11.tgz#db23a376761b3d31c26502122f349a21b592c8de"
integrity sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==
dependencies:
tslib "^2.4.0"
@ -3566,6 +3564,13 @@
dependencies:
"@types/node" "*"
"@types/yauzl@^2.9.1":
version "2.10.0"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@5.36.0":
version "5.36.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.0.tgz#8f159c4cdb3084eb5d4b72619a2ded942aa109e5"
@ -4318,7 +4323,7 @@ base16@^1.0.0:
resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70"
integrity sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==
base64-js@^1.3.0:
base64-js@^1.3.0, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@ -4348,6 +4353,15 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bl@^4.0.3:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
dependencies:
buffer "^5.5.0"
inherits "^2.0.4"
readable-stream "^3.4.0"
bluebird@^3.7.1:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
@ -4461,6 +4475,11 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4
node-releases "^2.0.3"
picocolors "^1.0.0"
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
@ -4471,6 +4490,14 @@ buffer-from@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buffer@^5.2.1, buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@ -4545,6 +4572,11 @@ caniuse-lite@^1.0.30001230, caniuse-lite@^1.0.30001332:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz#59590c8ffa8b5939cf4161f00827b8873ad72498"
integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA==
caniuse-lite@^1.0.30001406:
version "1.0.30001409"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001409.tgz#6135da9dcab34cd9761d9cdb12a68e6740c5e96e"
integrity sha512-V0mnJ5dwarmhYv8/MzhJ//aW68UpvnQBXv8lJ2QUsvn2pHcmAuNtu8hQEDz37XnA1iE+lRR9CIfGWWpgJ5QedQ==
ccount@^1.0.0, ccount@^1.0.3:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043"
@ -4645,6 +4677,11 @@ chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
chownr@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chrome-trace-event@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
@ -5011,7 +5048,7 @@ cross-env@^7.0.3:
dependencies:
cross-spawn "^7.0.1"
cross-fetch@^3.1.5:
cross-fetch@3.1.5, cross-fetch@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
@ -5227,11 +5264,6 @@ d3-format@^1.4.4:
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==
d3-hierarchy@^1.1.8:
version "1.1.9"
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83"
integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==
"d3-interpolate@1 - 2", "d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163"
@ -5338,7 +5370,7 @@ debug@3.1.0:
dependencies:
ms "2.0.0"
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -5494,6 +5526,11 @@ detective@^5.2.1:
defined "^1.0.0"
minimist "^1.2.6"
devtools-protocol@0.0.1036444:
version "0.0.1036444"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1036444.tgz#a570d3cdde61527c82f9b03919847b8ac7b1c2b9"
integrity sha512-0y4f/T8H9lsESV9kKP1HDUXgHxCdniFeJh6Erq+FbdOEvp/Ydp9t8kcAAM5gOd17pMrTDlFWntoHtzzeTUWKNw==
dicer@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.1.tgz#abf28921e3475bc5e801e74e0159fd94f927ba97"
@ -6234,6 +6271,17 @@ extend@^3.0.0, extend@^3.0.2, extend@~3.0.2:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
extract-zip@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
dependencies:
debug "^4.1.1"
get-stream "^5.1.0"
yauzl "^2.10.0"
optionalDependencies:
"@types/yauzl" "^2.9.1"
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@ -6316,6 +6364,13 @@ fbjs@^3.0.0, fbjs@^3.0.1:
setimmediate "^1.0.5"
ua-parser-js "^0.7.30"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==
dependencies:
pend "~1.2.0"
feed@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e"
@ -6575,6 +6630,11 @@ fresh@0.5.2:
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
fs-extra@^10.0.1:
version "10.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
@ -7293,6 +7353,14 @@ http-proxy@^1.18.1:
follow-redirects "^1.0.0"
requires-port "^1.0.0"
https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"
https-proxy-agent@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81"
@ -7301,14 +7369,6 @@ https-proxy-agent@^3.0.0:
agent-base "^4.3.0"
debug "^3.1.0"
https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
@ -7331,6 +7391,11 @@ idb@7.0.1:
resolved "https://registry.yarnpkg.com/idb/-/idb-7.0.1.tgz#d2875b3a2f205d854ee307f6d196f246fea590a7"
integrity sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
@ -7404,7 +7469,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -8559,6 +8624,11 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp-classic@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mkdirp@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e"
@ -8637,31 +8707,31 @@ next-sitemap@^2.5.14:
"@corex/deepmerge" "^2.6.148"
minimist "^1.2.6"
next@12.2.5:
version "12.2.5"
resolved "https://registry.yarnpkg.com/next/-/next-12.2.5.tgz#14fb5975e8841fad09553b8ef41fe1393602b717"
integrity sha512-tBdjqX5XC/oFs/6gxrZhjmiq90YWizUYU6qOWAfat7zJwrwapJ+BYgX2PmiacunXMaRpeVT4vz5MSPSLgNkrpA==
next@12.3.1:
version "12.3.1"
resolved "https://registry.yarnpkg.com/next/-/next-12.3.1.tgz#127b825ad2207faf869b33393ec8c75fe61e50f1"
integrity sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==
dependencies:
"@next/env" "12.2.5"
"@swc/helpers" "0.4.3"
caniuse-lite "^1.0.30001332"
"@next/env" "12.3.1"
"@swc/helpers" "0.4.11"
caniuse-lite "^1.0.30001406"
postcss "8.4.14"
styled-jsx "5.0.4"
styled-jsx "5.0.7"
use-sync-external-store "1.2.0"
optionalDependencies:
"@next/swc-android-arm-eabi" "12.2.5"
"@next/swc-android-arm64" "12.2.5"
"@next/swc-darwin-arm64" "12.2.5"
"@next/swc-darwin-x64" "12.2.5"
"@next/swc-freebsd-x64" "12.2.5"
"@next/swc-linux-arm-gnueabihf" "12.2.5"
"@next/swc-linux-arm64-gnu" "12.2.5"
"@next/swc-linux-arm64-musl" "12.2.5"
"@next/swc-linux-x64-gnu" "12.2.5"
"@next/swc-linux-x64-musl" "12.2.5"
"@next/swc-win32-arm64-msvc" "12.2.5"
"@next/swc-win32-ia32-msvc" "12.2.5"
"@next/swc-win32-x64-msvc" "12.2.5"
"@next/swc-android-arm-eabi" "12.3.1"
"@next/swc-android-arm64" "12.3.1"
"@next/swc-darwin-arm64" "12.3.1"
"@next/swc-darwin-x64" "12.3.1"
"@next/swc-freebsd-x64" "12.3.1"
"@next/swc-linux-arm-gnueabihf" "12.3.1"
"@next/swc-linux-arm64-gnu" "12.3.1"
"@next/swc-linux-arm64-musl" "12.3.1"
"@next/swc-linux-x64-gnu" "12.3.1"
"@next/swc-linux-x64-musl" "12.3.1"
"@next/swc-win32-arm64-msvc" "12.3.1"
"@next/swc-win32-ia32-msvc" "12.3.1"
"@next/swc-win32-x64-msvc" "12.3.1"
no-case@^3.0.4:
version "3.0.4"
@ -9202,15 +9272,10 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
performance-now@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
picocolors@^1.0.0:
version "1.0.0"
@ -9634,6 +9699,11 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
progress@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
@ -9656,7 +9726,7 @@ prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -9824,7 +9894,7 @@ proxy-agent@^3.0.3:
proxy-from-env "^1.0.0"
socks-proxy-agent "^4.0.1"
proxy-from-env@^1.0.0:
proxy-from-env@1.1.0, proxy-from-env@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
@ -9873,6 +9943,23 @@ pupa@^2.1.1:
dependencies:
escape-goat "^2.0.0"
puppeteer@18.0.5:
version "18.0.5"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-18.0.5.tgz#873223b17b92345182c5b5e8cfbd6f3117f1547d"
integrity sha512-s4erjxU0VtKojPvF+KvLKG6OHUPw7gO2YV1dtOsoryyCbhrs444fXb4QZqGWuTv3V/rgSCUzeixxu34g0ZkSMA==
dependencies:
cross-fetch "3.1.5"
debug "4.3.4"
devtools-protocol "0.0.1036444"
extract-zip "2.0.1"
https-proxy-agent "5.0.1"
progress "2.0.3"
proxy-from-env "1.1.0"
rimraf "3.0.2"
tar-fs "2.1.1"
unbzip2-stream "1.4.3"
ws "8.8.1"
pure-color@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e"
@ -9917,13 +10004,6 @@ raf-schd@^4.0.2:
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
raf@^3.1.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
dependencies:
performance-now "^2.1.0"
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -10123,15 +10203,6 @@ react-masonry-css@1.0.16:
resolved "https://registry.yarnpkg.com/react-masonry-css/-/react-masonry-css-1.0.16.tgz#72b28b4ae3484e250534700860597553a10f1a2c"
integrity sha512-KSW0hR2VQmltt/qAa3eXOctQDyOu7+ZBevtKgpNDSzT7k5LA/0XntNa9z9HKCdz3QlxmJHglTZ18e4sX4V8zZQ==
react-motion@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==
dependencies:
performance-now "^0.2.0"
prop-types "^15.5.8"
raf "^3.1.0"
react-query@3.39.0:
version "3.39.0"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.0.tgz#0caca7b0da98e65008bbcd4df0d25618c2100050"
@ -10269,7 +10340,7 @@ readable-stream@2, readable-stream@^2.0.1, readable-stream@~2.3.6:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@^3.0.6, readable-stream@^3.1.1:
readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@ -10533,11 +10604,6 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@ -11267,10 +11333,10 @@ style-to-object@0.3.0, style-to-object@^0.3.0:
dependencies:
inline-style-parser "0.1.1"
styled-jsx@5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.4.tgz#5b1bd0b9ab44caae3dd1361295559706e044aa53"
integrity sha512-sDFWLbg4zR+UkNzfk5lPilyIgtpddfxXEULxhujorr5jtePTUqiPDc5BC0v1NRqTr/WaFBGQQUoYToGlF4B2KQ==
styled-jsx@5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.7.tgz#be44afc53771b983769ac654d355ca8d019dff48"
integrity sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==
stylehacks@^5.1.0:
version "5.1.0"
@ -11362,6 +11428,27 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
tar-fs@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"
pump "^3.0.0"
tar-stream "^2.1.4"
tar-stream@^2.1.4:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
dependencies:
bl "^4.0.3"
end-of-stream "^1.4.1"
fs-constants "^1.0.0"
inherits "^2.0.3"
readable-stream "^3.1.1"
teeny-request@^7.1.3:
version "7.2.0"
resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-7.2.0.tgz#41347ece068f08d741e7b86df38a4498208b2633"
@ -11399,6 +11486,11 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
through@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
thunkify@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d"
@ -11622,6 +11714,14 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
unbzip2-stream@1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
dependencies:
buffer "^5.2.1"
through "^2.3.8"
undefsafe@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
@ -12229,6 +12329,11 @@ write-file-atomic@^3.0.0:
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"
ws@8.8.1:
version "8.8.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0"
integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==
ws@>=7.4.6:
version "8.6.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.6.0.tgz#e5e9f1d9e7ff88083d0c0dd8281ea662a42c9c23"
@ -12309,6 +12414,14 @@ yargs@^16.2.0:
y18n "^5.0.5"
yargs-parser "^20.2.2"
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"