Merge branch 'main' into loans2
This commit is contained in:
commit
b17bb31972
151
common/contract-details.ts
Normal file
151
common/contract-details.ts
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import { Challenge } from './challenge'
|
||||||
|
import { BinaryContract, Contract } from './contract'
|
||||||
|
import { getFormattedMappedValue } from './pseudo-numeric'
|
||||||
|
import { getProbability } from './calculate'
|
||||||
|
import { richTextToString } from './util/parse'
|
||||||
|
import { getCpmmProbability } from './calculate-cpmm'
|
||||||
|
import { getDpmProbability } from './calculate-dpm'
|
||||||
|
import { formatMoney, formatPercent } from './util/format'
|
||||||
|
|
||||||
|
export function contractMetrics(contract: Contract) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const dayjs = require('dayjs')
|
||||||
|
const { createdTime, resolutionTime, isResolved } = contract
|
||||||
|
|
||||||
|
const createdDate = dayjs(createdTime).format('MMM D')
|
||||||
|
|
||||||
|
const resolvedDate = isResolved
|
||||||
|
? dayjs(resolutionTime).format('MMM D')
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const volumeLabel = `${formatMoney(contract.volume)} bet`
|
||||||
|
|
||||||
|
return { volumeLabel, createdDate, resolvedDate }
|
||||||
|
}
|
||||||
|
|
||||||
|
// String version of the above, to send to the OpenGraph image generator
|
||||||
|
export function contractTextDetails(contract: Contract) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const dayjs = require('dayjs')
|
||||||
|
const { closeTime, tags } = contract
|
||||||
|
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
|
||||||
|
|
||||||
|
const hashtags = tags.map((tag) => `#${tag}`)
|
||||||
|
|
||||||
|
return (
|
||||||
|
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
|
||||||
|
(closeTime
|
||||||
|
? ` • ${closeTime > Date.now() ? 'Closes' : 'Closed'} ${dayjs(
|
||||||
|
closeTime
|
||||||
|
).format('MMM D, h:mma')}`
|
||||||
|
: '') +
|
||||||
|
` • ${volumeLabel}` +
|
||||||
|
(hashtags.length > 0 ? ` • ${hashtags.join(' ')}` : '')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBinaryProb(contract: BinaryContract) {
|
||||||
|
const { pool, resolutionProbability, mechanism } = contract
|
||||||
|
|
||||||
|
return (
|
||||||
|
resolutionProbability ??
|
||||||
|
(mechanism === 'cpmm-1'
|
||||||
|
? getCpmmProbability(pool, contract.p)
|
||||||
|
: getDpmProbability(contract.totalShares))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOpenGraphProps = (contract: Contract) => {
|
||||||
|
const {
|
||||||
|
resolution,
|
||||||
|
question,
|
||||||
|
creatorName,
|
||||||
|
creatorUsername,
|
||||||
|
outcomeType,
|
||||||
|
creatorAvatarUrl,
|
||||||
|
description: desc,
|
||||||
|
} = contract
|
||||||
|
const probPercent =
|
||||||
|
outcomeType === 'BINARY'
|
||||||
|
? formatPercent(getBinaryProb(contract))
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const numericValue =
|
||||||
|
outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
? getFormattedMappedValue(contract)(getProbability(contract))
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc)
|
||||||
|
|
||||||
|
const description = resolution
|
||||||
|
? `Resolved ${resolution}. ${stringDesc}`
|
||||||
|
: probPercent
|
||||||
|
? `${probPercent} chance. ${stringDesc}`
|
||||||
|
: stringDesc
|
||||||
|
|
||||||
|
return {
|
||||||
|
question,
|
||||||
|
probability: probPercent,
|
||||||
|
metadata: contractTextDetails(contract),
|
||||||
|
creatorName,
|
||||||
|
creatorUsername,
|
||||||
|
creatorAvatarUrl,
|
||||||
|
description,
|
||||||
|
numericValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OgCardProps = {
|
||||||
|
question: string
|
||||||
|
probability?: string
|
||||||
|
metadata: string
|
||||||
|
creatorName: string
|
||||||
|
creatorUsername: string
|
||||||
|
creatorAvatarUrl?: string
|
||||||
|
numericValue?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCardUrl(props: OgCardProps, challenge?: Challenge) {
|
||||||
|
const {
|
||||||
|
creatorAmount,
|
||||||
|
acceptances,
|
||||||
|
acceptorAmount,
|
||||||
|
creatorOutcome,
|
||||||
|
acceptorOutcome,
|
||||||
|
} = challenge || {}
|
||||||
|
const { userName, userAvatarUrl } = acceptances?.[0] ?? {}
|
||||||
|
|
||||||
|
const probabilityParam =
|
||||||
|
props.probability === undefined
|
||||||
|
? ''
|
||||||
|
: `&probability=${encodeURIComponent(props.probability ?? '')}`
|
||||||
|
|
||||||
|
const numericValueParam =
|
||||||
|
props.numericValue === undefined
|
||||||
|
? ''
|
||||||
|
: `&numericValue=${encodeURIComponent(props.numericValue ?? '')}`
|
||||||
|
|
||||||
|
const creatorAvatarUrlParam =
|
||||||
|
props.creatorAvatarUrl === undefined
|
||||||
|
? ''
|
||||||
|
: `&creatorAvatarUrl=${encodeURIComponent(props.creatorAvatarUrl ?? '')}`
|
||||||
|
|
||||||
|
const challengeUrlParams = challenge
|
||||||
|
? `&creatorAmount=${creatorAmount}&creatorOutcome=${creatorOutcome}` +
|
||||||
|
`&challengerAmount=${acceptorAmount}&challengerOutcome=${acceptorOutcome}` +
|
||||||
|
`&acceptedName=${userName ?? ''}&acceptedAvatarUrl=${userAvatarUrl ?? ''}`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
// URL encode each of the props, then add them as query params
|
||||||
|
return (
|
||||||
|
`https://manifold-og-image.vercel.app/m.png` +
|
||||||
|
`?question=${encodeURIComponent(props.question)}` +
|
||||||
|
probabilityParam +
|
||||||
|
numericValueParam +
|
||||||
|
`&metadata=${encodeURIComponent(props.metadata)}` +
|
||||||
|
`&creatorName=${encodeURIComponent(props.creatorName)}` +
|
||||||
|
creatorAvatarUrlParam +
|
||||||
|
`&creatorUsername=${encodeURIComponent(props.creatorUsername)}` +
|
||||||
|
challengeUrlParams
|
||||||
|
)
|
||||||
|
}
|
|
@ -31,7 +31,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||||
description: string | JSONContent // More info about what the contract is about
|
description: string | JSONContent // More info about what the contract is about
|
||||||
tags: string[]
|
tags: string[]
|
||||||
lowercaseTags: string[]
|
lowercaseTags: string[]
|
||||||
visibility: 'public' | 'unlisted'
|
visibility: visibility
|
||||||
|
|
||||||
createdTime: number // Milliseconds since epoch
|
createdTime: number // Milliseconds since epoch
|
||||||
lastUpdatedTime?: number // Updated on new bet or comment
|
lastUpdatedTime?: number // Updated on new bet or comment
|
||||||
|
@ -143,3 +143,6 @@ export const MAX_DESCRIPTION_LENGTH = 16000
|
||||||
export const MAX_TAG_LENGTH = 60
|
export const MAX_TAG_LENGTH = 60
|
||||||
|
|
||||||
export const CPMM_MIN_POOL_QTY = 0.01
|
export const CPMM_MIN_POOL_QTY = 0.01
|
||||||
|
|
||||||
|
export type visibility = 'public' | 'unlisted'
|
||||||
|
export const VISIBILITIES = ['public', 'unlisted'] as const
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
Numeric,
|
Numeric,
|
||||||
outcomeType,
|
outcomeType,
|
||||||
PseudoNumeric,
|
PseudoNumeric,
|
||||||
|
visibility,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
import { parseTags, richTextToString } from './util/parse'
|
import { parseTags, richTextToString } from './util/parse'
|
||||||
|
@ -34,7 +35,8 @@ export function getNewContract(
|
||||||
isLogScale: boolean,
|
isLogScale: boolean,
|
||||||
|
|
||||||
// for multiple choice
|
// for multiple choice
|
||||||
answers: string[]
|
answers: string[],
|
||||||
|
visibility: visibility
|
||||||
) {
|
) {
|
||||||
const tags = parseTags(
|
const tags = parseTags(
|
||||||
[
|
[
|
||||||
|
@ -70,7 +72,7 @@ export function getNewContract(
|
||||||
description,
|
description,
|
||||||
tags,
|
tags,
|
||||||
lowercaseTags,
|
lowercaseTags,
|
||||||
visibility: 'public',
|
visibility,
|
||||||
isResolved: false,
|
isResolved: false,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
closeTime,
|
closeTime,
|
||||||
|
|
|
@ -5,4 +5,5 @@ export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
||||||
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
||||||
export const UNIQUE_BETTOR_BONUS_AMOUNT = 10
|
export const UNIQUE_BETTOR_BONUS_AMOUNT = 10
|
||||||
export const BETTING_STREAK_BONUS_AMOUNT = 5
|
export const BETTING_STREAK_BONUS_AMOUNT = 5
|
||||||
export const BETTING_STREAK_RESET_HOUR = 9
|
export const BETTING_STREAK_BONUS_MAX = 100
|
||||||
|
export const BETTING_STREAK_RESET_HOUR = 0
|
||||||
|
|
|
@ -59,6 +59,7 @@ export type PrivateUser = {
|
||||||
unsubscribedFromCommentEmails?: boolean
|
unsubscribedFromCommentEmails?: boolean
|
||||||
unsubscribedFromAnswerEmails?: boolean
|
unsubscribedFromAnswerEmails?: boolean
|
||||||
unsubscribedFromGenericEmails?: boolean
|
unsubscribedFromGenericEmails?: boolean
|
||||||
|
unsubscribedFromWeeklyTrendingEmails?: boolean
|
||||||
manaBonusEmailSent?: boolean
|
manaBonusEmailSent?: boolean
|
||||||
initialDeviceToken?: string
|
initialDeviceToken?: string
|
||||||
initialIpAddress?: string
|
initialIpAddress?: string
|
||||||
|
|
|
@ -63,7 +63,7 @@ service cloud.firestore {
|
||||||
allow read: if userId == request.auth.uid || isAdmin();
|
allow read: if userId == request.auth.uid || isAdmin();
|
||||||
allow update: if (userId == request.auth.uid || isAdmin())
|
allow update: if (userId == request.auth.uid || isAdmin())
|
||||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences' ]);
|
.hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences', 'unsubscribedFromWeeklyTrendingEmails' ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
match /private-users/{userId}/views/{viewId} {
|
match /private-users/{userId}/views/{viewId} {
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mailgun-js": "0.22.0",
|
"mailgun-js": "0.22.0",
|
||||||
"module-alias": "2.2.2",
|
"module-alias": "2.2.2",
|
||||||
|
"react-masonry-css": "1.0.16",
|
||||||
"stripe": "8.194.0",
|
"stripe": "8.194.0",
|
||||||
"zod": "3.17.2"
|
"zod": "3.17.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
MultipleChoiceContract,
|
MultipleChoiceContract,
|
||||||
NumericContract,
|
NumericContract,
|
||||||
OUTCOME_TYPES,
|
OUTCOME_TYPES,
|
||||||
|
VISIBILITIES,
|
||||||
} from '../../common/contract'
|
} from '../../common/contract'
|
||||||
import { slugify } from '../../common/util/slugify'
|
import { slugify } from '../../common/util/slugify'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
|
@ -69,6 +70,7 @@ const bodySchema = z.object({
|
||||||
),
|
),
|
||||||
outcomeType: z.enum(OUTCOME_TYPES),
|
outcomeType: z.enum(OUTCOME_TYPES),
|
||||||
groupId: z.string().min(1).max(MAX_ID_LENGTH).optional(),
|
groupId: z.string().min(1).max(MAX_ID_LENGTH).optional(),
|
||||||
|
visibility: z.enum(VISIBILITIES).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const binarySchema = z.object({
|
const binarySchema = z.object({
|
||||||
|
@ -90,8 +92,15 @@ const multipleChoiceSchema = z.object({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createmarket = newEndpoint({}, async (req, auth) => {
|
export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
const { question, description, tags, closeTime, outcomeType, groupId } =
|
const {
|
||||||
validate(bodySchema, req.body)
|
question,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
closeTime,
|
||||||
|
outcomeType,
|
||||||
|
groupId,
|
||||||
|
visibility = 'public',
|
||||||
|
} = validate(bodySchema, req.body)
|
||||||
|
|
||||||
let min, max, initialProb, isLogScale, answers
|
let min, max, initialProb, isLogScale, answers
|
||||||
|
|
||||||
|
@ -196,7 +205,8 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||||
min ?? 0,
|
min ?? 0,
|
||||||
max ?? 0,
|
max ?? 0,
|
||||||
isLogScale ?? false,
|
isLogScale ?? false,
|
||||||
answers ?? []
|
answers ?? [],
|
||||||
|
visibility
|
||||||
)
|
)
|
||||||
|
|
||||||
if (ante) await chargeUser(user.id, ante, true)
|
if (ante) await chargeUser(user.id, ante, true)
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
cleanDisplayName,
|
cleanDisplayName,
|
||||||
cleanUsername,
|
cleanUsername,
|
||||||
} from '../../common/util/clean-username'
|
} from '../../common/util/clean-username'
|
||||||
import { sendPersonalFollowupEmail, sendWelcomeEmail } from './emails'
|
|
||||||
import { isWhitelisted } from '../../common/envs/constants'
|
import { isWhitelisted } from '../../common/envs/constants'
|
||||||
import {
|
import {
|
||||||
CATEGORIES_GROUP_SLUG_POSTFIX,
|
CATEGORIES_GROUP_SLUG_POSTFIX,
|
||||||
|
@ -93,10 +92,8 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
|
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
|
||||||
|
|
||||||
await addUserToDefaultGroups(user)
|
await addUserToDefaultGroups(user)
|
||||||
await sendWelcomeEmail(user, privateUser)
|
|
||||||
await sendPersonalFollowupEmail(user, privateUser)
|
|
||||||
await track(auth.uid, 'create user', { username }, { ip: req.ip })
|
await track(auth.uid, 'create user', { username }, { ip: req.ip })
|
||||||
|
|
||||||
return { user, privateUser }
|
return { user, privateUser }
|
||||||
|
|
476
functions/src/email-templates/interesting-markets.html
Normal file
476
functions/src/email-templates/interesting-markets.html
Normal file
|
@ -0,0 +1,476 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
|
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Interesting markets on Manifold</title>
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<!--<![endif]-->
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<style type="text/css">
|
||||||
|
#outlook a {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
td {
|
||||||
|
border-collapse: collapse;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
height: auto;
|
||||||
|
line-height: 100%;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
margin: 13px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<noscript>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG />
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
</noscript>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if lte mso 11]>
|
||||||
|
<style type="text/css">
|
||||||
|
.mj-outlook-group-fix {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||||
|
<style type="text/css">
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||||
|
</style>
|
||||||
|
<!--<![endif]-->
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (min-width: 480px) {
|
||||||
|
.mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style media="screen and (min-width:480px)">
|
||||||
|
.moz-text-html .mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
[owa] .mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
table.mj-full-width-mobile {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mj-full-width-mobile {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="word-spacing: normal; background-color: #f4f4f4">
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
|
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center"
|
||||||
|
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="border-collapse:collapse;border-spacing:0px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="width:550px;">
|
||||||
|
|
||||||
|
<a href="https://manifold.markets" target="_blank">
|
||||||
|
|
||||||
|
<img alt="banner logo" height="auto"
|
||||||
|
src="https://manifold.markets/logo-banner.png"
|
||||||
|
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
|
||||||
|
title="" width="550">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="
|
||||||
|
background: #ffffff;
|
||||||
|
background-color: #ffffff;
|
||||||
|
margin: 0px auto;
|
||||||
|
max-width: 600px;
|
||||||
|
">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="background: #ffffff; background-color: #ffffff; width: 100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 20px 0px 0px 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
padding-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||||
|
font-size: 0px;
|
||||||
|
text-align: left;
|
||||||
|
direction: ltr;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="vertical-align: top" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content"
|
||||||
|
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||||
|
data-testid="4XoHRGw1Y"><span
|
||||||
|
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||||
|
</span>Hi {{name}},</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content"
|
||||||
|
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||||
|
data-testid="4XoHRGw1Y"><span
|
||||||
|
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||||
|
Here is a selection of markets on Manifold you might find
|
||||||
|
interesting!</span></p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center"
|
||||||
|
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:center;color:#000000;">
|
||||||
|
<a href="{{question1Link}}">
|
||||||
|
<img alt="{{question1Title}}" width="375" height="200"
|
||||||
|
style="border: 1px solid #4337c9;" src="{{question1ImgSrc}}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius: 4px;" bgcolor="#4337c9">
|
||||||
|
<a href="{{question1Link}}" target="_blank"
|
||||||
|
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
|
||||||
|
View market
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center"
|
||||||
|
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:center;color:#000000;">
|
||||||
|
<a href="{{question2Link}}">
|
||||||
|
<img alt="{{question2Title}}" width="375" height="200"
|
||||||
|
style="border: 1px solid #4337c9;" src="{{question2ImgSrc}}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius: 4px;" bgcolor="#4337c9">
|
||||||
|
<a href="{{question2Link}}" target="_blank"
|
||||||
|
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
|
||||||
|
View market
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center"
|
||||||
|
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:center;color:#000000;">
|
||||||
|
<a href="{{question3Link}}">
|
||||||
|
<img alt="{{question3Title}}" width="375" height="200"
|
||||||
|
style="border: 1px solid #4337c9;" src="{{question3ImgSrc}}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius: 4px;" bgcolor="#4337c9">
|
||||||
|
<a href="{{question3Link}}" target="_blank"
|
||||||
|
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
|
||||||
|
View market
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center"
|
||||||
|
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:center;color:#000000;">
|
||||||
|
<a href="{{question4Link}}">
|
||||||
|
<img alt="{{question4Title}}" width="375" height="200"
|
||||||
|
style="border: 1px solid #4337c9;" src="{{question4ImgSrc}}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius: 4px;" bgcolor="#4337c9">
|
||||||
|
<a href="{{question4Link}}" target="_blank"
|
||||||
|
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
|
||||||
|
View market
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center"
|
||||||
|
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:center;color:#000000;">
|
||||||
|
<a href="{{question5Link}}">
|
||||||
|
<img alt="{{question5Title}}" width="375" height="200"
|
||||||
|
style="border: 1px solid #4337c9;" src="{{question5ImgSrc}}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius: 4px;" bgcolor="#4337c9">
|
||||||
|
<a href="{{question5Link}}" target="_blank"
|
||||||
|
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
|
||||||
|
View market
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center"
|
||||||
|
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:center;color:#000000;">
|
||||||
|
<a href="{{question6Link}}">
|
||||||
|
<img alt="{{question6Title}}" width="375" height="200"
|
||||||
|
style="border: 1px solid #4337c9;" src="{{question6ImgSrc}}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius: 4px;" bgcolor="#4337c9">
|
||||||
|
<a href="{{question6Link}}" target="_blank"
|
||||||
|
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
|
||||||
|
View market
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin: 0px auto; max-width: 600px">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 0 0 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin: 0px auto; max-width: 600px">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="width: 100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 20px 0px 20px 0px;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||||
|
font-size: 0px;
|
||||||
|
text-align: left;
|
||||||
|
direction: ltr;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align: top; padding: 0">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0"
|
||||||
|
role="presentation" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
font-family: Ubuntu, Helvetica, Arial,
|
||||||
|
sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 22px;
|
||||||
|
text-align: center;
|
||||||
|
color: #000000;
|
||||||
|
">
|
||||||
|
<p style="margin: 10px 0">
|
||||||
|
This e-mail has been sent to
|
||||||
|
{{name}},
|
||||||
|
<a href="{{unsubscribeLink}}"
|
||||||
|
style="
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
" target="_blank">click here to unsubscribe</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -1,4 +1,3 @@
|
||||||
import * as dayjs from 'dayjs'
|
|
||||||
|
|
||||||
import { DOMAIN } from '../../common/envs/constants'
|
import { DOMAIN } from '../../common/envs/constants'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
|
@ -20,6 +19,7 @@ import { sendTemplateEmail, sendTextEmail } from './send-email'
|
||||||
import { getPrivateUser, getUser } from './utils'
|
import { getPrivateUser, getUser } from './utils'
|
||||||
import { getFunctionUrl } from '../../common/api'
|
import { getFunctionUrl } from '../../common/api'
|
||||||
import { richTextToString } from '../../common/util/parse'
|
import { richTextToString } from '../../common/util/parse'
|
||||||
|
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
|
||||||
|
|
||||||
const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe')
|
const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe')
|
||||||
|
|
||||||
|
@ -169,7 +169,8 @@ export const sendWelcomeEmail = async (
|
||||||
|
|
||||||
export const sendPersonalFollowupEmail = async (
|
export const sendPersonalFollowupEmail = async (
|
||||||
user: User,
|
user: User,
|
||||||
privateUser: PrivateUser
|
privateUser: PrivateUser,
|
||||||
|
sendTime: string
|
||||||
) => {
|
) => {
|
||||||
if (!privateUser || !privateUser.email) return
|
if (!privateUser || !privateUser.email) return
|
||||||
|
|
||||||
|
@ -191,7 +192,6 @@ Cofounder of Manifold Markets
|
||||||
https://manifold.markets
|
https://manifold.markets
|
||||||
`
|
`
|
||||||
|
|
||||||
const sendTime = dayjs().add(4, 'hours').toString()
|
|
||||||
|
|
||||||
await sendTextEmail(
|
await sendTextEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
|
@ -460,3 +460,61 @@ export const sendNewAnswerEmail = async (
|
||||||
{ from }
|
{ from }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sendInterestingMarketsEmail = async (
|
||||||
|
user: User,
|
||||||
|
privateUser: PrivateUser,
|
||||||
|
contractsToSend: Contract[],
|
||||||
|
deliveryTime?: string
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!privateUser ||
|
||||||
|
!privateUser.email ||
|
||||||
|
privateUser?.unsubscribedFromWeeklyTrendingEmails
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
const emailType = 'weekly-trending'
|
||||||
|
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${privateUser.id}&type=${emailType}`
|
||||||
|
|
||||||
|
const { name } = user
|
||||||
|
const firstName = name.split(' ')[0]
|
||||||
|
|
||||||
|
await sendTemplateEmail(
|
||||||
|
privateUser.email,
|
||||||
|
`${contractsToSend[0].question} & 5 more interesting markets on Manifold`,
|
||||||
|
'interesting-markets',
|
||||||
|
{
|
||||||
|
name: firstName,
|
||||||
|
unsubscribeLink: unsubscribeUrl,
|
||||||
|
|
||||||
|
question1Title: contractsToSend[0].question,
|
||||||
|
question1Link: contractUrl(contractsToSend[0]),
|
||||||
|
question1ImgSrc: imageSourceUrl(contractsToSend[0]),
|
||||||
|
question2Title: contractsToSend[1].question,
|
||||||
|
question2Link: contractUrl(contractsToSend[1]),
|
||||||
|
question2ImgSrc: imageSourceUrl(contractsToSend[1]),
|
||||||
|
question3Title: contractsToSend[2].question,
|
||||||
|
question3Link: contractUrl(contractsToSend[2]),
|
||||||
|
question3ImgSrc: imageSourceUrl(contractsToSend[2]),
|
||||||
|
question4Title: contractsToSend[3].question,
|
||||||
|
question4Link: contractUrl(contractsToSend[3]),
|
||||||
|
question4ImgSrc: imageSourceUrl(contractsToSend[3]),
|
||||||
|
question5Title: contractsToSend[4].question,
|
||||||
|
question5Link: contractUrl(contractsToSend[4]),
|
||||||
|
question5ImgSrc: imageSourceUrl(contractsToSend[4]),
|
||||||
|
question6Title: contractsToSend[5].question,
|
||||||
|
question6Link: contractUrl(contractsToSend[5]),
|
||||||
|
question6ImgSrc: imageSourceUrl(contractsToSend[5]),
|
||||||
|
},
|
||||||
|
deliveryTime ? { 'o:deliverytime': deliveryTime } : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function contractUrl(contract: Contract) {
|
||||||
|
return `https://manifold.markets/${contract.creatorUsername}/${contract.slug}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageSourceUrl(contract: Contract) {
|
||||||
|
return buildCardUrl(getOpenGraphProps(contract))
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { EndpointDefinition } from './api'
|
||||||
admin.initializeApp()
|
admin.initializeApp()
|
||||||
|
|
||||||
// v1
|
// v1
|
||||||
|
export * from './on-create-user'
|
||||||
export * from './on-create-bet'
|
export * from './on-create-bet'
|
||||||
export * from './on-create-comment-on-contract'
|
export * from './on-create-comment-on-contract'
|
||||||
export * from './on-view'
|
export * from './on-view'
|
||||||
|
@ -26,6 +27,7 @@ export * from './on-create-comment-on-group'
|
||||||
export * from './on-create-txn'
|
export * from './on-create-txn'
|
||||||
export * from './on-delete-group'
|
export * from './on-delete-group'
|
||||||
export * from './score-contracts'
|
export * from './score-contracts'
|
||||||
|
export * from './weekly-markets-emails'
|
||||||
export * from './reset-betting-streaks'
|
export * from './reset-betting-streaks'
|
||||||
|
|
||||||
// v2
|
// v2
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { Contract } from '../../common/contract'
|
||||||
import { runTxn, TxnData } from './transact'
|
import { runTxn, TxnData } from './transact'
|
||||||
import {
|
import {
|
||||||
BETTING_STREAK_BONUS_AMOUNT,
|
BETTING_STREAK_BONUS_AMOUNT,
|
||||||
|
BETTING_STREAK_BONUS_MAX,
|
||||||
BETTING_STREAK_RESET_HOUR,
|
BETTING_STREAK_RESET_HOUR,
|
||||||
UNIQUE_BETTOR_BONUS_AMOUNT,
|
UNIQUE_BETTOR_BONUS_AMOUNT,
|
||||||
} from '../../common/numeric-constants'
|
} from '../../common/numeric-constants'
|
||||||
|
@ -86,7 +87,7 @@ const updateBettingStreak = async (
|
||||||
// Send them the bonus times their streak
|
// Send them the bonus times their streak
|
||||||
const bonusAmount = Math.min(
|
const bonusAmount = Math.min(
|
||||||
BETTING_STREAK_BONUS_AMOUNT * newBettingStreak,
|
BETTING_STREAK_BONUS_AMOUNT * newBettingStreak,
|
||||||
100
|
BETTING_STREAK_BONUS_MAX
|
||||||
)
|
)
|
||||||
const fromUserId = isProd()
|
const fromUserId = isProd()
|
||||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
|
41
functions/src/on-create-user.ts
Normal file
41
functions/src/on-create-user.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as dayjs from 'dayjs'
|
||||||
|
import * as utc from 'dayjs/plugin/utc'
|
||||||
|
dayjs.extend(utc)
|
||||||
|
|
||||||
|
import { getPrivateUser } from './utils'
|
||||||
|
import { User } from 'common/user'
|
||||||
|
import {
|
||||||
|
sendInterestingMarketsEmail,
|
||||||
|
sendPersonalFollowupEmail,
|
||||||
|
sendWelcomeEmail,
|
||||||
|
} from './emails'
|
||||||
|
import { getTrendingContracts } from './weekly-markets-emails'
|
||||||
|
|
||||||
|
export const onCreateUser = functions
|
||||||
|
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||||
|
.firestore.document('users/{userId}')
|
||||||
|
.onCreate(async (snapshot) => {
|
||||||
|
const user = snapshot.data() as User
|
||||||
|
const privateUser = await getPrivateUser(user.id)
|
||||||
|
if (!privateUser) return
|
||||||
|
|
||||||
|
await sendWelcomeEmail(user, privateUser)
|
||||||
|
|
||||||
|
const followupSendTime = dayjs().add(4, 'hours').toString()
|
||||||
|
await sendPersonalFollowupEmail(user, privateUser, followupSendTime)
|
||||||
|
|
||||||
|
// skip email if weekly email is about to go out
|
||||||
|
const day = dayjs().utc().day()
|
||||||
|
if (day === 0 || (day === 1 && dayjs().utc().hour() <= 19)) return
|
||||||
|
|
||||||
|
const contracts = await getTrendingContracts()
|
||||||
|
const marketsSendTime = dayjs().add(24, 'hours').toString()
|
||||||
|
|
||||||
|
await sendInterestingMarketsEmail(
|
||||||
|
user,
|
||||||
|
privateUser,
|
||||||
|
contracts,
|
||||||
|
marketsSendTime
|
||||||
|
)
|
||||||
|
})
|
|
@ -9,6 +9,7 @@ const firestore = admin.firestore()
|
||||||
|
|
||||||
export const resetBettingStreaksForUsers = functions.pubsub
|
export const resetBettingStreaksForUsers = functions.pubsub
|
||||||
.schedule(`0 ${BETTING_STREAK_RESET_HOUR} * * *`)
|
.schedule(`0 ${BETTING_STREAK_RESET_HOUR} * * *`)
|
||||||
|
.timeZone('utc')
|
||||||
.onRun(async () => {
|
.onRun(async () => {
|
||||||
await resetBettingStreaksInternal()
|
await resetBettingStreaksInternal()
|
||||||
})
|
})
|
||||||
|
|
29
functions/src/scripts/unlist-contracts.ts
Normal file
29
functions/src/scripts/unlist-contracts.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
import { initAdmin } from './script-init'
|
||||||
|
initAdmin()
|
||||||
|
|
||||||
|
import { Contract } from '../../../common/contract'
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
async function unlistContracts() {
|
||||||
|
console.log('Updating some contracts to be unlisted')
|
||||||
|
|
||||||
|
const snapshot = await firestore
|
||||||
|
.collection('contracts')
|
||||||
|
.where('groupSlugs', 'array-contains', 'fantasy-football-stock-exchange')
|
||||||
|
.get()
|
||||||
|
const contracts = snapshot.docs.map((doc) => doc.data() as Contract)
|
||||||
|
|
||||||
|
console.log('Loaded', contracts.length, 'contracts')
|
||||||
|
|
||||||
|
for (const contract of contracts) {
|
||||||
|
const contractRef = firestore.doc(`contracts/${contract.id}`)
|
||||||
|
|
||||||
|
console.log('Updating', contract.question)
|
||||||
|
await contractRef.update({ visibility: 'unlisted' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) unlistContracts().then(() => process.exit())
|
|
@ -21,6 +21,7 @@ export const unsubscribe: EndpointDefinition = {
|
||||||
'market-comment',
|
'market-comment',
|
||||||
'market-answer',
|
'market-answer',
|
||||||
'generic',
|
'generic',
|
||||||
|
'weekly-trending',
|
||||||
].includes(type)
|
].includes(type)
|
||||||
) {
|
) {
|
||||||
res.status(400).send('Invalid type parameter.')
|
res.status(400).send('Invalid type parameter.')
|
||||||
|
@ -49,6 +50,9 @@ export const unsubscribe: EndpointDefinition = {
|
||||||
...(type === 'generic' && {
|
...(type === 'generic' && {
|
||||||
unsubscribedFromGenericEmails: true,
|
unsubscribedFromGenericEmails: true,
|
||||||
}),
|
}),
|
||||||
|
...(type === 'weekly-trending' && {
|
||||||
|
unsubscribedFromWeeklyTrendingEmails: true,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
await firestore.collection('private-users').doc(id).update(update)
|
await firestore.collection('private-users').doc(id).update(update)
|
||||||
|
|
|
@ -88,6 +88,12 @@ export const getPrivateUser = (userId: string) => {
|
||||||
return getDoc<PrivateUser>('private-users', userId)
|
return getDoc<PrivateUser>('private-users', userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAllPrivateUsers = async () => {
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
const users = await firestore.collection('private-users').get()
|
||||||
|
return users.docs.map((doc) => doc.data() as PrivateUser)
|
||||||
|
}
|
||||||
|
|
||||||
export const getUserByUsername = async (username: string) => {
|
export const getUserByUsername = async (username: string) => {
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
const snap = await firestore
|
const snap = await firestore
|
||||||
|
|
81
functions/src/weekly-markets-emails.ts
Normal file
81
functions/src/weekly-markets-emails.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
import { getAllPrivateUsers, getUser, getValues, log } from './utils'
|
||||||
|
import { sendInterestingMarketsEmail } from './emails'
|
||||||
|
import { createRNG, shuffle } from '../../common/util/random'
|
||||||
|
import { DAY_MS } from '../../common/util/time'
|
||||||
|
|
||||||
|
export const weeklyMarketsEmails = functions
|
||||||
|
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||||
|
// every Monday at 12pm PT (UTC -07:00)
|
||||||
|
.pubsub.schedule('0 19 * * 1')
|
||||||
|
.timeZone('utc')
|
||||||
|
.onRun(async () => {
|
||||||
|
await sendTrendingMarketsEmailsToAllUsers()
|
||||||
|
})
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
export async function getTrendingContracts() {
|
||||||
|
return await getValues<Contract>(
|
||||||
|
firestore
|
||||||
|
.collection('contracts')
|
||||||
|
.where('isResolved', '==', false)
|
||||||
|
.where('visibility', '==', 'public')
|
||||||
|
// can't use multiple inequality (/orderBy) operators on different fields,
|
||||||
|
// so have to filter for closed contracts separately
|
||||||
|
.orderBy('popularityScore', 'desc')
|
||||||
|
// might as well go big and do a quick filter for closed ones later
|
||||||
|
.limit(500)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendTrendingMarketsEmailsToAllUsers() {
|
||||||
|
const numContractsToSend = 6
|
||||||
|
const privateUsers = await getAllPrivateUsers()
|
||||||
|
// get all users that haven't unsubscribed from weekly emails
|
||||||
|
const privateUsersToSendEmailsTo = privateUsers.filter((user) => {
|
||||||
|
return !user.unsubscribedFromWeeklyTrendingEmails
|
||||||
|
})
|
||||||
|
const trendingContracts = (await getTrendingContracts())
|
||||||
|
.filter(
|
||||||
|
(contract) =>
|
||||||
|
!(
|
||||||
|
contract.question.toLowerCase().includes('trump') &&
|
||||||
|
contract.question.toLowerCase().includes('president')
|
||||||
|
) && (contract?.closeTime ?? 0) > Date.now() + DAY_MS
|
||||||
|
)
|
||||||
|
.slice(0, 20)
|
||||||
|
for (const privateUser of privateUsersToSendEmailsTo) {
|
||||||
|
if (!privateUser.email) {
|
||||||
|
log(`No email for ${privateUser.username}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const contractsAvailableToSend = trendingContracts.filter((contract) => {
|
||||||
|
return !contract.uniqueBettorIds?.includes(privateUser.id)
|
||||||
|
})
|
||||||
|
if (contractsAvailableToSend.length < numContractsToSend) {
|
||||||
|
log('not enough new, unbet-on contracts to send to user', privateUser.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// choose random subset of contracts to send to user
|
||||||
|
const contractsToSend = chooseRandomSubset(
|
||||||
|
contractsAvailableToSend,
|
||||||
|
numContractsToSend
|
||||||
|
)
|
||||||
|
|
||||||
|
const user = await getUser(privateUser.id)
|
||||||
|
if (!user) continue
|
||||||
|
|
||||||
|
await sendInterestingMarketsEmail(user, privateUser, contractsToSend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseRandomSubset(contracts: Contract[], count: number) {
|
||||||
|
const fiveMinutes = 5 * 60 * 1000
|
||||||
|
const seed = Math.round(Date.now() / fiveMinutes).toString()
|
||||||
|
shuffle(contracts, createRNG(seed))
|
||||||
|
return contracts.slice(0, count)
|
||||||
|
}
|
|
@ -1,61 +1,7 @@
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { Challenge } from 'common/challenge'
|
import { Challenge } from 'common/challenge'
|
||||||
|
import { buildCardUrl, OgCardProps } from 'common/contract-details'
|
||||||
export type OgCardProps = {
|
|
||||||
question: string
|
|
||||||
probability?: string
|
|
||||||
metadata: string
|
|
||||||
creatorName: string
|
|
||||||
creatorUsername: string
|
|
||||||
creatorAvatarUrl?: string
|
|
||||||
numericValue?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCardUrl(props: OgCardProps, challenge?: Challenge) {
|
|
||||||
const {
|
|
||||||
creatorAmount,
|
|
||||||
acceptances,
|
|
||||||
acceptorAmount,
|
|
||||||
creatorOutcome,
|
|
||||||
acceptorOutcome,
|
|
||||||
} = challenge || {}
|
|
||||||
const { userName, userAvatarUrl } = acceptances?.[0] ?? {}
|
|
||||||
|
|
||||||
const probabilityParam =
|
|
||||||
props.probability === undefined
|
|
||||||
? ''
|
|
||||||
: `&probability=${encodeURIComponent(props.probability ?? '')}`
|
|
||||||
|
|
||||||
const numericValueParam =
|
|
||||||
props.numericValue === undefined
|
|
||||||
? ''
|
|
||||||
: `&numericValue=${encodeURIComponent(props.numericValue ?? '')}`
|
|
||||||
|
|
||||||
const creatorAvatarUrlParam =
|
|
||||||
props.creatorAvatarUrl === undefined
|
|
||||||
? ''
|
|
||||||
: `&creatorAvatarUrl=${encodeURIComponent(props.creatorAvatarUrl ?? '')}`
|
|
||||||
|
|
||||||
const challengeUrlParams = challenge
|
|
||||||
? `&creatorAmount=${creatorAmount}&creatorOutcome=${creatorOutcome}` +
|
|
||||||
`&challengerAmount=${acceptorAmount}&challengerOutcome=${acceptorOutcome}` +
|
|
||||||
`&acceptedName=${userName ?? ''}&acceptedAvatarUrl=${userAvatarUrl ?? ''}`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
// URL encode each of the props, then add them as query params
|
|
||||||
return (
|
|
||||||
`https://manifold-og-image.vercel.app/m.png` +
|
|
||||||
`?question=${encodeURIComponent(props.question)}` +
|
|
||||||
probabilityParam +
|
|
||||||
numericValueParam +
|
|
||||||
`&metadata=${encodeURIComponent(props.metadata)}` +
|
|
||||||
`&creatorName=${encodeURIComponent(props.creatorName)}` +
|
|
||||||
creatorAvatarUrlParam +
|
|
||||||
`&creatorUsername=${encodeURIComponent(props.creatorUsername)}` +
|
|
||||||
challengeUrlParams
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SEO(props: {
|
export function SEO(props: {
|
||||||
title: string
|
title: string
|
||||||
|
|
|
@ -534,9 +534,8 @@ export function ContractBetsTable(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
isYourBets: boolean
|
isYourBets: boolean
|
||||||
className?: string
|
|
||||||
}) {
|
}) {
|
||||||
const { contract, className, isYourBets } = props
|
const { contract, isYourBets } = props
|
||||||
|
|
||||||
const bets = sortBy(
|
const bets = sortBy(
|
||||||
props.bets.filter((b) => !b.isAnte && b.amount !== 0),
|
props.bets.filter((b) => !b.isAnte && b.amount !== 0),
|
||||||
|
@ -568,7 +567,7 @@ export function ContractBetsTable(props: {
|
||||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('overflow-x-auto', className)}>
|
<div className="overflow-x-auto">
|
||||||
{amountRedeemed > 0 && (
|
{amountRedeemed > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="pl-2 text-sm text-gray-500">
|
<div className="pl-2 text-sm text-gray-500">
|
||||||
|
@ -771,7 +770,7 @@ function SellButton(props: {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-4 text-2xl">
|
<div className="mb-4 text-xl">
|
||||||
Sell {formatWithCommas(shares)} shares of{' '}
|
Sell {formatWithCommas(shares)} shares of{' '}
|
||||||
<OutcomeLabel outcome={outcome} contract={contract} truncate="long" />{' '}
|
<OutcomeLabel outcome={outcome} contract={contract} truncate="long" />{' '}
|
||||||
for {formatMoney(saleAmount)}?
|
for {formatMoney(saleAmount)}?
|
||||||
|
|
|
@ -83,7 +83,6 @@ export function ContractSearch(props: {
|
||||||
highlightOptions?: ContractHighlightOptions
|
highlightOptions?: ContractHighlightOptions
|
||||||
onContractClick?: (contract: Contract) => void
|
onContractClick?: (contract: Contract) => void
|
||||||
hideOrderSelector?: boolean
|
hideOrderSelector?: boolean
|
||||||
overrideGridClassName?: string
|
|
||||||
cardHideOptions?: {
|
cardHideOptions?: {
|
||||||
hideGroupLink?: boolean
|
hideGroupLink?: boolean
|
||||||
hideQuickBet?: boolean
|
hideQuickBet?: boolean
|
||||||
|
@ -99,7 +98,6 @@ export function ContractSearch(props: {
|
||||||
defaultFilter,
|
defaultFilter,
|
||||||
additionalFilter,
|
additionalFilter,
|
||||||
onContractClick,
|
onContractClick,
|
||||||
overrideGridClassName,
|
|
||||||
hideOrderSelector,
|
hideOrderSelector,
|
||||||
cardHideOptions,
|
cardHideOptions,
|
||||||
highlightOptions,
|
highlightOptions,
|
||||||
|
@ -183,7 +181,6 @@ export function ContractSearch(props: {
|
||||||
loadMore={performQuery}
|
loadMore={performQuery}
|
||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
onContractClick={onContractClick}
|
onContractClick={onContractClick}
|
||||||
overrideGridClassName={overrideGridClassName}
|
|
||||||
highlightOptions={highlightOptions}
|
highlightOptions={highlightOptions}
|
||||||
cardHideOptions={cardHideOptions}
|
cardHideOptions={cardHideOptions}
|
||||||
/>
|
/>
|
||||||
|
@ -258,9 +255,12 @@ function ContractSearchControls(props: {
|
||||||
? additionalFilters
|
? additionalFilters
|
||||||
: [
|
: [
|
||||||
...additionalFilters,
|
...additionalFilters,
|
||||||
|
additionalFilter ? '' : 'visibility:public',
|
||||||
|
|
||||||
filter === 'open' ? 'isResolved:false' : '',
|
filter === 'open' ? 'isResolved:false' : '',
|
||||||
filter === 'closed' ? 'isResolved:false' : '',
|
filter === 'closed' ? 'isResolved:false' : '',
|
||||||
filter === 'resolved' ? 'isResolved:true' : '',
|
filter === 'resolved' ? 'isResolved:true' : '',
|
||||||
|
|
||||||
pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets'
|
pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets'
|
||||||
? `groupLinks.slug:${pillFilter}`
|
? `groupLinks.slug:${pillFilter}`
|
||||||
: '',
|
: '',
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { Contract } from 'common/contract'
|
|
||||||
import { getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
|
||||||
import { richTextToString } from 'common/util/parse'
|
|
||||||
import { contractTextDetails } from 'web/components/contract/contract-details'
|
|
||||||
import { getFormattedMappedValue } from 'common/pseudo-numeric'
|
|
||||||
import { getProbability } from 'common/calculate'
|
|
||||||
|
|
||||||
export const getOpenGraphProps = (contract: Contract) => {
|
|
||||||
const {
|
|
||||||
resolution,
|
|
||||||
question,
|
|
||||||
creatorName,
|
|
||||||
creatorUsername,
|
|
||||||
outcomeType,
|
|
||||||
creatorAvatarUrl,
|
|
||||||
description: desc,
|
|
||||||
} = contract
|
|
||||||
const probPercent =
|
|
||||||
outcomeType === 'BINARY' ? getBinaryProbPercent(contract) : undefined
|
|
||||||
|
|
||||||
const numericValue =
|
|
||||||
outcomeType === 'PSEUDO_NUMERIC'
|
|
||||||
? getFormattedMappedValue(contract)(getProbability(contract))
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc)
|
|
||||||
|
|
||||||
const description = resolution
|
|
||||||
? `Resolved ${resolution}. ${stringDesc}`
|
|
||||||
: probPercent
|
|
||||||
? `${probPercent} chance. ${stringDesc}`
|
|
||||||
: stringDesc
|
|
||||||
|
|
||||||
return {
|
|
||||||
question,
|
|
||||||
probability: probPercent,
|
|
||||||
metadata: contractTextDetails(contract),
|
|
||||||
creatorName,
|
|
||||||
creatorUsername,
|
|
||||||
creatorAvatarUrl,
|
|
||||||
description,
|
|
||||||
numericValue,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -120,7 +120,7 @@ export function ContractCard(props: {
|
||||||
truncate={'long'}
|
truncate={'long'}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FreeResponseTopAnswer contract={contract} truncate="long" />
|
<FreeResponseTopAnswer contract={contract} />
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
{showQuickBet ? (
|
{showQuickBet ? (
|
||||||
|
@ -230,10 +230,9 @@ export function BinaryResolutionOrChance(props: {
|
||||||
|
|
||||||
function FreeResponseTopAnswer(props: {
|
function FreeResponseTopAnswer(props: {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
truncate: 'short' | 'long' | 'none'
|
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, truncate } = props
|
const { contract } = props
|
||||||
|
|
||||||
const topAnswer = getTopAnswer(contract)
|
const topAnswer = getTopAnswer(contract)
|
||||||
|
|
||||||
|
@ -241,7 +240,7 @@ function FreeResponseTopAnswer(props: {
|
||||||
<AnswerLabel
|
<AnswerLabel
|
||||||
className="!text-gray-600"
|
className="!text-gray-600"
|
||||||
answer={topAnswer}
|
answer={topAnswer}
|
||||||
truncate={truncate}
|
truncate="long"
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,7 @@ import {
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { UserLink } from '../user-page'
|
import { UserLink } from '../user-page'
|
||||||
import {
|
import { Contract, updateContract } from 'web/lib/firebase/contracts'
|
||||||
Contract,
|
|
||||||
contractMetrics,
|
|
||||||
updateContract,
|
|
||||||
} from 'web/lib/firebase/contracts'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { DateTimeTooltip } from '../datetime-tooltip'
|
import { DateTimeTooltip } from '../datetime-tooltip'
|
||||||
import { fromNow } from 'web/lib/util/time'
|
import { fromNow } from 'web/lib/util/time'
|
||||||
|
@ -35,6 +31,7 @@ import { SiteLink } from 'web/components/site-link'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
import { insertContent } from '../editor/utils'
|
import { insertContent } from '../editor/utils'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { contractMetrics } from 'common/contract-details'
|
||||||
|
|
||||||
export type ShowTime = 'resolve-date' | 'close-date'
|
export type ShowTime = 'resolve-date' | 'close-date'
|
||||||
|
|
||||||
|
@ -245,25 +242,6 @@ export function ContractDetails(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String version of the above, to send to the OpenGraph image generator
|
|
||||||
export function contractTextDetails(contract: Contract) {
|
|
||||||
const { closeTime, tags } = contract
|
|
||||||
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
|
|
||||||
|
|
||||||
const hashtags = tags.map((tag) => `#${tag}`)
|
|
||||||
|
|
||||||
return (
|
|
||||||
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
|
|
||||||
(closeTime
|
|
||||||
? ` • ${closeTime > Date.now() ? 'Closes' : 'Closed'} ${dayjs(
|
|
||||||
closeTime
|
|
||||||
).format('MMM D, h:mma')}`
|
|
||||||
: '') +
|
|
||||||
` • ${volumeLabel}` +
|
|
||||||
(hashtags.length > 0 ? ` • ${hashtags.join(' ')}` : '')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditableCloseDate(props: {
|
function EditableCloseDate(props: {
|
||||||
closeTime: number
|
closeTime: number
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { useCallback } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { LoadingIndicator } from '../loading-indicator'
|
import { LoadingIndicator } from '../loading-indicator'
|
||||||
import { VisibilityObserver } from '../visibility-observer'
|
import { VisibilityObserver } from '../visibility-observer'
|
||||||
|
import Masonry from 'react-masonry-css'
|
||||||
|
|
||||||
export type ContractHighlightOptions = {
|
export type ContractHighlightOptions = {
|
||||||
contractIds?: string[]
|
contractIds?: string[]
|
||||||
|
@ -20,7 +21,6 @@ export function ContractsGrid(props: {
|
||||||
loadMore?: () => void
|
loadMore?: () => void
|
||||||
showTime?: ShowTime
|
showTime?: ShowTime
|
||||||
onContractClick?: (contract: Contract) => void
|
onContractClick?: (contract: Contract) => void
|
||||||
overrideGridClassName?: string
|
|
||||||
cardHideOptions?: {
|
cardHideOptions?: {
|
||||||
hideQuickBet?: boolean
|
hideQuickBet?: boolean
|
||||||
hideGroupLink?: boolean
|
hideGroupLink?: boolean
|
||||||
|
@ -32,7 +32,6 @@ export function ContractsGrid(props: {
|
||||||
showTime,
|
showTime,
|
||||||
loadMore,
|
loadMore,
|
||||||
onContractClick,
|
onContractClick,
|
||||||
overrideGridClassName,
|
|
||||||
cardHideOptions,
|
cardHideOptions,
|
||||||
highlightOptions,
|
highlightOptions,
|
||||||
} = props
|
} = props
|
||||||
|
@ -64,12 +63,11 @@ export function ContractsGrid(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-8">
|
<Col className="gap-8">
|
||||||
<ul
|
<Masonry
|
||||||
className={clsx(
|
// Show only 1 column on tailwind's md breakpoint (768px)
|
||||||
overrideGridClassName
|
breakpointCols={{ default: 2, 768: 1 }}
|
||||||
? overrideGridClassName
|
className="-ml-4 flex w-auto"
|
||||||
: 'grid w-full grid-cols-1 gap-4 md:grid-cols-2'
|
columnClassName="pl-4 bg-clip-padding"
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{contracts.map((contract) => (
|
{contracts.map((contract) => (
|
||||||
<ContractCard
|
<ContractCard
|
||||||
|
@ -81,14 +79,13 @@ export function ContractsGrid(props: {
|
||||||
}
|
}
|
||||||
hideQuickBet={hideQuickBet}
|
hideQuickBet={hideQuickBet}
|
||||||
hideGroupLink={hideGroupLink}
|
hideGroupLink={hideGroupLink}
|
||||||
className={
|
className={clsx(
|
||||||
contractIds?.includes(contract.id)
|
'mb-4 break-inside-avoid-column overflow-hidden', // prevent content from wrapping (needs overflow on firefox)
|
||||||
? highlightClassName
|
contractIds?.includes(contract.id) && highlightClassName
|
||||||
: undefined
|
)}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</Masonry>
|
||||||
<VisibilityObserver
|
<VisibilityObserver
|
||||||
onVisibilityUpdated={onVisibilityUpdated}
|
onVisibilityUpdated={onVisibilityUpdated}
|
||||||
className="relative -top-96 h-1"
|
className="relative -top-96 h-1"
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { useState } from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||||
import { placeBet } from 'web/lib/firebase/api'
|
import { placeBet } from 'web/lib/firebase/api'
|
||||||
import { getBinaryProb, getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
import { getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
||||||
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
|
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
|
||||||
import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
|
import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
|
@ -34,6 +34,7 @@ import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||||
|
import { getBinaryProb } from 'common/contract-details'
|
||||||
|
|
||||||
const BET_SIZE = 10
|
const BET_SIZE = 10
|
||||||
|
|
||||||
|
|
|
@ -65,9 +65,6 @@ export function MarketModal(props: {
|
||||||
<ContractSearch
|
<ContractSearch
|
||||||
hideOrderSelector
|
hideOrderSelector
|
||||||
onContractClick={addContract}
|
onContractClick={addContract}
|
||||||
overrideGridClassName={
|
|
||||||
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
|
|
||||||
}
|
|
||||||
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
||||||
highlightOptions={{
|
highlightOptions={{
|
||||||
contractIds: contracts.map((c) => c.id),
|
contractIds: contracts.map((c) => c.id),
|
||||||
|
|
|
@ -11,7 +11,6 @@ import clsx from 'clsx'
|
||||||
import { OutcomeLabel } from '../outcome-label'
|
import { OutcomeLabel } from '../outcome-label'
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
contractMetrics,
|
|
||||||
contractPath,
|
contractPath,
|
||||||
tradingAllowed,
|
tradingAllowed,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
|
@ -38,6 +37,7 @@ import { FeedLiquidity } from './feed-liquidity'
|
||||||
import { SignUpPrompt } from '../sign-up-prompt'
|
import { SignUpPrompt } from '../sign-up-prompt'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { PlayMoneyDisclaimer } from '../play-money-disclaimer'
|
import { PlayMoneyDisclaimer } from '../play-money-disclaimer'
|
||||||
|
import { contractMetrics } from 'common/contract-details'
|
||||||
|
|
||||||
export function FeedItems(props: {
|
export function FeedItems(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
|
|
@ -90,13 +90,11 @@ export function FreeResponseOutcomeLabel(props: {
|
||||||
const chosen = contract.answers?.find((answer) => answer.id === resolution)
|
const chosen = contract.answers?.find((answer) => answer.id === resolution)
|
||||||
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
||||||
return (
|
return (
|
||||||
<Tooltip text={chosen.text}>
|
<AnswerLabel
|
||||||
<AnswerLabel
|
answer={chosen}
|
||||||
answer={chosen}
|
truncate={truncate}
|
||||||
truncate={truncate}
|
className={answerClassName}
|
||||||
className={answerClassName}
|
/>
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,11 +163,13 @@ export function AnswerLabel(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<Tooltip text={truncated === text ? false : text}>
|
||||||
style={{ wordBreak: 'break-word' }}
|
<span
|
||||||
className={clsx('whitespace-pre-line break-words', className)}
|
style={{ wordBreak: 'break-word' }}
|
||||||
>
|
className={clsx('whitespace-pre-line break-words', className)}
|
||||||
{truncated}
|
>
|
||||||
</span>
|
{truncated}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
|
import {
|
||||||
|
BETTING_STREAK_BONUS_AMOUNT,
|
||||||
|
BETTING_STREAK_BONUS_MAX,
|
||||||
|
} from 'common/numeric-constants'
|
||||||
|
import { formatMoney } from 'common/util/format'
|
||||||
|
|
||||||
export function BettingStreakModal(props: {
|
export function BettingStreakModal(props: {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
@ -11,12 +16,13 @@ export function BettingStreakModal(props: {
|
||||||
<Modal open={isOpen} setOpen={setOpen}>
|
<Modal open={isOpen} setOpen={setOpen}>
|
||||||
<Col className="items-center gap-4 rounded-md bg-white px-8 py-6">
|
<Col className="items-center gap-4 rounded-md bg-white px-8 py-6">
|
||||||
<span className={'text-8xl'}>🔥</span>
|
<span className={'text-8xl'}>🔥</span>
|
||||||
<span>Betting streaks are here!</span>
|
<span>Daily betting streaks</span>
|
||||||
<Col className={'gap-2'}>
|
<Col className={'gap-2'}>
|
||||||
<span className={'text-indigo-700'}>• What are they?</span>
|
<span className={'text-indigo-700'}>• What are they?</span>
|
||||||
<span className={'ml-2'}>
|
<span className={'ml-2'}>
|
||||||
You get a reward for every consecutive day that you place a bet. The
|
You get {formatMoney(BETTING_STREAK_BONUS_AMOUNT)} more for each day
|
||||||
more days you bet in a row, the more you earn!
|
of consecutive betting up to {formatMoney(BETTING_STREAK_BONUS_MAX)}
|
||||||
|
. The more days you bet in a row, the more you earn!
|
||||||
</span>
|
</span>
|
||||||
<span className={'text-indigo-700'}>
|
<span className={'text-indigo-700'}>
|
||||||
• Where can I check my streak?
|
• Where can I check my streak?
|
||||||
|
|
|
@ -79,7 +79,7 @@ export function Tooltip(props: {
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
ref={floating}
|
ref={floating}
|
||||||
style={{ position: strategy, top: y ?? 0, left: x ?? 0 }}
|
style={{ position: strategy, top: y ?? 0, left: x ?? 0 }}
|
||||||
className="z-10 max-w-xs rounded bg-slate-700 px-2 py-1 text-center text-sm text-white"
|
className="z-10 max-w-xs whitespace-normal rounded bg-slate-700 px-2 py-1 text-center text-sm text-white"
|
||||||
{...getFloatingProps()}
|
{...getFloatingProps()}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
|
|
|
@ -72,13 +72,28 @@ export function UserPage(props: { user: User }) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const claimedMana = router.query['claimed-mana'] === 'yes'
|
const claimedMana = router.query['claimed-mana'] === 'yes'
|
||||||
setShowConfetti(claimedMana)
|
|
||||||
const showBettingStreak = router.query['show'] === 'betting-streak'
|
const showBettingStreak = router.query['show'] === 'betting-streak'
|
||||||
setShowBettingStreakModal(showBettingStreak)
|
setShowBettingStreakModal(showBettingStreak)
|
||||||
|
setShowConfetti(claimedMana || showBettingStreak)
|
||||||
|
|
||||||
const showLoansModel = router.query['show'] === 'loans'
|
const showLoansModel = router.query['show'] === 'loans'
|
||||||
setShowLoansModal(showLoansModel)
|
setShowLoansModal(showLoansModel)
|
||||||
}, [router])
|
|
||||||
|
const query = { ...router.query }
|
||||||
|
if (query.claimedMana || query.show) {
|
||||||
|
delete query['claimed-mana']
|
||||||
|
delete query['show']
|
||||||
|
router.replace(
|
||||||
|
{
|
||||||
|
pathname: router.pathname,
|
||||||
|
query,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ shallow: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
const profit = user.profitCached.allTime
|
const profit = user.profitCached.allTime
|
||||||
|
|
||||||
|
@ -89,10 +104,9 @@ export function UserPage(props: { user: User }) {
|
||||||
description={user.bio ?? ''}
|
description={user.bio ?? ''}
|
||||||
url={`/${user.username}`}
|
url={`/${user.username}`}
|
||||||
/>
|
/>
|
||||||
{showConfetti ||
|
{showConfetti && (
|
||||||
(showBettingStreakModal && (
|
<FullscreenConfetti recycle={false} numberOfPieces={300} />
|
||||||
<FullscreenConfetti recycle={false} numberOfPieces={300} />
|
)}
|
||||||
))}
|
|
||||||
<BettingStreakModal
|
<BettingStreakModal
|
||||||
isOpen={showBettingStreakModal}
|
isOpen={showBettingStreakModal}
|
||||||
setOpen={setShowBettingStreakModal}
|
setOpen={setShowBettingStreakModal}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import {
|
import {
|
||||||
collection,
|
collection,
|
||||||
deleteDoc,
|
deleteDoc,
|
||||||
|
@ -17,14 +16,13 @@ import { sortBy, sum } from 'lodash'
|
||||||
|
|
||||||
import { coll, getValues, listenForValue, listenForValues } from './utils'
|
import { coll, getValues, listenForValue, listenForValues } from './utils'
|
||||||
import { BinaryContract, Contract } from 'common/contract'
|
import { BinaryContract, Contract } from 'common/contract'
|
||||||
import { getDpmProbability } from 'common/calculate-dpm'
|
|
||||||
import { createRNG, shuffle } from 'common/util/random'
|
import { createRNG, shuffle } from 'common/util/random'
|
||||||
import { getCpmmProbability } from 'common/calculate-cpmm'
|
|
||||||
import { formatMoney, formatPercent } from 'common/util/format'
|
import { formatMoney, formatPercent } from 'common/util/format'
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Comment } from 'common/comment'
|
import { Comment } from 'common/comment'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
import { getBinaryProb } from 'common/contract-details'
|
||||||
|
|
||||||
export const contracts = coll<Contract>('contracts')
|
export const contracts = coll<Contract>('contracts')
|
||||||
|
|
||||||
|
@ -49,20 +47,6 @@ export function contractUrl(contract: Contract) {
|
||||||
return `https://${ENV_CONFIG.domain}${contractPath(contract)}`
|
return `https://${ENV_CONFIG.domain}${contractPath(contract)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function contractMetrics(contract: Contract) {
|
|
||||||
const { createdTime, resolutionTime, isResolved } = contract
|
|
||||||
|
|
||||||
const createdDate = dayjs(createdTime).format('MMM D')
|
|
||||||
|
|
||||||
const resolvedDate = isResolved
|
|
||||||
? dayjs(resolutionTime).format('MMM D')
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const volumeLabel = `${formatMoney(contract.volume)} bet`
|
|
||||||
|
|
||||||
return { volumeLabel, createdDate, resolvedDate }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function contractPool(contract: Contract) {
|
export function contractPool(contract: Contract) {
|
||||||
return contract.mechanism === 'cpmm-1'
|
return contract.mechanism === 'cpmm-1'
|
||||||
? formatMoney(contract.totalLiquidity)
|
? formatMoney(contract.totalLiquidity)
|
||||||
|
@ -71,17 +55,6 @@ export function contractPool(contract: Contract) {
|
||||||
: 'Empty pool'
|
: 'Empty pool'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBinaryProb(contract: BinaryContract) {
|
|
||||||
const { pool, resolutionProbability, mechanism } = contract
|
|
||||||
|
|
||||||
return (
|
|
||||||
resolutionProbability ??
|
|
||||||
(mechanism === 'cpmm-1'
|
|
||||||
? getCpmmProbability(pool, contract.p)
|
|
||||||
: getDpmProbability(contract.totalShares))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBinaryProbPercent(contract: BinaryContract) {
|
export function getBinaryProbPercent(contract: BinaryContract) {
|
||||||
return formatPercent(getBinaryProb(contract))
|
return formatPercent(getBinaryProb(contract))
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,13 +36,13 @@ import { AlertBox } from 'web/components/alert-box'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
||||||
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
import { getOpenGraphProps } from 'web/components/contract/contract-card-preview'
|
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { ContractComment } from 'common/comment'
|
import { ContractComment } from 'common/comment'
|
||||||
import { listUsers } from 'web/lib/firebase/users'
|
import { listUsers } from 'web/lib/firebase/users'
|
||||||
import { FeedComment } from 'web/components/feed/feed-comments'
|
import { FeedComment } from 'web/components/feed/feed-comments'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { FeedBet } from 'web/components/feed/feed-bets'
|
import { FeedBet } from 'web/components/feed/feed-bets'
|
||||||
|
import { getOpenGraphProps } from 'common/contract-details'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: {
|
export async function getStaticPropz(props: {
|
||||||
|
|
|
@ -28,11 +28,11 @@ import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
import { Bet, listAllBets } from 'web/lib/firebase/bets'
|
import { Bet, listAllBets } from 'web/lib/firebase/bets'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
import { getOpenGraphProps } from 'web/components/contract/contract-card-preview'
|
|
||||||
import Custom404 from 'web/pages/404'
|
import Custom404 from 'web/pages/404'
|
||||||
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
import { BinaryContract } from 'common/contract'
|
import { BinaryContract } from 'common/contract'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
|
import { getOpenGraphProps } from 'common/contract-details'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,9 @@ import { User } from 'common/user'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const txns = await getAllCharityTxns()
|
let txns = await getAllCharityTxns()
|
||||||
|
// Sort by newest txns first
|
||||||
|
txns = sortBy(txns, 'createdTime').reverse()
|
||||||
const totals = mapValues(groupBy(txns, 'toId'), (txns) =>
|
const totals = mapValues(groupBy(txns, 'toId'), (txns) =>
|
||||||
sumBy(txns, (txn) => txn.amount)
|
sumBy(txns, (txn) => txn.amount)
|
||||||
)
|
)
|
||||||
|
@ -37,7 +39,8 @@ export async function getStaticProps() {
|
||||||
])
|
])
|
||||||
const matches = quadraticMatches(txns, totalRaised)
|
const matches = quadraticMatches(txns, totalRaised)
|
||||||
const numDonors = uniqBy(txns, (txn) => txn.fromId).length
|
const numDonors = uniqBy(txns, (txn) => txn.fromId).length
|
||||||
const mostRecentDonor = await getUser(txns[txns.length - 1].fromId)
|
const mostRecentDonor = await getUser(txns[0].fromId)
|
||||||
|
const mostRecentCharity = txns[0].toId
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
@ -47,6 +50,7 @@ export async function getStaticProps() {
|
||||||
txns,
|
txns,
|
||||||
numDonors,
|
numDonors,
|
||||||
mostRecentDonor,
|
mostRecentDonor,
|
||||||
|
mostRecentCharity,
|
||||||
},
|
},
|
||||||
revalidate: 60,
|
revalidate: 60,
|
||||||
}
|
}
|
||||||
|
@ -71,7 +75,7 @@ function DonatedStats(props: { stats: Stat[] }) {
|
||||||
{stat.name}
|
{stat.name}
|
||||||
</dt>
|
</dt>
|
||||||
|
|
||||||
<dd className="mt-1 text-3xl font-semibold text-gray-900">
|
<dd className="mt-1 text-2xl font-semibold text-gray-900">
|
||||||
{stat.url ? (
|
{stat.url ? (
|
||||||
<SiteLink href={stat.url}>{stat.stat}</SiteLink>
|
<SiteLink href={stat.url}>{stat.stat}</SiteLink>
|
||||||
) : (
|
) : (
|
||||||
|
@ -91,11 +95,21 @@ export default function Charity(props: {
|
||||||
txns: Txn[]
|
txns: Txn[]
|
||||||
numDonors: number
|
numDonors: number
|
||||||
mostRecentDonor: User
|
mostRecentDonor: User
|
||||||
|
mostRecentCharity: string
|
||||||
}) {
|
}) {
|
||||||
const { totalRaised, charities, matches, numDonors, mostRecentDonor } = props
|
const {
|
||||||
|
totalRaised,
|
||||||
|
charities,
|
||||||
|
matches,
|
||||||
|
mostRecentCharity,
|
||||||
|
mostRecentDonor,
|
||||||
|
} = props
|
||||||
|
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const debouncedQuery = debounce(setQuery, 50)
|
const debouncedQuery = debounce(setQuery, 50)
|
||||||
|
const recentCharityName =
|
||||||
|
charities.find((charity) => charity.id === mostRecentCharity)?.name ??
|
||||||
|
'Nobody'
|
||||||
|
|
||||||
const filterCharities = useMemo(
|
const filterCharities = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -143,15 +157,16 @@ export default function Charity(props: {
|
||||||
name: 'Raised by Manifold users',
|
name: 'Raised by Manifold users',
|
||||||
stat: manaToUSD(totalRaised),
|
stat: manaToUSD(totalRaised),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Number of donors',
|
|
||||||
stat: `${numDonors}`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Most recent donor',
|
name: 'Most recent donor',
|
||||||
stat: mostRecentDonor.name ?? 'Nobody',
|
stat: mostRecentDonor.name ?? 'Nobody',
|
||||||
url: `/${mostRecentDonor.username}`,
|
url: `/${mostRecentDonor.username}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Most recent donation',
|
||||||
|
stat: recentCharityName,
|
||||||
|
url: `/charity/${mostRecentCharity}`,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Spacer h={10} />
|
<Spacer h={10} />
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
MAX_DESCRIPTION_LENGTH,
|
MAX_DESCRIPTION_LENGTH,
|
||||||
MAX_QUESTION_LENGTH,
|
MAX_QUESTION_LENGTH,
|
||||||
outcomeType,
|
outcomeType,
|
||||||
|
visibility,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
|
@ -150,6 +151,7 @@ export function NewContract(props: {
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
const [showGroupSelector, setShowGroupSelector] = useState(true)
|
const [showGroupSelector, setShowGroupSelector] = useState(true)
|
||||||
|
const [visibility, setVisibility] = useState<visibility>('public')
|
||||||
|
|
||||||
const closeTime = closeDate
|
const closeTime = closeDate
|
||||||
? dayjs(`${closeDate}T${closeHoursMinutes}`).valueOf()
|
? dayjs(`${closeDate}T${closeHoursMinutes}`).valueOf()
|
||||||
|
@ -234,6 +236,7 @@ export function NewContract(props: {
|
||||||
isLogScale,
|
isLogScale,
|
||||||
answers,
|
answers,
|
||||||
groupId: selectedGroup?.id,
|
groupId: selectedGroup?.id,
|
||||||
|
visibility,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
track('create market', {
|
track('create market', {
|
||||||
|
@ -367,17 +370,33 @@ export function NewContract(props: {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={'mt-2'}>
|
<div className="form-control mb-1 items-start gap-1">
|
||||||
<GroupSelector
|
<label className="label gap-2">
|
||||||
selectedGroup={selectedGroup}
|
<span className="mb-1">Visibility</span>
|
||||||
setSelectedGroup={setSelectedGroup}
|
<InfoTooltip text="Whether the market will be listed on the home page." />
|
||||||
creator={creator}
|
</label>
|
||||||
options={{ showSelector: showGroupSelector, showLabel: true }}
|
<ChoicesToggleGroup
|
||||||
|
currentChoice={visibility}
|
||||||
|
setChoice={(choice) => setVisibility(choice as visibility)}
|
||||||
|
choicesMap={{
|
||||||
|
Public: 'public',
|
||||||
|
Unlisted: 'unlisted',
|
||||||
|
}}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
|
<GroupSelector
|
||||||
|
selectedGroup={selectedGroup}
|
||||||
|
setSelectedGroup={setSelectedGroup}
|
||||||
|
creator={creator}
|
||||||
|
options={{ showSelector: showGroupSelector, showLabel: true }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Spacer h={6} />
|
||||||
|
|
||||||
<div className="form-control mb-1 items-start">
|
<div className="form-control mb-1 items-start">
|
||||||
<label className="label mb-1 gap-2">
|
<label className="label mb-1 gap-2">
|
||||||
<span>Question closes in</span>
|
<span>Question closes in</span>
|
||||||
|
|
|
@ -551,7 +551,12 @@ function AddContractButton(props: { group: Group; user: User }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'flex justify-center'}>
|
<div className={'flex justify-center'}>
|
||||||
<Button size="sm" color="gradient" onClick={() => setOpen(true)}>
|
<Button
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
size="sm"
|
||||||
|
color="gradient"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
Add market
|
Add market
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -607,9 +612,6 @@ function AddContractButton(props: { group: Group; user: User }) {
|
||||||
user={user}
|
user={user}
|
||||||
hideOrderSelector={true}
|
hideOrderSelector={true}
|
||||||
onContractClick={addContractToCurrentGroup}
|
onContractClick={addContractToCurrentGroup}
|
||||||
overrideGridClassName={
|
|
||||||
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
|
|
||||||
}
|
|
||||||
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
||||||
additionalFilter={{ excludeContractIds: group.contractIds }}
|
additionalFilter={{ excludeContractIds: group.contractIds }}
|
||||||
highlightOptions={{
|
highlightOptions={{
|
||||||
|
|
|
@ -377,6 +377,7 @@ function IncomeNotificationItem(props: {
|
||||||
function reasonAndLink(simple: boolean) {
|
function reasonAndLink(simple: boolean) {
|
||||||
const { sourceText } = notification
|
const { sourceText } = notification
|
||||||
let reasonText = ''
|
let reasonText = ''
|
||||||
|
|
||||||
if (sourceType === 'bonus' && sourceText) {
|
if (sourceType === 'bonus' && sourceText) {
|
||||||
reasonText = !simple
|
reasonText = !simple
|
||||||
? `Bonus for ${
|
? `Bonus for ${
|
||||||
|
@ -385,13 +386,20 @@ function IncomeNotificationItem(props: {
|
||||||
: 'bonus on'
|
: 'bonus on'
|
||||||
} else if (sourceType === 'tip') {
|
} else if (sourceType === 'tip') {
|
||||||
reasonText = !simple ? `tipped you on` : `in tips on`
|
reasonText = !simple ? `tipped you on` : `in tips on`
|
||||||
|
} else if (sourceType === 'betting_streak_bonus') {
|
||||||
|
reasonText = 'for your'
|
||||||
} else if (sourceType === 'loan' && sourceText) {
|
} else if (sourceType === 'loan' && sourceText) {
|
||||||
reasonText = `of your invested bets returned as`
|
reasonText = `of your invested bets returned as`
|
||||||
} else if (sourceType === 'betting_streak_bonus' && sourceText) {
|
|
||||||
reasonText = `for your ${
|
|
||||||
parseInt(sourceText) / BETTING_STREAK_BONUS_AMOUNT
|
|
||||||
}-day`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bettingStreakText =
|
||||||
|
sourceType === 'betting_streak_bonus' &&
|
||||||
|
(sourceText
|
||||||
|
? `🔥 ${
|
||||||
|
parseInt(sourceText) / BETTING_STREAK_BONUS_AMOUNT
|
||||||
|
} day Betting Streak`
|
||||||
|
: 'Betting Streak')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{reasonText}
|
{reasonText}
|
||||||
|
@ -405,13 +413,13 @@ function IncomeNotificationItem(props: {
|
||||||
)
|
)
|
||||||
) : sourceType === 'betting_streak_bonus' ? (
|
) : sourceType === 'betting_streak_bonus' ? (
|
||||||
simple ? (
|
simple ? (
|
||||||
<span className={'ml-1 font-bold'}>Betting Streak</span>
|
<span className={'ml-1 font-bold'}>{bettingStreakText}</span>
|
||||||
) : (
|
) : (
|
||||||
<SiteLink
|
<SiteLink
|
||||||
className={'ml-1 font-bold'}
|
className={'ml-1 font-bold'}
|
||||||
href={'/betting-streak-bonus'}
|
href={'/betting-streak-bonus'}
|
||||||
>
|
>
|
||||||
Betting Streak
|
{bettingStreakText}
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -9947,6 +9947,11 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.10.3"
|
"@babel/runtime" "^7.10.3"
|
||||||
|
|
||||||
|
react-masonry-css@1.0.16:
|
||||||
|
version "1.0.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-masonry-css/-/react-masonry-css-1.0.16.tgz#72b28b4ae3484e250534700860597553a10f1a2c"
|
||||||
|
integrity sha512-KSW0hR2VQmltt/qAa3eXOctQDyOu7+ZBevtKgpNDSzT7k5LA/0XntNa9z9HKCdz3QlxmJHglTZ18e4sX4V8zZQ==
|
||||||
|
|
||||||
react-motion@^0.5.2:
|
react-motion@^0.5.2:
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
|
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user