Merge branch 'manifoldmarkets:main' into main
This commit is contained in:
commit
f8d16d384a
|
@ -8,7 +8,7 @@ import { User } from '../../common/user'
|
|||
import { Contract } from '../../common/contract'
|
||||
import { getPrivateUser, getValues } from './utils'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { uniq } from 'lodash'
|
||||
import { groupBy, uniq } from 'lodash'
|
||||
import { Bet, LimitBet } from '../../common/bet'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { getContractBetMetrics } from '../../common/calculate'
|
||||
|
@ -23,6 +23,7 @@ import {
|
|||
sendNewAnswerEmail,
|
||||
sendNewCommentEmail,
|
||||
sendNewFollowedMarketEmail,
|
||||
sendNewUniqueBettorsEmail,
|
||||
} from './emails'
|
||||
import { filterDefined } from '../../common/util/array'
|
||||
const firestore = admin.firestore()
|
||||
|
@ -774,17 +775,16 @@ export const createUniqueBettorBonusNotification = async (
|
|||
txnId: string,
|
||||
contract: Contract,
|
||||
amount: number,
|
||||
uniqueBettorIds: string[],
|
||||
idempotencyKey: string
|
||||
) => {
|
||||
console.log('createUniqueBettorBonusNotification')
|
||||
const privateUser = await getPrivateUser(contractCreatorId)
|
||||
if (!privateUser) return
|
||||
const { sendToBrowser } = await getDestinationsForUser(
|
||||
const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
|
||||
privateUser,
|
||||
'unique_bettors_on_your_contract'
|
||||
)
|
||||
if (!sendToBrowser) return
|
||||
|
||||
if (sendToBrowser) {
|
||||
const notificationRef = firestore
|
||||
.collection(`/users/${contractCreatorId}/notifications`)
|
||||
.doc(idempotencyKey)
|
||||
|
@ -809,9 +809,50 @@ export const createUniqueBettorBonusNotification = async (
|
|||
sourceContractTitle: contract.question,
|
||||
sourceContractCreatorUsername: contract.creatorUsername,
|
||||
}
|
||||
return await notificationRef.set(removeUndefinedProps(notification))
|
||||
await notificationRef.set(removeUndefinedProps(notification))
|
||||
}
|
||||
|
||||
// TODO send email notification
|
||||
if (!sendToEmail) return
|
||||
const uniqueBettorsExcludingCreator = uniqueBettorIds.filter(
|
||||
(id) => id !== contractCreatorId
|
||||
)
|
||||
// only send on 1st and 6th bettor
|
||||
if (
|
||||
uniqueBettorsExcludingCreator.length !== 1 &&
|
||||
uniqueBettorsExcludingCreator.length !== 6
|
||||
)
|
||||
return
|
||||
const totalNewBettorsToReport =
|
||||
uniqueBettorsExcludingCreator.length === 1 ? 1 : 5
|
||||
|
||||
const mostRecentUniqueBettors = await getValues<User>(
|
||||
firestore
|
||||
.collection('users')
|
||||
.where(
|
||||
'id',
|
||||
'in',
|
||||
uniqueBettorsExcludingCreator.slice(
|
||||
uniqueBettorsExcludingCreator.length - totalNewBettorsToReport,
|
||||
uniqueBettorsExcludingCreator.length
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const bets = await getValues<Bet>(
|
||||
firestore.collection('contracts').doc(contract.id).collection('bets')
|
||||
)
|
||||
// group bets by bettors
|
||||
const bettorsToTheirBets = groupBy(bets, (bet) => bet.userId)
|
||||
await sendNewUniqueBettorsEmail(
|
||||
'unique_bettors_on_your_contract',
|
||||
contractCreatorId,
|
||||
privateUser,
|
||||
contract,
|
||||
uniqueBettorsExcludingCreator.length,
|
||||
mostRecentUniqueBettors,
|
||||
bettorsToTheirBets,
|
||||
Math.round(amount * totalNewBettorsToReport)
|
||||
)
|
||||
}
|
||||
|
||||
export const createNewContractNotification = async (
|
||||
|
|
397
functions/src/email-templates/new-unique-bettor.html
Normal file
397
functions/src/email-templates/new-unique-bettor.html
Normal file
|
@ -0,0 +1,397 @@
|
|||
<!DOCTYPE html>
|
||||
<html style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>New unique predictors on your market</title>
|
||||
|
||||
<style type="text/css">
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body itemscope itemtype="http://schema.org/EmailMessage" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<table class="body-wrap" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
" valign="top"></td>
|
||||
<td class="container" width="600" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
display: block !important;
|
||||
max-width: 600px !important;
|
||||
clear: both !important;
|
||||
margin: 0 auto;
|
||||
" valign="top">
|
||||
<div class="content" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
max-width: 600px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
margin: 0;
|
||||
border: 1px solid #e9e9e9;
|
||||
" bgcolor="#fff">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-wrap aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
" align="center" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
width: 90%;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 0px 0;
|
||||
text-align: left;
|
||||
" valign="top">
|
||||
<a href="https://manifold.markets" target="_blank">
|
||||
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
|
||||
alt="Manifold Markets" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
Your market <a href='{{marketUrl}}'>{{marketTitle}}</a> just got its first prediction from a user!
|
||||
<br/>
|
||||
<br/>
|
||||
We sent you a <span style='color: #11b981'>{{bonusString}}</span> bonus for
|
||||
creating a market that appeals to others, and we'll do so for each new predictor.
|
||||
<br/>
|
||||
<br/>
|
||||
Keep up the good work and check out your newest predictor below!
|
||||
</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
" align="center" valign="top">
|
||||
<table class="invoice" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
width: 80%;
|
||||
margin: 40px auto;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor1AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor1Name}}</span>
|
||||
{{bet1Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="padding: 20px 0 0 0; margin: 0">
|
||||
<div align="center">
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
|
||||
<a href="{{marketUrl}}" target="_blank" style="
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
font-family: arial, helvetica, sans-serif;
|
||||
text-decoration: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
background-color: #11b981;
|
||||
border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
mso-border-alt: none;
|
||||
">
|
||||
<span style="
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
line-height: 120%;
|
||||
"><span style="
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 18.8px;
|
||||
">View market</span></span>
|
||||
</a>
|
||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
clear: both;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
">
|
||||
<table width="100%" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="aligncenter content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
vertical-align: top;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" align="center" valign="top">
|
||||
Questions? Come ask in
|
||||
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
">our Discord</a>! Or,
|
||||
<a href="{{unsubscribeUrl}}" style="
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
" target="_blank">click here to manage your notifications</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
" valign="top"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
501
functions/src/email-templates/new-unique-bettors.html
Normal file
501
functions/src/email-templates/new-unique-bettors.html
Normal file
|
@ -0,0 +1,501 @@
|
|||
<!DOCTYPE html>
|
||||
<html style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>New unique predictors on your market</title>
|
||||
|
||||
<style type="text/css">
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body itemscope itemtype="http://schema.org/EmailMessage" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<table class="body-wrap" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
" valign="top"></td>
|
||||
<td class="container" width="600" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
display: block !important;
|
||||
max-width: 600px !important;
|
||||
clear: both !important;
|
||||
margin: 0 auto;
|
||||
" valign="top">
|
||||
<div class="content" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
max-width: 600px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
margin: 0;
|
||||
border: 1px solid #e9e9e9;
|
||||
" bgcolor="#fff">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-wrap aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
" align="center" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
width: 90%;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 0px 0;
|
||||
text-align: left;
|
||||
" valign="top">
|
||||
<a href="https://manifold.markets" target="_blank">
|
||||
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
|
||||
alt="Manifold Markets" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
Your market <a href='{{marketUrl}}'>{{marketTitle}}</a> got predictions from a total of {{totalPredictors}} users!
|
||||
<br/>
|
||||
<br/>
|
||||
We sent you a <span style='color: #11b981'>{{bonusString}}</span> bonus for getting {{newPredictors}} new predictors,
|
||||
and we'll continue to do so for each new predictor, (although we won't send you any more emails about it for this market).
|
||||
<br/>
|
||||
<br/>
|
||||
Keep up the good work and check out your newest predictors below!
|
||||
</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
" align="center" valign="top">
|
||||
<table class="invoice" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
width: 80%;
|
||||
margin: 40px auto;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor1AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor1Name}}</span>
|
||||
{{bet1Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor2AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor2Name}}</span>
|
||||
{{bet2Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor3AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor3Name}}</span>
|
||||
{{bet3Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor4AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor4Name}}</span>
|
||||
{{bet4Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor5AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor5Name}}</span>
|
||||
{{bet5Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="padding: 20px 0 0 0; margin: 0">
|
||||
<div align="center">
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
|
||||
<a href="{{marketUrl}}" target="_blank" style="
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
font-family: arial, helvetica, sans-serif;
|
||||
text-decoration: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
background-color: #11b981;
|
||||
border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
mso-border-alt: none;
|
||||
">
|
||||
<span style="
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
line-height: 120%;
|
||||
"><span style="
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 18.8px;
|
||||
">View market</span></span>
|
||||
</a>
|
||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
clear: both;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
">
|
||||
<table width="100%" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="aligncenter content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
vertical-align: top;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" align="center" valign="top">
|
||||
Questions? Come ask in
|
||||
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
">our Discord</a>! Or,
|
||||
<a href="{{unsubscribeUrl}}" style="
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
" target="_blank">click here to manage your notifications</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
" valign="top"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -22,6 +22,7 @@ import {
|
|||
notification_reason_types,
|
||||
getDestinationsForUser,
|
||||
} from '../../common/notification'
|
||||
import { Dictionary } from 'lodash'
|
||||
|
||||
export const sendMarketResolutionEmail = async (
|
||||
reason: notification_reason_types,
|
||||
|
@ -544,3 +545,63 @@ export const sendNewFollowedMarketEmail = async (
|
|||
}
|
||||
)
|
||||
}
|
||||
export const sendNewUniqueBettorsEmail = async (
|
||||
reason: notification_reason_types,
|
||||
userId: string,
|
||||
privateUser: PrivateUser,
|
||||
contract: Contract,
|
||||
totalPredictors: number,
|
||||
newPredictors: User[],
|
||||
userBets: Dictionary<[Bet, ...Bet[]]>,
|
||||
bonusAmount: number
|
||||
) => {
|
||||
const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
|
||||
await getDestinationsForUser(privateUser, reason)
|
||||
if (!privateUser.email || !sendToEmail) return
|
||||
const user = await getUser(privateUser.id)
|
||||
if (!user) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const creatorName = contract.creatorName
|
||||
// make the emails stack for the same contract
|
||||
const subject = `You made a popular market! ${
|
||||
contract.question.length > 50
|
||||
? contract.question.slice(0, 50) + '...'
|
||||
: contract.question
|
||||
} just got ${
|
||||
newPredictors.length
|
||||
} new predictions. Check out who's predicting on it inside.`
|
||||
const templateData: Record<string, string> = {
|
||||
name: firstName,
|
||||
creatorName,
|
||||
totalPredictors: totalPredictors.toString(),
|
||||
bonusString: formatMoney(bonusAmount),
|
||||
marketTitle: contract.question,
|
||||
marketUrl: contractUrl(contract),
|
||||
unsubscribeUrl,
|
||||
newPredictors: newPredictors.length.toString(),
|
||||
}
|
||||
|
||||
newPredictors.forEach((p, i) => {
|
||||
templateData[`bettor${i + 1}Name`] = p.name
|
||||
if (p.avatarUrl) templateData[`bettor${i + 1}AvatarUrl`] = p.avatarUrl
|
||||
const bet = userBets[p.id][0]
|
||||
if (bet) {
|
||||
const { amount, sale } = bet
|
||||
templateData[`bet${i + 1}Description`] = `${
|
||||
sale || amount < 0 ? 'sold' : 'bought'
|
||||
} ${formatMoney(Math.abs(amount))}`
|
||||
}
|
||||
})
|
||||
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
newPredictors.length === 1 ? 'new-unique-bettor' : 'new-unique-bettors',
|
||||
templateData,
|
||||
{
|
||||
from: `Manifold Markets <no-reply@manifold.markets>`,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -28,8 +28,9 @@ import { User } from '../../common/user'
|
|||
const firestore = admin.firestore()
|
||||
const BONUS_START_DATE = new Date('2022-07-13T15:30:00.000Z').getTime()
|
||||
|
||||
export const onCreateBet = functions.firestore
|
||||
.document('contracts/{contractId}/bets/{betId}')
|
||||
export const onCreateBet = functions
|
||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||
.firestore.document('contracts/{contractId}/bets/{betId}')
|
||||
.onCreate(async (change, context) => {
|
||||
const { contractId } = context.params as {
|
||||
contractId: string
|
||||
|
@ -198,6 +199,7 @@ const updateUniqueBettorsAndGiveCreatorBonus = async (
|
|||
result.txn.id,
|
||||
contract,
|
||||
result.txn.amount,
|
||||
newUniqueBettorIds,
|
||||
eventId + '-unique-bettor-bonus'
|
||||
)
|
||||
}
|
||||
|
|
|
@ -111,9 +111,9 @@ export const getHomeItems = (groups: Group[], sections: string[]) => {
|
|||
if (!isArray(sections)) sections = []
|
||||
|
||||
const items = [
|
||||
{ label: 'Daily movers', id: 'daily-movers' },
|
||||
{ label: 'Trending', id: 'score' },
|
||||
{ label: 'New for you', id: 'newest' },
|
||||
{ label: 'Daily movers', id: 'daily-movers' },
|
||||
...groups.map((g) => ({
|
||||
label: g.name,
|
||||
id: g.id,
|
||||
|
|
|
@ -754,7 +754,10 @@ function SellButton(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function ProfitBadge(props: { profitPercent: number; className?: string }) {
|
||||
export function ProfitBadge(props: {
|
||||
profitPercent: number
|
||||
className?: string
|
||||
}) {
|
||||
const { profitPercent, className } = props
|
||||
if (!profitPercent) return null
|
||||
const colors =
|
||||
|
|
|
@ -200,7 +200,7 @@ export function ContractSearch(props: {
|
|||
}
|
||||
|
||||
return (
|
||||
<Col className="h-full">
|
||||
<Col>
|
||||
<ContractSearchControls
|
||||
className={headerClassName}
|
||||
defaultSort={defaultSort}
|
||||
|
|
102
web/components/contract-select-modal.tsx
Normal file
102
web/components/contract-select-modal.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
import { Contract } from 'common/contract'
|
||||
import { useState } from 'react'
|
||||
import { Button } from './button'
|
||||
import { ContractSearch } from './contract-search'
|
||||
import { Col } from './layout/col'
|
||||
import { Modal } from './layout/modal'
|
||||
import { Row } from './layout/row'
|
||||
import { LoadingIndicator } from './loading-indicator'
|
||||
|
||||
export function SelectMarketsModal(props: {
|
||||
title: string
|
||||
description?: React.ReactNode
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
submitLabel: (length: number) => string
|
||||
onSubmit: (contracts: Contract[]) => void | Promise<void>
|
||||
contractSearchOptions?: Partial<Parameters<typeof ContractSearch>[0]>
|
||||
}) {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
open,
|
||||
setOpen,
|
||||
submitLabel,
|
||||
onSubmit,
|
||||
contractSearchOptions,
|
||||
} = props
|
||||
|
||||
const [contracts, setContracts] = useState<Contract[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function addContract(contract: Contract) {
|
||||
if (contracts.map((c) => c.id).includes(contract.id)) {
|
||||
setContracts(contracts.filter((c) => c.id !== contract.id))
|
||||
} else setContracts([...contracts, contract])
|
||||
}
|
||||
|
||||
async function onFinish() {
|
||||
setLoading(true)
|
||||
await onSubmit(contracts)
|
||||
setLoading(false)
|
||||
setOpen(false)
|
||||
setContracts([])
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}>
|
||||
<Col className="h-[85vh] w-full gap-4 rounded-md bg-white">
|
||||
<div className="p-8 pb-0">
|
||||
<Row>
|
||||
<div className={'text-xl text-indigo-700'}>{title}</div>
|
||||
|
||||
{!loading && (
|
||||
<Row className="grow justify-end gap-4">
|
||||
{contracts.length > 0 && (
|
||||
<Button onClick={onFinish} color="indigo">
|
||||
{submitLabel(contracts.length)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (contracts.length > 0) {
|
||||
setContracts([])
|
||||
} else {
|
||||
setOpen(false)
|
||||
}
|
||||
}}
|
||||
color="gray"
|
||||
>
|
||||
{contracts.length > 0 ? 'Reset' : 'Cancel'}
|
||||
</Button>
|
||||
</Row>
|
||||
)}
|
||||
</Row>
|
||||
{description}
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="w-full justify-center">
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-y-auto sm:px-8">
|
||||
<ContractSearch
|
||||
hideOrderSelector
|
||||
onContractClick={addContract}
|
||||
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
||||
highlightOptions={{
|
||||
contractIds: contracts.map((c) => c.id),
|
||||
highlightClassName:
|
||||
'!bg-indigo-100 outline outline-2 outline-indigo-300',
|
||||
}}
|
||||
additionalFilter={{}} /* hide pills */
|
||||
headerClassName="bg-white"
|
||||
{...contractSearchOptions}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Modal>
|
||||
)
|
||||
}
|
|
@ -294,7 +294,7 @@ export function ExtraMobileContractDetails(props: {
|
|||
<Tooltip
|
||||
text={`${formatMoney(
|
||||
volume
|
||||
)} bet - ${uniqueBettors} unique traders`}
|
||||
)} bet - ${uniqueBettors} unique predictors`}
|
||||
>
|
||||
{volumeTranslation}
|
||||
</Tooltip>
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
import { Editor } from '@tiptap/react'
|
||||
import { Contract } from 'common/contract'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '../button'
|
||||
import { ContractSearch } from '../contract-search'
|
||||
import { Col } from '../layout/col'
|
||||
import { Modal } from '../layout/modal'
|
||||
import { Row } from '../layout/row'
|
||||
import { LoadingIndicator } from '../loading-indicator'
|
||||
import { SelectMarketsModal } from '../contract-select-modal'
|
||||
import { embedContractCode, embedContractGridCode } from '../share-embed-button'
|
||||
import { insertContent } from './utils'
|
||||
|
||||
|
@ -17,83 +11,23 @@ export function MarketModal(props: {
|
|||
}) {
|
||||
const { editor, open, setOpen } = props
|
||||
|
||||
const [contracts, setContracts] = useState<Contract[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function addContract(contract: Contract) {
|
||||
if (contracts.map((c) => c.id).includes(contract.id)) {
|
||||
setContracts(contracts.filter((c) => c.id !== contract.id))
|
||||
} else setContracts([...contracts, contract])
|
||||
}
|
||||
|
||||
async function doneAddingContracts() {
|
||||
setLoading(true)
|
||||
function onSubmit(contracts: Contract[]) {
|
||||
if (contracts.length == 1) {
|
||||
insertContent(editor, embedContractCode(contracts[0]))
|
||||
} else if (contracts.length > 1) {
|
||||
insertContent(editor, embedContractGridCode(contracts))
|
||||
}
|
||||
setLoading(false)
|
||||
setOpen(false)
|
||||
setContracts([])
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}>
|
||||
<Col className="h-[85vh] w-full gap-4 rounded-md bg-white">
|
||||
<Row className="p-8 pb-0">
|
||||
<div className={'text-xl text-indigo-700'}>Embed a market</div>
|
||||
|
||||
{!loading && (
|
||||
<Row className="grow justify-end gap-4">
|
||||
{contracts.length == 1 && (
|
||||
<Button onClick={doneAddingContracts} color={'indigo'}>
|
||||
Embed 1 question
|
||||
</Button>
|
||||
)}
|
||||
{contracts.length > 1 && (
|
||||
<Button onClick={doneAddingContracts} color={'indigo'}>
|
||||
Embed grid of {contracts.length} question
|
||||
{contracts.length > 1 && 's'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (contracts.length > 0) {
|
||||
setContracts([])
|
||||
} else {
|
||||
setOpen(false)
|
||||
<SelectMarketsModal
|
||||
title="Embed markets"
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
submitLabel={(len) =>
|
||||
len == 1 ? 'Embed 1 question' : `Embed grid of ${len} questions`
|
||||
}
|
||||
}}
|
||||
color="gray"
|
||||
>
|
||||
{contracts.length > 0 ? 'Reset' : 'Cancel'}
|
||||
</Button>
|
||||
</Row>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
{loading && (
|
||||
<div className="w-full justify-center">
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-y-scroll sm:px-8">
|
||||
<ContractSearch
|
||||
hideOrderSelector
|
||||
onContractClick={addContract}
|
||||
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
||||
highlightOptions={{
|
||||
contractIds: contracts.map((c) => c.id),
|
||||
highlightClassName:
|
||||
'!bg-indigo-100 outline outline-2 outline-indigo-300',
|
||||
}}
|
||||
additionalFilter={{}} /* hide pills */
|
||||
headerClassName="bg-white"
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { usePrivateUser } from 'web/hooks/use-user'
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
import React, { memo, ReactNode, useEffect, useState } from 'react'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
notification_subscription_types,
|
||||
notification_destination_types,
|
||||
PrivateUser,
|
||||
} from 'common/user'
|
||||
import { updatePrivateUser } from 'web/lib/firebase/users'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
|
@ -23,21 +22,22 @@ import {
|
|||
UsersIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import { WatchMarketModal } from 'web/components/contract/watch-market-modal'
|
||||
import { filterDefined } from 'common/util/array'
|
||||
import toast from 'react-hot-toast'
|
||||
import { SwitchSetting } from 'web/components/switch-setting'
|
||||
import { uniq } from 'lodash'
|
||||
import {
|
||||
storageStore,
|
||||
usePersistentState,
|
||||
} from 'web/hooks/use-persistent-state'
|
||||
import { safeLocalStorage } from 'web/lib/util/local'
|
||||
|
||||
export function NotificationSettings(props: {
|
||||
navigateToSection: string | undefined
|
||||
privateUser: PrivateUser
|
||||
}) {
|
||||
const { navigateToSection } = props
|
||||
const privateUser = usePrivateUser()
|
||||
const { navigateToSection, privateUser } = props
|
||||
const [showWatchModal, setShowWatchModal] = useState(false)
|
||||
|
||||
if (!privateUser || !privateUser.notificationSubscriptionTypes) {
|
||||
return <LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} />
|
||||
}
|
||||
|
||||
const emailsEnabled: Array<keyof notification_subscription_types> = [
|
||||
'all_comments_on_watched_markets',
|
||||
'all_replies_to_my_comments_on_watched_markets',
|
||||
|
@ -60,11 +60,14 @@ export function NotificationSettings(props: {
|
|||
|
||||
'tagged_user', // missing tagged on contract description email
|
||||
'contract_from_followed_user',
|
||||
'unique_bettors_on_your_contract',
|
||||
// TODO: add these
|
||||
// one-click unsubscribe only unsubscribes them from that type only, (well highlighted), then a link to manage the rest of their notifications
|
||||
// 'profit_loss_updates', - changes in markets you have shares in
|
||||
// biggest winner, here are the rest of your markets
|
||||
|
||||
// 'referral_bonuses',
|
||||
// 'unique_bettors_on_your_contract',
|
||||
// 'on_new_follow',
|
||||
// 'profit_loss_updates',
|
||||
// 'tips_on_your_markets',
|
||||
// 'tips_on_your_comments',
|
||||
// maybe the following?
|
||||
|
@ -78,14 +81,14 @@ export function NotificationSettings(props: {
|
|||
'thank_you_for_purchases',
|
||||
]
|
||||
|
||||
type sectionData = {
|
||||
type SectionData = {
|
||||
label: string
|
||||
subscriptionTypeToDescription: {
|
||||
[key in keyof Partial<notification_subscription_types>]: string
|
||||
}
|
||||
}
|
||||
|
||||
const comments: sectionData = {
|
||||
const comments: SectionData = {
|
||||
label: 'New Comments',
|
||||
subscriptionTypeToDescription: {
|
||||
all_comments_on_watched_markets: 'All new comments',
|
||||
|
@ -99,7 +102,7 @@ export function NotificationSettings(props: {
|
|||
},
|
||||
}
|
||||
|
||||
const answers: sectionData = {
|
||||
const answers: SectionData = {
|
||||
label: 'New Answers',
|
||||
subscriptionTypeToDescription: {
|
||||
all_answers_on_watched_markets: 'All new answers',
|
||||
|
@ -108,7 +111,7 @@ export function NotificationSettings(props: {
|
|||
// answers_by_market_creator_on_watched_markets: 'By market creator',
|
||||
},
|
||||
}
|
||||
const updates: sectionData = {
|
||||
const updates: SectionData = {
|
||||
label: 'Updates & Resolutions',
|
||||
subscriptionTypeToDescription: {
|
||||
market_updates_on_watched_markets: 'All creator updates',
|
||||
|
@ -118,7 +121,7 @@ export function NotificationSettings(props: {
|
|||
// probability_updates_on_watched_markets: 'Probability updates',
|
||||
},
|
||||
}
|
||||
const yourMarkets: sectionData = {
|
||||
const yourMarkets: SectionData = {
|
||||
label: 'Markets You Created',
|
||||
subscriptionTypeToDescription: {
|
||||
your_contract_closed: 'Your market has closed (and needs resolution)',
|
||||
|
@ -128,15 +131,15 @@ export function NotificationSettings(props: {
|
|||
tips_on_your_markets: 'Likes on your markets',
|
||||
},
|
||||
}
|
||||
const bonuses: sectionData = {
|
||||
const bonuses: SectionData = {
|
||||
label: 'Bonuses',
|
||||
subscriptionTypeToDescription: {
|
||||
betting_streaks: 'Betting streak bonuses',
|
||||
betting_streaks: 'Prediction streak bonuses',
|
||||
referral_bonuses: 'Referral bonuses from referring users',
|
||||
unique_bettors_on_your_contract: 'Unique bettor bonuses on your markets',
|
||||
},
|
||||
}
|
||||
const otherBalances: sectionData = {
|
||||
const otherBalances: SectionData = {
|
||||
label: 'Other',
|
||||
subscriptionTypeToDescription: {
|
||||
loan_income: 'Automatic loans from your profitable bets',
|
||||
|
@ -144,7 +147,7 @@ export function NotificationSettings(props: {
|
|||
tips_on_your_comments: 'Tips on your comments',
|
||||
},
|
||||
}
|
||||
const userInteractions: sectionData = {
|
||||
const userInteractions: SectionData = {
|
||||
label: 'Users',
|
||||
subscriptionTypeToDescription: {
|
||||
tagged_user: 'A user tagged you',
|
||||
|
@ -152,7 +155,7 @@ export function NotificationSettings(props: {
|
|||
contract_from_followed_user: 'New markets created by users you follow',
|
||||
},
|
||||
}
|
||||
const generalOther: sectionData = {
|
||||
const generalOther: SectionData = {
|
||||
label: 'Other',
|
||||
subscriptionTypeToDescription: {
|
||||
trending_markets: 'Weekly interesting markets',
|
||||
|
@ -162,32 +165,29 @@ export function NotificationSettings(props: {
|
|||
},
|
||||
}
|
||||
|
||||
const NotificationSettingLine = (
|
||||
description: string,
|
||||
key: keyof notification_subscription_types,
|
||||
value: notification_destination_types[]
|
||||
) => {
|
||||
const previousInAppValue = value.includes('browser')
|
||||
const previousEmailValue = value.includes('email')
|
||||
function NotificationSettingLine(props: {
|
||||
description: string
|
||||
subscriptionTypeKey: keyof notification_subscription_types
|
||||
destinations: notification_destination_types[]
|
||||
}) {
|
||||
const { description, subscriptionTypeKey, destinations } = props
|
||||
const previousInAppValue = destinations.includes('browser')
|
||||
const previousEmailValue = destinations.includes('email')
|
||||
const [inAppEnabled, setInAppEnabled] = useState(previousInAppValue)
|
||||
const [emailEnabled, setEmailEnabled] = useState(previousEmailValue)
|
||||
const loading = 'Changing Notifications Settings'
|
||||
const success = 'Changed Notification Settings!'
|
||||
const highlight = navigateToSection === key
|
||||
const highlight = navigateToSection === subscriptionTypeKey
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
inAppEnabled !== previousInAppValue ||
|
||||
emailEnabled !== previousEmailValue
|
||||
) {
|
||||
toast.promise(
|
||||
const changeSetting = (setting: 'browser' | 'email', newValue: boolean) => {
|
||||
toast
|
||||
.promise(
|
||||
updatePrivateUser(privateUser.id, {
|
||||
notificationSubscriptionTypes: {
|
||||
...privateUser.notificationSubscriptionTypes,
|
||||
[key]: filterDefined([
|
||||
inAppEnabled ? 'browser' : undefined,
|
||||
emailEnabled ? 'email' : undefined,
|
||||
]),
|
||||
[subscriptionTypeKey]: destinations.includes(setting)
|
||||
? destinations.filter((d) => d !== setting)
|
||||
: uniq([...destinations, setting]),
|
||||
},
|
||||
}),
|
||||
{
|
||||
|
@ -196,14 +196,14 @@ export function NotificationSettings(props: {
|
|||
error: 'Error changing notification settings. Try again?',
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
if (setting === 'browser') {
|
||||
setInAppEnabled(newValue)
|
||||
} else {
|
||||
setEmailEnabled(newValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [
|
||||
inAppEnabled,
|
||||
emailEnabled,
|
||||
previousInAppValue,
|
||||
previousEmailValue,
|
||||
key,
|
||||
])
|
||||
|
||||
return (
|
||||
<Row
|
||||
|
@ -217,17 +217,17 @@ export function NotificationSettings(props: {
|
|||
<span>{description}</span>
|
||||
</Row>
|
||||
<Row className={'gap-4'}>
|
||||
{!browserDisabled.includes(key) && (
|
||||
{!browserDisabled.includes(subscriptionTypeKey) && (
|
||||
<SwitchSetting
|
||||
checked={inAppEnabled}
|
||||
onChange={setInAppEnabled}
|
||||
onChange={(newVal) => changeSetting('browser', newVal)}
|
||||
label={'Web'}
|
||||
/>
|
||||
)}
|
||||
{emailsEnabled.includes(key) && (
|
||||
{emailsEnabled.includes(subscriptionTypeKey) && (
|
||||
<SwitchSetting
|
||||
checked={emailEnabled}
|
||||
onChange={setEmailEnabled}
|
||||
onChange={(newVal) => changeSetting('email', newVal)}
|
||||
label={'Email'}
|
||||
/>
|
||||
)}
|
||||
|
@ -243,17 +243,29 @@ export function NotificationSettings(props: {
|
|||
return privateUser.notificationSubscriptionTypes[key] ?? []
|
||||
}
|
||||
|
||||
const Section = (icon: ReactNode, data: sectionData) => {
|
||||
const Section = memo(function Section(props: {
|
||||
icon: ReactNode
|
||||
data: SectionData
|
||||
}) {
|
||||
const { icon, data } = props
|
||||
const { label, subscriptionTypeToDescription } = data
|
||||
const expand =
|
||||
navigateToSection &&
|
||||
Object.keys(subscriptionTypeToDescription).includes(navigateToSection)
|
||||
const [expanded, setExpanded] = useState(expand)
|
||||
|
||||
// Not sure how to prevent re-render (and collapse of an open section)
|
||||
// due to a private user settings change. Just going to persist expanded state here
|
||||
const [expanded, setExpanded] = usePersistentState(expand ?? false, {
|
||||
key:
|
||||
'NotificationsSettingsSection-' +
|
||||
Object.keys(subscriptionTypeToDescription).join('-'),
|
||||
store: storageStore(safeLocalStorage()),
|
||||
})
|
||||
|
||||
// Not working as the default value for expanded, so using a useEffect
|
||||
useEffect(() => {
|
||||
if (expand) setExpanded(true)
|
||||
}, [expand])
|
||||
}, [expand, setExpanded])
|
||||
|
||||
return (
|
||||
<Col className={clsx('ml-2 gap-2')}>
|
||||
|
@ -275,19 +287,19 @@ export function NotificationSettings(props: {
|
|||
)}
|
||||
</Row>
|
||||
<Col className={clsx(expanded ? 'block' : 'hidden', 'gap-2 p-2')}>
|
||||
{Object.entries(subscriptionTypeToDescription).map(([key, value]) =>
|
||||
NotificationSettingLine(
|
||||
value,
|
||||
key as keyof notification_subscription_types,
|
||||
getUsersSavedPreference(
|
||||
{Object.entries(subscriptionTypeToDescription).map(([key, value]) => (
|
||||
<NotificationSettingLine
|
||||
subscriptionTypeKey={key as keyof notification_subscription_types}
|
||||
destinations={getUsersSavedPreference(
|
||||
key as keyof notification_subscription_types
|
||||
)
|
||||
)
|
||||
)}
|
||||
description={value}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={'p-2'}>
|
||||
|
@ -299,20 +311,38 @@ export function NotificationSettings(props: {
|
|||
onClick={() => setShowWatchModal(true)}
|
||||
/>
|
||||
</Row>
|
||||
{Section(<ChatIcon className={'h-6 w-6'} />, comments)}
|
||||
{Section(<LightBulbIcon className={'h-6 w-6'} />, answers)}
|
||||
{Section(<TrendingUpIcon className={'h-6 w-6'} />, updates)}
|
||||
{Section(<UserIcon className={'h-6 w-6'} />, yourMarkets)}
|
||||
<Section icon={<ChatIcon className={'h-6 w-6'} />} data={comments} />
|
||||
<Section
|
||||
icon={<TrendingUpIcon className={'h-6 w-6'} />}
|
||||
data={updates}
|
||||
/>
|
||||
<Section
|
||||
icon={<LightBulbIcon className={'h-6 w-6'} />}
|
||||
data={answers}
|
||||
/>
|
||||
<Section icon={<UserIcon className={'h-6 w-6'} />} data={yourMarkets} />
|
||||
<Row className={'gap-2 text-xl text-gray-700'}>
|
||||
<span>Balance Changes</span>
|
||||
</Row>
|
||||
{Section(<CurrencyDollarIcon className={'h-6 w-6'} />, bonuses)}
|
||||
{Section(<CashIcon className={'h-6 w-6'} />, otherBalances)}
|
||||
<Section
|
||||
icon={<CurrencyDollarIcon className={'h-6 w-6'} />}
|
||||
data={bonuses}
|
||||
/>
|
||||
<Section
|
||||
icon={<CashIcon className={'h-6 w-6'} />}
|
||||
data={otherBalances}
|
||||
/>
|
||||
<Row className={'gap-2 text-xl text-gray-700'}>
|
||||
<span>General</span>
|
||||
</Row>
|
||||
{Section(<UsersIcon className={'h-6 w-6'} />, userInteractions)}
|
||||
{Section(<InboxInIcon className={'h-6 w-6'} />, generalOther)}
|
||||
<Section
|
||||
icon={<UsersIcon className={'h-6 w-6'} />}
|
||||
data={userInteractions}
|
||||
/>
|
||||
<Section
|
||||
icon={<InboxInIcon className={'h-6 w-6'} />}
|
||||
data={generalOther}
|
||||
/>
|
||||
<WatchMarketModal open={showWatchModal} setOpen={setShowWatchModal} />
|
||||
</Col>
|
||||
</div>
|
||||
|
|
|
@ -16,13 +16,14 @@ export function BettingStreakModal(props: {
|
|||
<Modal open={isOpen} setOpen={setOpen}>
|
||||
<Col className="items-center gap-4 rounded-md bg-white px-8 py-6">
|
||||
<span className={'text-8xl'}>🔥</span>
|
||||
<span className="text-xl">Daily betting streaks</span>
|
||||
<span className="text-xl">Daily prediction streaks</span>
|
||||
<Col className={'gap-2'}>
|
||||
<span className={'text-indigo-700'}>• What are they?</span>
|
||||
<span className={'ml-2'}>
|
||||
You get {formatMoney(BETTING_STREAK_BONUS_AMOUNT)} more for each day
|
||||
of consecutive betting up to {formatMoney(BETTING_STREAK_BONUS_MAX)}
|
||||
. The more days you bet in a row, the more you earn!
|
||||
of consecutive predicting up to{' '}
|
||||
{formatMoney(BETTING_STREAK_BONUS_MAX)}. The more days you predict
|
||||
in a row, the more you earn!
|
||||
</span>
|
||||
<span className={'text-indigo-700'}>
|
||||
• Where can I check my streak?
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEvent } from '../hooks/use-event'
|
||||
|
||||
export function VisibilityObserver(props: {
|
||||
|
@ -8,18 +8,16 @@ export function VisibilityObserver(props: {
|
|||
const { className } = props
|
||||
const [elem, setElem] = useState<HTMLElement | null>(null)
|
||||
const onVisibilityUpdated = useEvent(props.onVisibilityUpdated)
|
||||
const observer = useRef(
|
||||
new IntersectionObserver(([entry]) => {
|
||||
onVisibilityUpdated(entry.isIntersecting)
|
||||
}, {})
|
||||
).current
|
||||
|
||||
useEffect(() => {
|
||||
if (elem) {
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
onVisibilityUpdated(entry.isIntersecting)
|
||||
}, {})
|
||||
observer.observe(elem)
|
||||
return () => observer.unobserve(elem)
|
||||
}
|
||||
}, [elem, observer])
|
||||
}, [elem, onVisibilityUpdated])
|
||||
|
||||
return <div ref={setElem} className={className}></div>
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
|
||||
import { isEqual } from 'lodash'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Contract,
|
||||
listenForActiveContracts,
|
||||
listenForContract,
|
||||
listenForContracts,
|
||||
listenForHotContracts,
|
||||
listenForInactiveContracts,
|
||||
|
@ -62,39 +60,6 @@ export const useHotContracts = () => {
|
|||
return hotContracts
|
||||
}
|
||||
|
||||
export const useUpdatedContracts = (contracts: Contract[] | undefined) => {
|
||||
const [__, triggerUpdate] = useState(0)
|
||||
const contractDict = useRef<{ [id: string]: Contract }>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (contracts === undefined) return
|
||||
|
||||
contractDict.current = Object.fromEntries(contracts.map((c) => [c.id, c]))
|
||||
|
||||
const disposes = contracts.map((contract) => {
|
||||
const { id } = contract
|
||||
|
||||
return listenForContract(id, (contract) => {
|
||||
const curr = contractDict.current[id]
|
||||
if (!isEqual(curr, contract)) {
|
||||
contractDict.current[id] = contract as Contract
|
||||
triggerUpdate((n) => n + 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
triggerUpdate((n) => n + 1)
|
||||
|
||||
return () => {
|
||||
disposes.forEach((dispose) => dispose())
|
||||
}
|
||||
}, [!!contracts])
|
||||
|
||||
return contracts && Object.keys(contractDict.current).length > 0
|
||||
? contracts.map((c) => contractDict.current[c.id])
|
||||
: undefined
|
||||
}
|
||||
|
||||
export const usePrefetchUserBetContracts = (userId: string) => {
|
||||
const queryClient = useQueryClient()
|
||||
return queryClient.prefetchQuery(
|
||||
|
|
|
@ -426,7 +426,7 @@ export function NewContract(props: {
|
|||
<div className="form-control mb-1 items-start">
|
||||
<label className="label mb-1 gap-2">
|
||||
<span>Question closes in</span>
|
||||
<InfoTooltip text="Betting will be halted after this time (local timezone)." />
|
||||
<InfoTooltip text="Predicting will be halted after this time (local timezone)." />
|
||||
</label>
|
||||
<Row className={'w-full items-center gap-2'}>
|
||||
<ChoicesToggleGroup
|
||||
|
@ -483,7 +483,7 @@ export function NewContract(props: {
|
|||
<label className="label mb-1 gap-2">
|
||||
<span>Cost</span>
|
||||
<InfoTooltip
|
||||
text={`Cost to create your question. This amount is used to subsidize betting.`}
|
||||
text={`Cost to create your question. This amount is used to subsidize predictions.`}
|
||||
/>
|
||||
</label>
|
||||
{!deservesFreeMarket ? (
|
||||
|
|
|
@ -28,7 +28,7 @@ export default function Home() {
|
|||
<Page>
|
||||
<Col className="pm:mx-10 gap-4 px-4 pb-6 pt-2">
|
||||
<Row className={'w-full items-center justify-between'}>
|
||||
<Title text="Edit your home page" />
|
||||
<Title text="Customize your home page" />
|
||||
<DoneButton />
|
||||
</Row>
|
||||
|
||||
|
@ -47,7 +47,11 @@ function DoneButton(props: { className?: string }) {
|
|||
|
||||
return (
|
||||
<SiteLink href="/experimental/home">
|
||||
<Button size="lg" color="blue" className={clsx(className, 'flex')}>
|
||||
<Button
|
||||
size="lg"
|
||||
color="blue"
|
||||
className={clsx(className, 'flex whitespace-nowrap')}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</SiteLink>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import Router from 'next/router'
|
||||
import {
|
||||
PencilIcon,
|
||||
AdjustmentsIcon,
|
||||
PlusSmIcon,
|
||||
ArrowSmRightIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
|
@ -26,11 +26,12 @@ import { Row } from 'web/components/layout/row'
|
|||
import { ProbChangeTable } from 'web/components/contract/prob-change-table'
|
||||
import { groupPath } from 'web/lib/firebase/groups'
|
||||
import { usePortfolioHistory } from 'web/hooks/use-portfolio-history'
|
||||
import { calculatePortfolioProfit } from 'common/calculate-metrics'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { useProbChanges } from 'web/hooks/use-prob-changes'
|
||||
import { ProfitBadge } from 'web/components/bets-list'
|
||||
import { calculatePortfolioProfit } from 'common/calculate-metrics'
|
||||
|
||||
const Home = () => {
|
||||
export default function Home() {
|
||||
const user = useUser()
|
||||
|
||||
useTracking('view home')
|
||||
|
@ -44,13 +45,13 @@ const Home = () => {
|
|||
return (
|
||||
<Page>
|
||||
<Col className="pm:mx-10 gap-4 px-4 pb-12">
|
||||
<Row className={'w-full items-center justify-between'}>
|
||||
<Title className="!mb-0" text="Home" />
|
||||
|
||||
<Row className={'mt-4 w-full items-start justify-between'}>
|
||||
<Row className="items-end gap-4">
|
||||
<Title className="!mb-1 !mt-0" text="Home" />
|
||||
<EditButton />
|
||||
</Row>
|
||||
|
||||
<DailyProfitAndBalance userId={user?.id} />
|
||||
<DailyProfitAndBalance className="" user={user} />
|
||||
</Row>
|
||||
|
||||
{sections.map((item) => {
|
||||
const { id } = item
|
||||
|
@ -97,17 +98,10 @@ function SearchSection(props: {
|
|||
followed?: boolean
|
||||
}) {
|
||||
const { label, user, sort, yourBets, followed } = props
|
||||
const href = `/home?s=${sort}`
|
||||
|
||||
return (
|
||||
<Col>
|
||||
<SiteLink className="mb-2 text-xl" href={href}>
|
||||
{label}{' '}
|
||||
<ArrowSmRightIcon
|
||||
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</SiteLink>
|
||||
<SectionHeader label={label} href={`/home?s=${sort}`} />
|
||||
<ContractSearch
|
||||
user={user}
|
||||
defaultSort={sort}
|
||||
|
@ -134,13 +128,7 @@ function GroupSection(props: {
|
|||
|
||||
return (
|
||||
<Col>
|
||||
<SiteLink className="mb-2 text-xl" href={groupPath(group.slug)}>
|
||||
{group.name}{' '}
|
||||
<ArrowSmRightIcon
|
||||
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</SiteLink>
|
||||
<SectionHeader label={group.name} href={groupPath(group.slug)} />
|
||||
<ContractSearch
|
||||
user={user}
|
||||
defaultSort={'score'}
|
||||
|
@ -159,15 +147,25 @@ function DailyMoversSection(props: { userId: string | null | undefined }) {
|
|||
|
||||
return (
|
||||
<Col className="gap-2">
|
||||
<SiteLink className="text-xl" href={'/daily-movers'}>
|
||||
Daily movers{' '}
|
||||
<SectionHeader label="Daily movers" href="daily-movers" />
|
||||
<ProbChangeTable changes={changes} />
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
function SectionHeader(props: { label: string; href: string }) {
|
||||
const { label, href } = props
|
||||
|
||||
return (
|
||||
<Row className="mb-3 items-center justify-between">
|
||||
<SiteLink className="text-xl" href={href}>
|
||||
{label}{' '}
|
||||
<ArrowSmRightIcon
|
||||
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</SiteLink>
|
||||
<ProbChangeTable changes={changes} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -176,45 +174,42 @@ function EditButton(props: { className?: string }) {
|
|||
|
||||
return (
|
||||
<SiteLink href="/experimental/home/edit">
|
||||
<Button size="lg" color="gray-white" className={clsx(className, 'flex')}>
|
||||
<PencilIcon className={clsx('mr-2 h-[24px] w-5')} aria-hidden="true" />{' '}
|
||||
Edit
|
||||
<Button size="sm" color="gray-white" className={clsx(className, 'flex')}>
|
||||
<AdjustmentsIcon className={clsx('h-[24px] w-5')} aria-hidden="true" />
|
||||
</Button>
|
||||
</SiteLink>
|
||||
)
|
||||
}
|
||||
|
||||
function DailyProfitAndBalance(props: {
|
||||
userId: string | null | undefined
|
||||
user: User | null | undefined
|
||||
className?: string
|
||||
}) {
|
||||
const { userId, className } = props
|
||||
const metrics = usePortfolioHistory(userId ?? '', 'daily') ?? []
|
||||
const { user, className } = props
|
||||
const metrics = usePortfolioHistory(user?.id ?? '', 'daily') ?? []
|
||||
const [first, last] = [metrics[0], metrics[metrics.length - 1]]
|
||||
|
||||
if (first === undefined || last === undefined) return null
|
||||
|
||||
const profit =
|
||||
calculatePortfolioProfit(last) - calculatePortfolioProfit(first)
|
||||
|
||||
const balanceChange = last.balance - first.balance
|
||||
const profitPercent = profit / first.investmentValue
|
||||
|
||||
return (
|
||||
<div className={clsx(className, 'text-lg')}>
|
||||
<span className={clsx(profit >= 0 ? 'text-green-500' : 'text-red-500')}>
|
||||
{profit >= 0 && '+'}
|
||||
{formatMoney(profit)}
|
||||
</span>{' '}
|
||||
profit and{' '}
|
||||
<span
|
||||
className={clsx(balanceChange >= 0 ? 'text-green-500' : 'text-red-500')}
|
||||
>
|
||||
{balanceChange >= 0 && '+'}
|
||||
{formatMoney(balanceChange)}
|
||||
</span>{' '}
|
||||
balance today
|
||||
</div>
|
||||
<Row className={'gap-4'}>
|
||||
<Col>
|
||||
<div className="text-gray-500">Daily profit</div>
|
||||
<Row className={clsx(className, 'items-center text-lg')}>
|
||||
<span>{formatMoney(profit)}</span>{' '}
|
||||
<ProfitBadge profitPercent={profitPercent * 100} />
|
||||
</Row>
|
||||
</Col>
|
||||
<Col>
|
||||
<div className="text-gray-500">Streak</div>
|
||||
<Row className={clsx(className, 'items-center text-lg')}>
|
||||
<span>🔥 {user?.currentBettingStreak ?? 0}</span>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
|
|
@ -31,8 +31,6 @@ import { SEO } from 'web/components/SEO'
|
|||
import { Linkify } from 'web/components/linkify'
|
||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||
import { Tabs } from 'web/components/layout/tabs'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
import { Modal } from 'web/components/layout/modal'
|
||||
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||
import { ContractSearch } from 'web/components/contract-search'
|
||||
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
|
||||
|
@ -51,6 +49,7 @@ import { Spacer } from 'web/components/layout/spacer'
|
|||
import { usePost } from 'web/hooks/use-post'
|
||||
import { useAdmin } from 'web/hooks/use-admin'
|
||||
import { track } from '@amplitude/analytics-browser'
|
||||
import { SelectMarketsModal } from 'web/components/contract-select-modal'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||
|
@ -401,27 +400,12 @@ function GroupLeaderboard(props: {
|
|||
function AddContractButton(props: { group: Group; user: User }) {
|
||||
const { group, user } = props
|
||||
const [open, setOpen] = useState(false)
|
||||
const [contracts, setContracts] = useState<Contract[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const groupContractIds = useGroupContractIds(group.id)
|
||||
|
||||
async function addContractToCurrentGroup(contract: Contract) {
|
||||
if (contracts.map((c) => c.id).includes(contract.id)) {
|
||||
setContracts(contracts.filter((c) => c.id !== contract.id))
|
||||
} else setContracts([...contracts, contract])
|
||||
}
|
||||
|
||||
async function doneAddingContracts() {
|
||||
Promise.all(
|
||||
contracts.map(async (contract) => {
|
||||
setLoading(true)
|
||||
await addContractToGroup(group, contract, user.id)
|
||||
})
|
||||
).then(() => {
|
||||
setLoading(false)
|
||||
setOpen(false)
|
||||
setContracts([])
|
||||
})
|
||||
async function onSubmit(contracts: Contract[]) {
|
||||
await Promise.all(
|
||||
contracts.map((contract) => addContractToGroup(group, contract, user.id))
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -437,18 +421,11 @@ function AddContractButton(props: { group: Group; user: User }) {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
<SelectMarketsModal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
className={'max-w-4xl sm:p-0'}
|
||||
size={'xl'}
|
||||
>
|
||||
<Col
|
||||
className={'min-h-screen w-full max-w-4xl gap-4 rounded-md bg-white'}
|
||||
>
|
||||
<Col className="p-8 pb-0">
|
||||
<div className={'text-xl text-indigo-700'}>Add markets</div>
|
||||
|
||||
title="Add markets"
|
||||
description={
|
||||
<div className={'text-md my-4 text-gray-600'}>
|
||||
Add pre-existing markets to this group, or{' '}
|
||||
<Link href={`/create?groupId=${group.id}`}>
|
||||
|
@ -458,50 +435,13 @@ function AddContractButton(props: { group: Group; user: User }) {
|
|||
</Link>
|
||||
.
|
||||
</div>
|
||||
|
||||
{contracts.length > 0 && (
|
||||
<Col className={'w-full '}>
|
||||
{!loading ? (
|
||||
<Row className={'justify-end gap-4'}>
|
||||
<Button onClick={doneAddingContracts} color={'indigo'}>
|
||||
Add {contracts.length} question
|
||||
{contracts.length > 1 && 's'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setContracts([])
|
||||
}}
|
||||
color={'gray'}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Row>
|
||||
) : (
|
||||
<Row className={'justify-center'}>
|
||||
<LoadingIndicator />
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
<div className={'overflow-y-scroll sm:px-8'}>
|
||||
<ContractSearch
|
||||
user={user}
|
||||
hideOrderSelector={true}
|
||||
onContractClick={addContractToCurrentGroup}
|
||||
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
||||
additionalFilter={{
|
||||
excludeContractIds: groupContractIds,
|
||||
}}
|
||||
highlightOptions={{
|
||||
contractIds: contracts.map((c) => c.id),
|
||||
highlightClassName: '!bg-indigo-100 border-indigo-100 border-2',
|
||||
}
|
||||
submitLabel={(len) => `Add ${len} question${len !== 1 ? 's' : ''}`}
|
||||
onSubmit={onSubmit}
|
||||
contractSearchOptions={{
|
||||
additionalFilter: { excludeContractIds: groupContractIds },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -112,6 +112,7 @@ export default function Notifications() {
|
|||
content: (
|
||||
<NotificationSettings
|
||||
navigateToSection={navigateToSection}
|
||||
privateUser={privateUser}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
@ -428,7 +429,7 @@ function IncomeNotificationItem(props: {
|
|||
reasonText = !simple
|
||||
? `Bonus for ${
|
||||
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
|
||||
} new traders on`
|
||||
} new predictors on`
|
||||
: 'bonus on'
|
||||
} else if (sourceType === 'tip') {
|
||||
reasonText = !simple ? `tipped you on` : `in tips on`
|
||||
|
@ -436,7 +437,7 @@ function IncomeNotificationItem(props: {
|
|||
if (sourceText && +sourceText === 50) reasonText = '(max) for your'
|
||||
else reasonText = 'for your'
|
||||
} else if (sourceType === 'loan' && sourceText) {
|
||||
reasonText = `of your invested bets returned as a`
|
||||
reasonText = `of your invested predictions returned as a`
|
||||
// TODO: support just 'like' notification without a tip
|
||||
} else if (sourceType === 'tip_and_like' && sourceText) {
|
||||
reasonText = !simple ? `liked` : `in likes on`
|
||||
|
@ -448,7 +449,9 @@ function IncomeNotificationItem(props: {
|
|||
: user?.currentBettingStreak ?? 0
|
||||
const bettingStreakText =
|
||||
sourceType === 'betting_streak_bonus' &&
|
||||
(sourceText ? `🔥 ${streakInDays} day Betting Streak` : 'Betting Streak')
|
||||
(sourceText
|
||||
? `🔥 ${streakInDays} day Prediction Streak`
|
||||
: 'Prediction Streak')
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -546,7 +549,7 @@ function IncomeNotificationItem(props: {
|
|||
{(isTip || isUniqueBettorBonus) && (
|
||||
<MultiUserTransactionLink
|
||||
userInfos={userLinks}
|
||||
modalLabel={isTip ? 'Who tipped you' : 'Unique traders'}
|
||||
modalLabel={isTip ? 'Who tipped you' : 'Unique predictors'}
|
||||
/>
|
||||
)}
|
||||
<Row className={'line-clamp-2 flex max-w-xl'}>
|
||||
|
|
|
@ -155,7 +155,7 @@ export default function TournamentPage(props: { sections: SectionInfo[] }) {
|
|||
<Page>
|
||||
<SEO
|
||||
title="Tournaments"
|
||||
description="Win money by betting in forecasting touraments on current events, sports, science, and more"
|
||||
description="Win money by predicting in forecasting tournaments on current events, sports, science, and more"
|
||||
/>
|
||||
<Col className="m-4 gap-10 sm:mx-10 sm:gap-24 xl:w-[125%]">
|
||||
{sections.map(
|
||||
|
|
Loading…
Reference in New Issue
Block a user