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 { Contract } from '../../common/contract'
|
||||||
import { getPrivateUser, getValues } from './utils'
|
import { getPrivateUser, getValues } from './utils'
|
||||||
import { Comment } from '../../common/comment'
|
import { Comment } from '../../common/comment'
|
||||||
import { uniq } from 'lodash'
|
import { groupBy, uniq } from 'lodash'
|
||||||
import { Bet, LimitBet } from '../../common/bet'
|
import { Bet, LimitBet } from '../../common/bet'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
import { getContractBetMetrics } from '../../common/calculate'
|
import { getContractBetMetrics } from '../../common/calculate'
|
||||||
|
@ -23,6 +23,7 @@ import {
|
||||||
sendNewAnswerEmail,
|
sendNewAnswerEmail,
|
||||||
sendNewCommentEmail,
|
sendNewCommentEmail,
|
||||||
sendNewFollowedMarketEmail,
|
sendNewFollowedMarketEmail,
|
||||||
|
sendNewUniqueBettorsEmail,
|
||||||
} from './emails'
|
} from './emails'
|
||||||
import { filterDefined } from '../../common/util/array'
|
import { filterDefined } from '../../common/util/array'
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
@ -774,17 +775,16 @@ export const createUniqueBettorBonusNotification = async (
|
||||||
txnId: string,
|
txnId: string,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
amount: number,
|
amount: number,
|
||||||
|
uniqueBettorIds: string[],
|
||||||
idempotencyKey: string
|
idempotencyKey: string
|
||||||
) => {
|
) => {
|
||||||
console.log('createUniqueBettorBonusNotification')
|
|
||||||
const privateUser = await getPrivateUser(contractCreatorId)
|
const privateUser = await getPrivateUser(contractCreatorId)
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
const { sendToBrowser } = await getDestinationsForUser(
|
const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
'unique_bettors_on_your_contract'
|
'unique_bettors_on_your_contract'
|
||||||
)
|
)
|
||||||
if (!sendToBrowser) return
|
if (sendToBrowser) {
|
||||||
|
|
||||||
const notificationRef = firestore
|
const notificationRef = firestore
|
||||||
.collection(`/users/${contractCreatorId}/notifications`)
|
.collection(`/users/${contractCreatorId}/notifications`)
|
||||||
.doc(idempotencyKey)
|
.doc(idempotencyKey)
|
||||||
|
@ -809,9 +809,50 @@ export const createUniqueBettorBonusNotification = async (
|
||||||
sourceContractTitle: contract.question,
|
sourceContractTitle: contract.question,
|
||||||
sourceContractCreatorUsername: contract.creatorUsername,
|
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 (
|
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,
|
notification_reason_types,
|
||||||
getDestinationsForUser,
|
getDestinationsForUser,
|
||||||
} from '../../common/notification'
|
} from '../../common/notification'
|
||||||
|
import { Dictionary } from 'lodash'
|
||||||
|
|
||||||
export const sendMarketResolutionEmail = async (
|
export const sendMarketResolutionEmail = async (
|
||||||
reason: notification_reason_types,
|
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 firestore = admin.firestore()
|
||||||
const BONUS_START_DATE = new Date('2022-07-13T15:30:00.000Z').getTime()
|
const BONUS_START_DATE = new Date('2022-07-13T15:30:00.000Z').getTime()
|
||||||
|
|
||||||
export const onCreateBet = functions.firestore
|
export const onCreateBet = functions
|
||||||
.document('contracts/{contractId}/bets/{betId}')
|
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||||
|
.firestore.document('contracts/{contractId}/bets/{betId}')
|
||||||
.onCreate(async (change, context) => {
|
.onCreate(async (change, context) => {
|
||||||
const { contractId } = context.params as {
|
const { contractId } = context.params as {
|
||||||
contractId: string
|
contractId: string
|
||||||
|
@ -198,6 +199,7 @@ const updateUniqueBettorsAndGiveCreatorBonus = async (
|
||||||
result.txn.id,
|
result.txn.id,
|
||||||
contract,
|
contract,
|
||||||
result.txn.amount,
|
result.txn.amount,
|
||||||
|
newUniqueBettorIds,
|
||||||
eventId + '-unique-bettor-bonus'
|
eventId + '-unique-bettor-bonus'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,9 +111,9 @@ export const getHomeItems = (groups: Group[], sections: string[]) => {
|
||||||
if (!isArray(sections)) sections = []
|
if (!isArray(sections)) sections = []
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{ label: 'Daily movers', id: 'daily-movers' },
|
|
||||||
{ label: 'Trending', id: 'score' },
|
{ label: 'Trending', id: 'score' },
|
||||||
{ label: 'New for you', id: 'newest' },
|
{ label: 'New for you', id: 'newest' },
|
||||||
|
{ label: 'Daily movers', id: 'daily-movers' },
|
||||||
...groups.map((g) => ({
|
...groups.map((g) => ({
|
||||||
label: g.name,
|
label: g.name,
|
||||||
id: g.id,
|
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
|
const { profitPercent, className } = props
|
||||||
if (!profitPercent) return null
|
if (!profitPercent) return null
|
||||||
const colors =
|
const colors =
|
||||||
|
|
|
@ -200,7 +200,7 @@ export function ContractSearch(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="h-full">
|
<Col>
|
||||||
<ContractSearchControls
|
<ContractSearchControls
|
||||||
className={headerClassName}
|
className={headerClassName}
|
||||||
defaultSort={defaultSort}
|
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
|
<Tooltip
|
||||||
text={`${formatMoney(
|
text={`${formatMoney(
|
||||||
volume
|
volume
|
||||||
)} bet - ${uniqueBettors} unique traders`}
|
)} bet - ${uniqueBettors} unique predictors`}
|
||||||
>
|
>
|
||||||
{volumeTranslation}
|
{volumeTranslation}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
import { Editor } from '@tiptap/react'
|
import { Editor } from '@tiptap/react'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { useState } from 'react'
|
import { SelectMarketsModal } from '../contract-select-modal'
|
||||||
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 { embedContractCode, embedContractGridCode } from '../share-embed-button'
|
import { embedContractCode, embedContractGridCode } from '../share-embed-button'
|
||||||
import { insertContent } from './utils'
|
import { insertContent } from './utils'
|
||||||
|
|
||||||
|
@ -17,83 +11,23 @@ export function MarketModal(props: {
|
||||||
}) {
|
}) {
|
||||||
const { editor, open, setOpen } = props
|
const { editor, open, setOpen } = props
|
||||||
|
|
||||||
const [contracts, setContracts] = useState<Contract[]>([])
|
function onSubmit(contracts: 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)
|
|
||||||
if (contracts.length == 1) {
|
if (contracts.length == 1) {
|
||||||
insertContent(editor, embedContractCode(contracts[0]))
|
insertContent(editor, embedContractCode(contracts[0]))
|
||||||
} else if (contracts.length > 1) {
|
} else if (contracts.length > 1) {
|
||||||
insertContent(editor, embedContractGridCode(contracts))
|
insertContent(editor, embedContractGridCode(contracts))
|
||||||
}
|
}
|
||||||
setLoading(false)
|
|
||||||
setOpen(false)
|
|
||||||
setContracts([])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}>
|
<SelectMarketsModal
|
||||||
<Col className="h-[85vh] w-full gap-4 rounded-md bg-white">
|
title="Embed markets"
|
||||||
<Row className="p-8 pb-0">
|
open={open}
|
||||||
<div className={'text-xl text-indigo-700'}>Embed a market</div>
|
setOpen={setOpen}
|
||||||
|
submitLabel={(len) =>
|
||||||
{!loading && (
|
len == 1 ? 'Embed 1 question' : `Embed grid of ${len} questions`
|
||||||
<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)
|
|
||||||
}
|
}
|
||||||
}}
|
onSubmit={onSubmit}
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Modal>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { usePrivateUser } from 'web/hooks/use-user'
|
import React, { memo, ReactNode, useEffect, useState } from 'react'
|
||||||
import React, { ReactNode, useEffect, useState } from 'react'
|
|
||||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
notification_subscription_types,
|
notification_subscription_types,
|
||||||
notification_destination_types,
|
notification_destination_types,
|
||||||
|
PrivateUser,
|
||||||
} from 'common/user'
|
} from 'common/user'
|
||||||
import { updatePrivateUser } from 'web/lib/firebase/users'
|
import { updatePrivateUser } from 'web/lib/firebase/users'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
|
@ -23,21 +22,22 @@ import {
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
} from '@heroicons/react/outline'
|
} from '@heroicons/react/outline'
|
||||||
import { WatchMarketModal } from 'web/components/contract/watch-market-modal'
|
import { WatchMarketModal } from 'web/components/contract/watch-market-modal'
|
||||||
import { filterDefined } from 'common/util/array'
|
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { SwitchSetting } from 'web/components/switch-setting'
|
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: {
|
export function NotificationSettings(props: {
|
||||||
navigateToSection: string | undefined
|
navigateToSection: string | undefined
|
||||||
|
privateUser: PrivateUser
|
||||||
}) {
|
}) {
|
||||||
const { navigateToSection } = props
|
const { navigateToSection, privateUser } = props
|
||||||
const privateUser = usePrivateUser()
|
|
||||||
const [showWatchModal, setShowWatchModal] = useState(false)
|
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> = [
|
const emailsEnabled: Array<keyof notification_subscription_types> = [
|
||||||
'all_comments_on_watched_markets',
|
'all_comments_on_watched_markets',
|
||||||
'all_replies_to_my_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
|
'tagged_user', // missing tagged on contract description email
|
||||||
'contract_from_followed_user',
|
'contract_from_followed_user',
|
||||||
|
'unique_bettors_on_your_contract',
|
||||||
// TODO: add these
|
// 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',
|
// 'referral_bonuses',
|
||||||
// 'unique_bettors_on_your_contract',
|
|
||||||
// 'on_new_follow',
|
// 'on_new_follow',
|
||||||
// 'profit_loss_updates',
|
|
||||||
// 'tips_on_your_markets',
|
// 'tips_on_your_markets',
|
||||||
// 'tips_on_your_comments',
|
// 'tips_on_your_comments',
|
||||||
// maybe the following?
|
// maybe the following?
|
||||||
|
@ -78,14 +81,14 @@ export function NotificationSettings(props: {
|
||||||
'thank_you_for_purchases',
|
'thank_you_for_purchases',
|
||||||
]
|
]
|
||||||
|
|
||||||
type sectionData = {
|
type SectionData = {
|
||||||
label: string
|
label: string
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypeToDescription: {
|
||||||
[key in keyof Partial<notification_subscription_types>]: string
|
[key in keyof Partial<notification_subscription_types>]: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const comments: sectionData = {
|
const comments: SectionData = {
|
||||||
label: 'New Comments',
|
label: 'New Comments',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypeToDescription: {
|
||||||
all_comments_on_watched_markets: 'All new comments',
|
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',
|
label: 'New Answers',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypeToDescription: {
|
||||||
all_answers_on_watched_markets: 'All new answers',
|
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',
|
// answers_by_market_creator_on_watched_markets: 'By market creator',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const updates: sectionData = {
|
const updates: SectionData = {
|
||||||
label: 'Updates & Resolutions',
|
label: 'Updates & Resolutions',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypeToDescription: {
|
||||||
market_updates_on_watched_markets: 'All creator updates',
|
market_updates_on_watched_markets: 'All creator updates',
|
||||||
|
@ -118,7 +121,7 @@ export function NotificationSettings(props: {
|
||||||
// probability_updates_on_watched_markets: 'Probability updates',
|
// probability_updates_on_watched_markets: 'Probability updates',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const yourMarkets: sectionData = {
|
const yourMarkets: SectionData = {
|
||||||
label: 'Markets You Created',
|
label: 'Markets You Created',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypeToDescription: {
|
||||||
your_contract_closed: 'Your market has closed (and needs resolution)',
|
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',
|
tips_on_your_markets: 'Likes on your markets',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const bonuses: sectionData = {
|
const bonuses: SectionData = {
|
||||||
label: 'Bonuses',
|
label: 'Bonuses',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypeToDescription: {
|
||||||
betting_streaks: 'Betting streak bonuses',
|
betting_streaks: 'Prediction streak bonuses',
|
||||||
referral_bonuses: 'Referral bonuses from referring users',
|
referral_bonuses: 'Referral bonuses from referring users',
|
||||||
unique_bettors_on_your_contract: 'Unique bettor bonuses on your markets',
|
unique_bettors_on_your_contract: 'Unique bettor bonuses on your markets',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const otherBalances: sectionData = {
|
const otherBalances: SectionData = {
|
||||||
label: 'Other',
|
label: 'Other',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypeToDescription: {
|
||||||
loan_income: 'Automatic loans from your profitable bets',
|
loan_income: 'Automatic loans from your profitable bets',
|
||||||
|
@ -144,7 +147,7 @@ export function NotificationSettings(props: {
|
||||||
tips_on_your_comments: 'Tips on your comments',
|
tips_on_your_comments: 'Tips on your comments',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const userInteractions: sectionData = {
|
const userInteractions: SectionData = {
|
||||||
label: 'Users',
|
label: 'Users',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypeToDescription: {
|
||||||
tagged_user: 'A user tagged you',
|
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',
|
contract_from_followed_user: 'New markets created by users you follow',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const generalOther: sectionData = {
|
const generalOther: SectionData = {
|
||||||
label: 'Other',
|
label: 'Other',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypeToDescription: {
|
||||||
trending_markets: 'Weekly interesting markets',
|
trending_markets: 'Weekly interesting markets',
|
||||||
|
@ -162,32 +165,29 @@ export function NotificationSettings(props: {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const NotificationSettingLine = (
|
function NotificationSettingLine(props: {
|
||||||
description: string,
|
description: string
|
||||||
key: keyof notification_subscription_types,
|
subscriptionTypeKey: keyof notification_subscription_types
|
||||||
value: notification_destination_types[]
|
destinations: notification_destination_types[]
|
||||||
) => {
|
}) {
|
||||||
const previousInAppValue = value.includes('browser')
|
const { description, subscriptionTypeKey, destinations } = props
|
||||||
const previousEmailValue = value.includes('email')
|
const previousInAppValue = destinations.includes('browser')
|
||||||
|
const previousEmailValue = destinations.includes('email')
|
||||||
const [inAppEnabled, setInAppEnabled] = useState(previousInAppValue)
|
const [inAppEnabled, setInAppEnabled] = useState(previousInAppValue)
|
||||||
const [emailEnabled, setEmailEnabled] = useState(previousEmailValue)
|
const [emailEnabled, setEmailEnabled] = useState(previousEmailValue)
|
||||||
const loading = 'Changing Notifications Settings'
|
const loading = 'Changing Notifications Settings'
|
||||||
const success = 'Changed Notification Settings!'
|
const success = 'Changed Notification Settings!'
|
||||||
const highlight = navigateToSection === key
|
const highlight = navigateToSection === subscriptionTypeKey
|
||||||
|
|
||||||
useEffect(() => {
|
const changeSetting = (setting: 'browser' | 'email', newValue: boolean) => {
|
||||||
if (
|
toast
|
||||||
inAppEnabled !== previousInAppValue ||
|
.promise(
|
||||||
emailEnabled !== previousEmailValue
|
|
||||||
) {
|
|
||||||
toast.promise(
|
|
||||||
updatePrivateUser(privateUser.id, {
|
updatePrivateUser(privateUser.id, {
|
||||||
notificationSubscriptionTypes: {
|
notificationSubscriptionTypes: {
|
||||||
...privateUser.notificationSubscriptionTypes,
|
...privateUser.notificationSubscriptionTypes,
|
||||||
[key]: filterDefined([
|
[subscriptionTypeKey]: destinations.includes(setting)
|
||||||
inAppEnabled ? 'browser' : undefined,
|
? destinations.filter((d) => d !== setting)
|
||||||
emailEnabled ? 'email' : undefined,
|
: uniq([...destinations, setting]),
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
@ -196,14 +196,14 @@ export function NotificationSettings(props: {
|
||||||
error: 'Error changing notification settings. Try again?',
|
error: 'Error changing notification settings. Try again?',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (setting === 'browser') {
|
||||||
|
setInAppEnabled(newValue)
|
||||||
|
} else {
|
||||||
|
setEmailEnabled(newValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [
|
|
||||||
inAppEnabled,
|
|
||||||
emailEnabled,
|
|
||||||
previousInAppValue,
|
|
||||||
previousEmailValue,
|
|
||||||
key,
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
|
@ -217,17 +217,17 @@ export function NotificationSettings(props: {
|
||||||
<span>{description}</span>
|
<span>{description}</span>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className={'gap-4'}>
|
<Row className={'gap-4'}>
|
||||||
{!browserDisabled.includes(key) && (
|
{!browserDisabled.includes(subscriptionTypeKey) && (
|
||||||
<SwitchSetting
|
<SwitchSetting
|
||||||
checked={inAppEnabled}
|
checked={inAppEnabled}
|
||||||
onChange={setInAppEnabled}
|
onChange={(newVal) => changeSetting('browser', newVal)}
|
||||||
label={'Web'}
|
label={'Web'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{emailsEnabled.includes(key) && (
|
{emailsEnabled.includes(subscriptionTypeKey) && (
|
||||||
<SwitchSetting
|
<SwitchSetting
|
||||||
checked={emailEnabled}
|
checked={emailEnabled}
|
||||||
onChange={setEmailEnabled}
|
onChange={(newVal) => changeSetting('email', newVal)}
|
||||||
label={'Email'}
|
label={'Email'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -243,17 +243,29 @@ export function NotificationSettings(props: {
|
||||||
return privateUser.notificationSubscriptionTypes[key] ?? []
|
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 { label, subscriptionTypeToDescription } = data
|
||||||
const expand =
|
const expand =
|
||||||
navigateToSection &&
|
navigateToSection &&
|
||||||
Object.keys(subscriptionTypeToDescription).includes(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
|
// Not working as the default value for expanded, so using a useEffect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (expand) setExpanded(true)
|
if (expand) setExpanded(true)
|
||||||
}, [expand])
|
}, [expand, setExpanded])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={clsx('ml-2 gap-2')}>
|
<Col className={clsx('ml-2 gap-2')}>
|
||||||
|
@ -275,19 +287,19 @@ export function NotificationSettings(props: {
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<Col className={clsx(expanded ? 'block' : 'hidden', 'gap-2 p-2')}>
|
<Col className={clsx(expanded ? 'block' : 'hidden', 'gap-2 p-2')}>
|
||||||
{Object.entries(subscriptionTypeToDescription).map(([key, value]) =>
|
{Object.entries(subscriptionTypeToDescription).map(([key, value]) => (
|
||||||
NotificationSettingLine(
|
<NotificationSettingLine
|
||||||
value,
|
subscriptionTypeKey={key as keyof notification_subscription_types}
|
||||||
key as keyof notification_subscription_types,
|
destinations={getUsersSavedPreference(
|
||||||
getUsersSavedPreference(
|
|
||||||
key as keyof notification_subscription_types
|
key as keyof notification_subscription_types
|
||||||
)
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
|
description={value}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'p-2'}>
|
<div className={'p-2'}>
|
||||||
|
@ -299,20 +311,38 @@ export function NotificationSettings(props: {
|
||||||
onClick={() => setShowWatchModal(true)}
|
onClick={() => setShowWatchModal(true)}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
{Section(<ChatIcon className={'h-6 w-6'} />, comments)}
|
<Section icon={<ChatIcon className={'h-6 w-6'} />} data={comments} />
|
||||||
{Section(<LightBulbIcon className={'h-6 w-6'} />, answers)}
|
<Section
|
||||||
{Section(<TrendingUpIcon className={'h-6 w-6'} />, updates)}
|
icon={<TrendingUpIcon className={'h-6 w-6'} />}
|
||||||
{Section(<UserIcon className={'h-6 w-6'} />, yourMarkets)}
|
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'}>
|
<Row className={'gap-2 text-xl text-gray-700'}>
|
||||||
<span>Balance Changes</span>
|
<span>Balance Changes</span>
|
||||||
</Row>
|
</Row>
|
||||||
{Section(<CurrencyDollarIcon className={'h-6 w-6'} />, bonuses)}
|
<Section
|
||||||
{Section(<CashIcon className={'h-6 w-6'} />, otherBalances)}
|
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'}>
|
<Row className={'gap-2 text-xl text-gray-700'}>
|
||||||
<span>General</span>
|
<span>General</span>
|
||||||
</Row>
|
</Row>
|
||||||
{Section(<UsersIcon className={'h-6 w-6'} />, userInteractions)}
|
<Section
|
||||||
{Section(<InboxInIcon className={'h-6 w-6'} />, generalOther)}
|
icon={<UsersIcon className={'h-6 w-6'} />}
|
||||||
|
data={userInteractions}
|
||||||
|
/>
|
||||||
|
<Section
|
||||||
|
icon={<InboxInIcon className={'h-6 w-6'} />}
|
||||||
|
data={generalOther}
|
||||||
|
/>
|
||||||
<WatchMarketModal open={showWatchModal} setOpen={setShowWatchModal} />
|
<WatchMarketModal open={showWatchModal} setOpen={setShowWatchModal} />
|
||||||
</Col>
|
</Col>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,13 +16,14 @@ export function BettingStreakModal(props: {
|
||||||
<Modal open={isOpen} setOpen={setOpen}>
|
<Modal open={isOpen} setOpen={setOpen}>
|
||||||
<Col className="items-center gap-4 rounded-md bg-white px-8 py-6">
|
<Col className="items-center gap-4 rounded-md bg-white px-8 py-6">
|
||||||
<span className={'text-8xl'}>🔥</span>
|
<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'}>
|
<Col className={'gap-2'}>
|
||||||
<span className={'text-indigo-700'}>• What are they?</span>
|
<span className={'text-indigo-700'}>• What are they?</span>
|
||||||
<span className={'ml-2'}>
|
<span className={'ml-2'}>
|
||||||
You get {formatMoney(BETTING_STREAK_BONUS_AMOUNT)} more for each day
|
You get {formatMoney(BETTING_STREAK_BONUS_AMOUNT)} more for each day
|
||||||
of consecutive betting up to {formatMoney(BETTING_STREAK_BONUS_MAX)}
|
of consecutive predicting up to{' '}
|
||||||
. The more days you bet in a row, the more you earn!
|
{formatMoney(BETTING_STREAK_BONUS_MAX)}. The more days you predict
|
||||||
|
in a row, the more you earn!
|
||||||
</span>
|
</span>
|
||||||
<span className={'text-indigo-700'}>
|
<span className={'text-indigo-700'}>
|
||||||
• Where can I check my streak?
|
• 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'
|
import { useEvent } from '../hooks/use-event'
|
||||||
|
|
||||||
export function VisibilityObserver(props: {
|
export function VisibilityObserver(props: {
|
||||||
|
@ -8,18 +8,16 @@ export function VisibilityObserver(props: {
|
||||||
const { className } = props
|
const { className } = props
|
||||||
const [elem, setElem] = useState<HTMLElement | null>(null)
|
const [elem, setElem] = useState<HTMLElement | null>(null)
|
||||||
const onVisibilityUpdated = useEvent(props.onVisibilityUpdated)
|
const onVisibilityUpdated = useEvent(props.onVisibilityUpdated)
|
||||||
const observer = useRef(
|
|
||||||
new IntersectionObserver(([entry]) => {
|
|
||||||
onVisibilityUpdated(entry.isIntersecting)
|
|
||||||
}, {})
|
|
||||||
).current
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (elem) {
|
if (elem) {
|
||||||
|
const observer = new IntersectionObserver(([entry]) => {
|
||||||
|
onVisibilityUpdated(entry.isIntersecting)
|
||||||
|
}, {})
|
||||||
observer.observe(elem)
|
observer.observe(elem)
|
||||||
return () => observer.unobserve(elem)
|
return () => observer.unobserve(elem)
|
||||||
}
|
}
|
||||||
}, [elem, observer])
|
}, [elem, onVisibilityUpdated])
|
||||||
|
|
||||||
return <div ref={setElem} className={className}></div>
|
return <div ref={setElem} className={className}></div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
|
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
|
||||||
import { isEqual } from 'lodash'
|
import { useEffect, useState } from 'react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
listenForActiveContracts,
|
listenForActiveContracts,
|
||||||
listenForContract,
|
|
||||||
listenForContracts,
|
listenForContracts,
|
||||||
listenForHotContracts,
|
listenForHotContracts,
|
||||||
listenForInactiveContracts,
|
listenForInactiveContracts,
|
||||||
|
@ -62,39 +60,6 @@ export const useHotContracts = () => {
|
||||||
return hotContracts
|
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) => {
|
export const usePrefetchUserBetContracts = (userId: string) => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
return queryClient.prefetchQuery(
|
return queryClient.prefetchQuery(
|
||||||
|
|
|
@ -426,7 +426,7 @@ export function NewContract(props: {
|
||||||
<div className="form-control mb-1 items-start">
|
<div className="form-control mb-1 items-start">
|
||||||
<label className="label mb-1 gap-2">
|
<label className="label mb-1 gap-2">
|
||||||
<span>Question closes in</span>
|
<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>
|
</label>
|
||||||
<Row className={'w-full items-center gap-2'}>
|
<Row className={'w-full items-center gap-2'}>
|
||||||
<ChoicesToggleGroup
|
<ChoicesToggleGroup
|
||||||
|
@ -483,7 +483,7 @@ export function NewContract(props: {
|
||||||
<label className="label mb-1 gap-2">
|
<label className="label mb-1 gap-2">
|
||||||
<span>Cost</span>
|
<span>Cost</span>
|
||||||
<InfoTooltip
|
<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>
|
</label>
|
||||||
{!deservesFreeMarket ? (
|
{!deservesFreeMarket ? (
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default function Home() {
|
||||||
<Page>
|
<Page>
|
||||||
<Col className="pm:mx-10 gap-4 px-4 pb-6 pt-2">
|
<Col className="pm:mx-10 gap-4 px-4 pb-6 pt-2">
|
||||||
<Row className={'w-full items-center justify-between'}>
|
<Row className={'w-full items-center justify-between'}>
|
||||||
<Title text="Edit your home page" />
|
<Title text="Customize your home page" />
|
||||||
<DoneButton />
|
<DoneButton />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
@ -47,7 +47,11 @@ function DoneButton(props: { className?: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteLink href="/experimental/home">
|
<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
|
Done
|
||||||
</Button>
|
</Button>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Router from 'next/router'
|
import Router from 'next/router'
|
||||||
import {
|
import {
|
||||||
PencilIcon,
|
AdjustmentsIcon,
|
||||||
PlusSmIcon,
|
PlusSmIcon,
|
||||||
ArrowSmRightIcon,
|
ArrowSmRightIcon,
|
||||||
} from '@heroicons/react/solid'
|
} 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 { ProbChangeTable } from 'web/components/contract/prob-change-table'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
import { usePortfolioHistory } from 'web/hooks/use-portfolio-history'
|
import { usePortfolioHistory } from 'web/hooks/use-portfolio-history'
|
||||||
import { calculatePortfolioProfit } from 'common/calculate-metrics'
|
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { useProbChanges } 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'
|
||||||
|
|
||||||
const Home = () => {
|
export default function Home() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
useTracking('view home')
|
useTracking('view home')
|
||||||
|
@ -44,13 +45,13 @@ const Home = () => {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Col className="pm:mx-10 gap-4 px-4 pb-12">
|
<Col className="pm:mx-10 gap-4 px-4 pb-12">
|
||||||
<Row className={'w-full items-center justify-between'}>
|
<Row className={'mt-4 w-full items-start justify-between'}>
|
||||||
<Title className="!mb-0" text="Home" />
|
<Row className="items-end gap-4">
|
||||||
|
<Title className="!mb-1 !mt-0" text="Home" />
|
||||||
<EditButton />
|
<EditButton />
|
||||||
</Row>
|
</Row>
|
||||||
|
<DailyProfitAndBalance className="" user={user} />
|
||||||
<DailyProfitAndBalance userId={user?.id} />
|
</Row>
|
||||||
|
|
||||||
{sections.map((item) => {
|
{sections.map((item) => {
|
||||||
const { id } = item
|
const { id } = item
|
||||||
|
@ -97,17 +98,10 @@ function SearchSection(props: {
|
||||||
followed?: boolean
|
followed?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { label, user, sort, yourBets, followed } = props
|
const { label, user, sort, yourBets, followed } = props
|
||||||
const href = `/home?s=${sort}`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<SiteLink className="mb-2 text-xl" href={href}>
|
<SectionHeader label={label} href={`/home?s=${sort}`} />
|
||||||
{label}{' '}
|
|
||||||
<ArrowSmRightIcon
|
|
||||||
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</SiteLink>
|
|
||||||
<ContractSearch
|
<ContractSearch
|
||||||
user={user}
|
user={user}
|
||||||
defaultSort={sort}
|
defaultSort={sort}
|
||||||
|
@ -134,13 +128,7 @@ function GroupSection(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<SiteLink className="mb-2 text-xl" href={groupPath(group.slug)}>
|
<SectionHeader label={group.name} href={groupPath(group.slug)} />
|
||||||
{group.name}{' '}
|
|
||||||
<ArrowSmRightIcon
|
|
||||||
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</SiteLink>
|
|
||||||
<ContractSearch
|
<ContractSearch
|
||||||
user={user}
|
user={user}
|
||||||
defaultSort={'score'}
|
defaultSort={'score'}
|
||||||
|
@ -159,15 +147,25 @@ function DailyMoversSection(props: { userId: string | null | undefined }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<SiteLink className="text-xl" href={'/daily-movers'}>
|
<SectionHeader label="Daily movers" href="daily-movers" />
|
||||||
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
|
<ArrowSmRightIcon
|
||||||
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
<ProbChangeTable changes={changes} />
|
</Row>
|
||||||
</Col>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,45 +174,42 @@ function EditButton(props: { className?: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteLink href="/experimental/home/edit">
|
<SiteLink href="/experimental/home/edit">
|
||||||
<Button size="lg" color="gray-white" className={clsx(className, 'flex')}>
|
<Button size="sm" color="gray-white" className={clsx(className, 'flex')}>
|
||||||
<PencilIcon className={clsx('mr-2 h-[24px] w-5')} aria-hidden="true" />{' '}
|
<AdjustmentsIcon className={clsx('h-[24px] w-5')} aria-hidden="true" />
|
||||||
Edit
|
|
||||||
</Button>
|
</Button>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DailyProfitAndBalance(props: {
|
function DailyProfitAndBalance(props: {
|
||||||
userId: string | null | undefined
|
user: User | null | undefined
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { userId, className } = props
|
const { user, className } = props
|
||||||
const metrics = usePortfolioHistory(userId ?? '', 'daily') ?? []
|
const metrics = usePortfolioHistory(user?.id ?? '', 'daily') ?? []
|
||||||
const [first, last] = [metrics[0], metrics[metrics.length - 1]]
|
const [first, last] = [metrics[0], metrics[metrics.length - 1]]
|
||||||
|
|
||||||
if (first === undefined || last === undefined) return null
|
if (first === undefined || last === undefined) return null
|
||||||
|
|
||||||
const profit =
|
const profit =
|
||||||
calculatePortfolioProfit(last) - calculatePortfolioProfit(first)
|
calculatePortfolioProfit(last) - calculatePortfolioProfit(first)
|
||||||
|
const profitPercent = profit / first.investmentValue
|
||||||
const balanceChange = last.balance - first.balance
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(className, 'text-lg')}>
|
<Row className={'gap-4'}>
|
||||||
<span className={clsx(profit >= 0 ? 'text-green-500' : 'text-red-500')}>
|
<Col>
|
||||||
{profit >= 0 && '+'}
|
<div className="text-gray-500">Daily profit</div>
|
||||||
{formatMoney(profit)}
|
<Row className={clsx(className, 'items-center text-lg')}>
|
||||||
</span>{' '}
|
<span>{formatMoney(profit)}</span>{' '}
|
||||||
profit and{' '}
|
<ProfitBadge profitPercent={profitPercent * 100} />
|
||||||
<span
|
</Row>
|
||||||
className={clsx(balanceChange >= 0 ? 'text-green-500' : 'text-red-500')}
|
</Col>
|
||||||
>
|
<Col>
|
||||||
{balanceChange >= 0 && '+'}
|
<div className="text-gray-500">Streak</div>
|
||||||
{formatMoney(balanceChange)}
|
<Row className={clsx(className, 'items-center text-lg')}>
|
||||||
</span>{' '}
|
<span>🔥 {user?.currentBettingStreak ?? 0}</span>
|
||||||
balance today
|
</Row>
|
||||||
</div>
|
</Col>
|
||||||
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home
|
|
||||||
|
|
|
@ -31,8 +31,6 @@ import { SEO } from 'web/components/SEO'
|
||||||
import { Linkify } from 'web/components/linkify'
|
import { Linkify } from 'web/components/linkify'
|
||||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||||
import { Tabs } from 'web/components/layout/tabs'
|
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 { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||||
import { ContractSearch } from 'web/components/contract-search'
|
import { ContractSearch } from 'web/components/contract-search'
|
||||||
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
|
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 { usePost } from 'web/hooks/use-post'
|
||||||
import { useAdmin } from 'web/hooks/use-admin'
|
import { useAdmin } from 'web/hooks/use-admin'
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
|
import { SelectMarketsModal } from 'web/components/contract-select-modal'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
|
@ -401,27 +400,12 @@ function GroupLeaderboard(props: {
|
||||||
function AddContractButton(props: { group: Group; user: User }) {
|
function AddContractButton(props: { group: Group; user: User }) {
|
||||||
const { group, user } = props
|
const { group, user } = props
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [contracts, setContracts] = useState<Contract[]>([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const groupContractIds = useGroupContractIds(group.id)
|
const groupContractIds = useGroupContractIds(group.id)
|
||||||
|
|
||||||
async function addContractToCurrentGroup(contract: Contract) {
|
async function onSubmit(contracts: Contract[]) {
|
||||||
if (contracts.map((c) => c.id).includes(contract.id)) {
|
await Promise.all(
|
||||||
setContracts(contracts.filter((c) => c.id !== contract.id))
|
contracts.map((contract) => addContractToGroup(group, contract, user.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([])
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -437,18 +421,11 @@ function AddContractButton(props: { group: Group; user: User }) {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<SelectMarketsModal
|
||||||
open={open}
|
open={open}
|
||||||
setOpen={setOpen}
|
setOpen={setOpen}
|
||||||
className={'max-w-4xl sm:p-0'}
|
title="Add markets"
|
||||||
size={'xl'}
|
description={
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className={'text-md my-4 text-gray-600'}>
|
<div className={'text-md my-4 text-gray-600'}>
|
||||||
Add pre-existing markets to this group, or{' '}
|
Add pre-existing markets to this group, or{' '}
|
||||||
<Link href={`/create?groupId=${group.id}`}>
|
<Link href={`/create?groupId=${group.id}`}>
|
||||||
|
@ -458,50 +435,13 @@ function AddContractButton(props: { group: Group; user: User }) {
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
{contracts.length > 0 && (
|
submitLabel={(len) => `Add ${len} question${len !== 1 ? 's' : ''}`}
|
||||||
<Col className={'w-full '}>
|
onSubmit={onSubmit}
|
||||||
{!loading ? (
|
contractSearchOptions={{
|
||||||
<Row className={'justify-end gap-4'}>
|
additionalFilter: { excludeContractIds: groupContractIds },
|
||||||
<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',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Modal>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,7 @@ export default function Notifications() {
|
||||||
content: (
|
content: (
|
||||||
<NotificationSettings
|
<NotificationSettings
|
||||||
navigateToSection={navigateToSection}
|
navigateToSection={navigateToSection}
|
||||||
|
privateUser={privateUser}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -428,7 +429,7 @@ function IncomeNotificationItem(props: {
|
||||||
reasonText = !simple
|
reasonText = !simple
|
||||||
? `Bonus for ${
|
? `Bonus for ${
|
||||||
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
|
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
|
||||||
} new traders on`
|
} new predictors on`
|
||||||
: 'bonus on'
|
: 'bonus on'
|
||||||
} else if (sourceType === 'tip') {
|
} else if (sourceType === 'tip') {
|
||||||
reasonText = !simple ? `tipped you on` : `in tips on`
|
reasonText = !simple ? `tipped you on` : `in tips on`
|
||||||
|
@ -436,7 +437,7 @@ function IncomeNotificationItem(props: {
|
||||||
if (sourceText && +sourceText === 50) reasonText = '(max) for your'
|
if (sourceText && +sourceText === 50) reasonText = '(max) for your'
|
||||||
else reasonText = 'for your'
|
else reasonText = 'for your'
|
||||||
} else if (sourceType === 'loan' && sourceText) {
|
} else if (sourceType === 'loan' && sourceText) {
|
||||||
reasonText = `of your invested bets returned as a`
|
reasonText = `of your invested predictions returned as a`
|
||||||
// TODO: support just 'like' notification without a tip
|
// TODO: support just 'like' notification without a tip
|
||||||
} else if (sourceType === 'tip_and_like' && sourceText) {
|
} else if (sourceType === 'tip_and_like' && sourceText) {
|
||||||
reasonText = !simple ? `liked` : `in likes on`
|
reasonText = !simple ? `liked` : `in likes on`
|
||||||
|
@ -448,7 +449,9 @@ function IncomeNotificationItem(props: {
|
||||||
: user?.currentBettingStreak ?? 0
|
: user?.currentBettingStreak ?? 0
|
||||||
const bettingStreakText =
|
const bettingStreakText =
|
||||||
sourceType === 'betting_streak_bonus' &&
|
sourceType === 'betting_streak_bonus' &&
|
||||||
(sourceText ? `🔥 ${streakInDays} day Betting Streak` : 'Betting Streak')
|
(sourceText
|
||||||
|
? `🔥 ${streakInDays} day Prediction Streak`
|
||||||
|
: 'Prediction Streak')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -546,7 +549,7 @@ function IncomeNotificationItem(props: {
|
||||||
{(isTip || isUniqueBettorBonus) && (
|
{(isTip || isUniqueBettorBonus) && (
|
||||||
<MultiUserTransactionLink
|
<MultiUserTransactionLink
|
||||||
userInfos={userLinks}
|
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'}>
|
<Row className={'line-clamp-2 flex max-w-xl'}>
|
||||||
|
|
|
@ -155,7 +155,7 @@ export default function TournamentPage(props: { sections: SectionInfo[] }) {
|
||||||
<Page>
|
<Page>
|
||||||
<SEO
|
<SEO
|
||||||
title="Tournaments"
|
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%]">
|
<Col className="m-4 gap-10 sm:mx-10 sm:gap-24 xl:w-[125%]">
|
||||||
{sections.map(
|
{sections.map(
|
||||||
|
|
Loading…
Reference in New Issue
Block a user