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,44 +775,84 @@ 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 | ||||
| 
 | ||||
|   const notificationRef = firestore | ||||
|     .collection(`/users/${contractCreatorId}/notifications`) | ||||
|     .doc(idempotencyKey) | ||||
|   const notification: Notification = { | ||||
|     id: idempotencyKey, | ||||
|     userId: contractCreatorId, | ||||
|     reason: 'unique_bettors_on_your_contract', | ||||
|     createdTime: Date.now(), | ||||
|     isSeen: false, | ||||
|     sourceId: txnId, | ||||
|     sourceType: 'bonus', | ||||
|     sourceUpdateType: 'created', | ||||
|     sourceUserName: bettor.name, | ||||
|     sourceUserUsername: bettor.username, | ||||
|     sourceUserAvatarUrl: bettor.avatarUrl, | ||||
|     sourceText: amount.toString(), | ||||
|     sourceSlug: contract.slug, | ||||
|     sourceTitle: contract.question, | ||||
|     // Perhaps not necessary, but just in case
 | ||||
|     sourceContractSlug: contract.slug, | ||||
|     sourceContractId: contract.id, | ||||
|     sourceContractTitle: contract.question, | ||||
|     sourceContractCreatorUsername: contract.creatorUsername, | ||||
|   if (sendToBrowser) { | ||||
|     const notificationRef = firestore | ||||
|       .collection(`/users/${contractCreatorId}/notifications`) | ||||
|       .doc(idempotencyKey) | ||||
|     const notification: Notification = { | ||||
|       id: idempotencyKey, | ||||
|       userId: contractCreatorId, | ||||
|       reason: 'unique_bettors_on_your_contract', | ||||
|       createdTime: Date.now(), | ||||
|       isSeen: false, | ||||
|       sourceId: txnId, | ||||
|       sourceType: 'bonus', | ||||
|       sourceUpdateType: 'created', | ||||
|       sourceUserName: bettor.name, | ||||
|       sourceUserUsername: bettor.username, | ||||
|       sourceUserAvatarUrl: bettor.avatarUrl, | ||||
|       sourceText: amount.toString(), | ||||
|       sourceSlug: contract.slug, | ||||
|       sourceTitle: contract.question, | ||||
|       // Perhaps not necessary, but just in case
 | ||||
|       sourceContractSlug: contract.slug, | ||||
|       sourceContractId: contract.id, | ||||
|       sourceContractTitle: contract.question, | ||||
|       sourceContractCreatorUsername: contract.creatorUsername, | ||||
|     } | ||||
|     await notificationRef.set(removeUndefinedProps(notification)) | ||||
|   } | ||||
|   return 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) | ||||
|                   } | ||||
|                 }} | ||||
|                 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> | ||||
|     <SelectMarketsModal | ||||
|       title="Embed markets" | ||||
|       open={open} | ||||
|       setOpen={setOpen} | ||||
|       submitLabel={(len) => | ||||
|         len == 1 ? 'Embed 1 question' : `Embed grid of ${len} questions` | ||||
|       } | ||||
|       onSubmit={onSubmit} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|  |  | |||
|  | @ -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?', | ||||
|           } | ||||
|         ) | ||||
|       } | ||||
|     }, [ | ||||
|       inAppEnabled, | ||||
|       emailEnabled, | ||||
|       previousInAppValue, | ||||
|       previousEmailValue, | ||||
|       key, | ||||
|     ]) | ||||
|         .then(() => { | ||||
|           if (setting === 'browser') { | ||||
|             setInAppEnabled(newValue) | ||||
|           } else { | ||||
|             setEmailEnabled(newValue) | ||||
|           } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     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,14 +45,14 @@ 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" /> | ||||
| 
 | ||||
|           <EditButton /> | ||||
|         <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 className="" user={user} /> | ||||
|         </Row> | ||||
| 
 | ||||
|         <DailyProfitAndBalance userId={user?.id} /> | ||||
| 
 | ||||
|         {sections.map((item) => { | ||||
|           const { id } = item | ||||
|           if (id === 'daily-movers') { | ||||
|  | @ -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,71 +421,27 @@ 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> | ||||
| 
 | ||||
|             <div className={'text-md my-4 text-gray-600'}> | ||||
|               Add pre-existing markets to this group, or{' '} | ||||
|               <Link href={`/create?groupId=${group.id}`}> | ||||
|                 <span className="cursor-pointer font-semibold underline"> | ||||
|                   create a new one | ||||
|                 </span> | ||||
|               </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', | ||||
|               }} | ||||
|             /> | ||||
|         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}`}> | ||||
|               <span className="cursor-pointer font-semibold underline"> | ||||
|                 create a new one | ||||
|               </span> | ||||
|             </Link> | ||||
|             . | ||||
|           </div> | ||||
|         </Col> | ||||
|       </Modal> | ||||
|         } | ||||
|         submitLabel={(len) => `Add ${len} question${len !== 1 ? 's' : ''}`} | ||||
|         onSubmit={onSubmit} | ||||
|         contractSearchOptions={{ | ||||
|           additionalFilter: { excludeContractIds: groupContractIds }, | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  |  | |||
|  | @ -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