Merge branch 'main' into cpmm
This commit is contained in:
commit
a9ccca6458
14
common/access.ts
Normal file
14
common/access.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export function isWhitelisted(email?: string) {
|
||||
return true
|
||||
// e.g. return email.endsWith('@theoremone.co') || isAdmin(email)
|
||||
}
|
||||
|
||||
export function isAdmin(email: string) {
|
||||
const ADMINS = [
|
||||
'akrolsmir@gmail.com', // Austin
|
||||
'jahooma@gmail.com', // James
|
||||
'taowell@gmail.com', // Stephen
|
||||
'manticmarkets@gmail.com', // Manifold
|
||||
]
|
||||
return ADMINS.includes(email)
|
||||
}
|
|
@ -4,6 +4,7 @@ export type Bet = {
|
|||
contractId: string
|
||||
|
||||
amount: number // bet size; negative if SELL bet
|
||||
loanAmount?: number
|
||||
outcome: string
|
||||
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
||||
|
||||
|
@ -21,3 +22,5 @@ export type Bet = {
|
|||
|
||||
createdTime: number
|
||||
}
|
||||
|
||||
export const MAX_LOAN_PER_CONTRACT = 20
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Bet } from './bet'
|
||||
import * as _ from 'lodash'
|
||||
import { Bet, MAX_LOAN_PER_CONTRACT } from './bet'
|
||||
import {
|
||||
calculateShares,
|
||||
getProbability,
|
||||
|
@ -20,6 +21,7 @@ export const getNewBinaryCpmmBetInfo = (
|
|||
outcome: 'YES' | 'NO',
|
||||
amount: number,
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
loanAmount: number,
|
||||
newBetId: string
|
||||
) => {
|
||||
const { pool, k } = contract
|
||||
|
@ -32,7 +34,7 @@ export const getNewBinaryCpmmBetInfo = (
|
|||
? [y - shares + amount, amount]
|
||||
: [amount, n - shares + amount]
|
||||
|
||||
const newBalance = user.balance - amount
|
||||
const newBalance = user.balance - (amount - loanAmount)
|
||||
|
||||
const probBefore = getCpmmProbability(pool)
|
||||
const newPool = { YES: newY, NO: newN }
|
||||
|
@ -45,6 +47,7 @@ export const getNewBinaryCpmmBetInfo = (
|
|||
amount,
|
||||
shares,
|
||||
outcome,
|
||||
loanAmount,
|
||||
probBefore,
|
||||
probAfter,
|
||||
createdTime: Date.now(),
|
||||
|
@ -58,6 +61,7 @@ export const getNewBinaryDpmBetInfo = (
|
|||
outcome: 'YES' | 'NO',
|
||||
amount: number,
|
||||
contract: FullContract<DPM, Binary>,
|
||||
loanAmount: number,
|
||||
newBetId: string
|
||||
) => {
|
||||
const { YES: yesPool, NO: noPool } = contract.pool
|
||||
|
@ -91,6 +95,7 @@ export const getNewBinaryDpmBetInfo = (
|
|||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
loanAmount,
|
||||
shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
|
@ -98,7 +103,7 @@ export const getNewBinaryDpmBetInfo = (
|
|||
createdTime: Date.now(),
|
||||
}
|
||||
|
||||
const newBalance = user.balance - amount
|
||||
const newBalance = user.balance - (amount - loanAmount)
|
||||
|
||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||
}
|
||||
|
@ -108,6 +113,7 @@ export const getNewMultiBetInfo = (
|
|||
outcome: string,
|
||||
amount: number,
|
||||
contract: FullContract<DPM, Multi | FreeResponse>,
|
||||
loanAmount: number,
|
||||
newBetId: string
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
|
@ -131,6 +137,7 @@ export const getNewMultiBetInfo = (
|
|||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
loanAmount,
|
||||
shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
|
@ -138,7 +145,17 @@ export const getNewMultiBetInfo = (
|
|||
createdTime: Date.now(),
|
||||
}
|
||||
|
||||
const newBalance = user.balance - amount
|
||||
const newBalance = user.balance - (amount - loanAmount)
|
||||
|
||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||
}
|
||||
|
||||
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
|
||||
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||
const prevLoanAmount = _.sumBy(openBets, (bet) => bet.loanAmount ?? 0)
|
||||
const loanAmount = Math.min(
|
||||
newBetAmount,
|
||||
MAX_LOAN_PER_CONTRACT - prevLoanAmount
|
||||
)
|
||||
return loanAmount
|
||||
}
|
||||
|
|
|
@ -281,3 +281,12 @@ export const getPayoutsMultiOutcome = (
|
|||
.map(({ userId, payout }) => ({ userId, payout }))
|
||||
.concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
|
||||
}
|
||||
|
||||
export const getLoanPayouts = (bets: Bet[]) => {
|
||||
const betsWithLoans = bets.filter((bet) => bet.loanAmount)
|
||||
const betsByUser = _.groupBy(betsWithLoans, (bet) => bet.userId)
|
||||
const loansByUser = _.mapValues(betsByUser, (bets) =>
|
||||
_.sumBy(bets, (bet) => -(bet.loanAmount ?? 0))
|
||||
)
|
||||
return _.toPairs(loansByUser).map(([userId, payout]) => ({ userId, payout }))
|
||||
}
|
||||
|
|
|
@ -48,8 +48,9 @@ export function scoreUsersByContract(
|
|||
const investments = bets
|
||||
.filter((bet) => !bet.sale)
|
||||
.map((bet) => {
|
||||
const { userId, amount } = bet
|
||||
return { userId, payout: -amount }
|
||||
const { userId, amount, loanAmount } = bet
|
||||
const payout = -amount - (loanAmount ?? 0)
|
||||
return { userId, payout }
|
||||
})
|
||||
|
||||
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
|
||||
|
|
|
@ -16,7 +16,7 @@ export const getSellBetInfo = (
|
|||
newBetId: string
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
const { id: betId, amount, shares, outcome } = bet
|
||||
const { id: betId, amount, shares, outcome, loanAmount } = bet
|
||||
|
||||
const adjShareValue = calculateShareValue(contract, bet)
|
||||
|
||||
|
@ -62,7 +62,7 @@ export const getSellBetInfo = (
|
|||
},
|
||||
}
|
||||
|
||||
const newBalance = user.balance + saleAmount
|
||||
const newBalance = user.balance + saleAmount - (loanAmount ?? 0)
|
||||
|
||||
return {
|
||||
newBet,
|
||||
|
|
|
@ -28,6 +28,8 @@ export type PrivateUser = {
|
|||
|
||||
email?: string
|
||||
unsubscribedFromResolutionEmails?: boolean
|
||||
unsubscribedFromCommentEmails?: boolean
|
||||
unsubscribedFromAnswerEmails?: boolean
|
||||
initialDeviceToken?: string
|
||||
initialIpAddress?: string
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"source": "functions"
|
||||
},
|
||||
"firestore": {
|
||||
"rules": "firestore.rules"
|
||||
"rules": "firestore.rules",
|
||||
"indexes": "firestore.indexes.json"
|
||||
}
|
||||
}
|
||||
|
|
296
firestore.indexes.json
Normal file
296
firestore.indexes.json
Normal file
|
@ -0,0 +1,296 @@
|
|||
{
|
||||
"indexes": [
|
||||
{
|
||||
"collectionGroup": "contracts",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "creatorId",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdTime",
|
||||
"order": "DESCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "contracts",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "isResolved",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "closeTime",
|
||||
"order": "ASCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "contracts",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "isResolved",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "closeTime",
|
||||
"order": "ASCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "contracts",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "isResolved",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "volume24Hours",
|
||||
"order": "ASCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "contracts",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "isResolved",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "volume24Hours",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "closeTime",
|
||||
"order": "ASCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "contracts",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "isResolved",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "volume24Hours",
|
||||
"order": "DESCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "contracts",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "isResolved",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "volume24Hours",
|
||||
"order": "DESCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "contracts",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "slug",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdTime",
|
||||
"order": "DESCENDING"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"fieldOverrides": [
|
||||
{
|
||||
"collectionGroup": "answers",
|
||||
"fieldPath": "isAnte",
|
||||
"indexes": [
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "DESCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"arrayConfig": "CONTAINS",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION_GROUP"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "answers",
|
||||
"fieldPath": "username",
|
||||
"indexes": [
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "DESCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"arrayConfig": "CONTAINS",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION_GROUP"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "bets",
|
||||
"fieldPath": "createdTime",
|
||||
"indexes": [
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "DESCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"arrayConfig": "CONTAINS",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION_GROUP"
|
||||
},
|
||||
{
|
||||
"order": "DESCENDING",
|
||||
"queryScope": "COLLECTION_GROUP"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "bets",
|
||||
"fieldPath": "userId",
|
||||
"indexes": [
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "DESCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"arrayConfig": "CONTAINS",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION_GROUP"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "comments",
|
||||
"fieldPath": "createdTime",
|
||||
"indexes": [
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "DESCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"arrayConfig": "CONTAINS",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "DESCENDING",
|
||||
"queryScope": "COLLECTION_GROUP"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "comments",
|
||||
"fieldPath": "userUsername",
|
||||
"indexes": [
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "DESCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"arrayConfig": "CONTAINS",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION_GROUP"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "followers",
|
||||
"fieldPath": "userId",
|
||||
"indexes": [
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "DESCENDING",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"arrayConfig": "CONTAINS",
|
||||
"queryScope": "COLLECTION"
|
||||
},
|
||||
{
|
||||
"order": "ASCENDING",
|
||||
"queryScope": "COLLECTION_GROUP"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
3
functions/.gitignore
vendored
3
functions/.gitignore
vendored
|
@ -1,3 +1,6 @@
|
|||
# Secrets
|
||||
.env*
|
||||
|
||||
# Compiled JavaScript files
|
||||
lib/**/*.js
|
||||
lib/**/*.js.map
|
||||
|
|
|
@ -3,9 +3,11 @@ import * as admin from 'firebase-admin'
|
|||
|
||||
import { Contract } from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
import { getNewMultiBetInfo } from '../../common/new-bet'
|
||||
import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { getValues } from './utils'
|
||||
import { getContract, getValues } from './utils'
|
||||
import { sendNewAnswerEmail } from './emails'
|
||||
import { Bet } from '../../common/bet'
|
||||
|
||||
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||
async (
|
||||
|
@ -28,7 +30,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
return { status: 'error', message: 'Invalid text' }
|
||||
|
||||
// Run as transaction to prevent race conditions.
|
||||
return await firestore.runTransaction(async (transaction) => {
|
||||
const result = await firestore.runTransaction(async (transaction) => {
|
||||
const userDoc = firestore.doc(`users/${userId}`)
|
||||
const userSnap = await transaction.get(userDoc)
|
||||
if (!userSnap.exists)
|
||||
|
@ -54,6 +56,11 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
if (closeTime && Date.now() > closeTime)
|
||||
return { status: 'error', message: 'Trading is closed' }
|
||||
|
||||
const yourBetsSnap = await transaction.get(
|
||||
contractDoc.collection('bets').where('userId', '==', userId)
|
||||
)
|
||||
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
||||
|
||||
const [lastAnswer] = await getValues<Answer>(
|
||||
firestore
|
||||
.collection(`contracts/${contractId}/answers`)
|
||||
|
@ -91,8 +98,17 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
.collection(`contracts/${contractId}/bets`)
|
||||
.doc()
|
||||
|
||||
const loanAmount = getLoanAmount(yourBets, amount)
|
||||
|
||||
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
||||
getNewMultiBetInfo(user, answerId, amount, contract, newBetDoc.id)
|
||||
getNewMultiBetInfo(
|
||||
user,
|
||||
answerId,
|
||||
amount,
|
||||
loanAmount,
|
||||
contract,
|
||||
newBetDoc.id
|
||||
)
|
||||
|
||||
transaction.create(newBetDoc, newBet)
|
||||
transaction.update(contractDoc, {
|
||||
|
@ -103,8 +119,15 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
})
|
||||
transaction.update(userDoc, { balance: newBalance })
|
||||
|
||||
return { status: 'success', answerId, betId: newBetDoc.id }
|
||||
return { status: 'success', answerId, betId: newBetDoc.id, answer }
|
||||
})
|
||||
|
||||
const { answer } = result
|
||||
const contract = await getContract(contractId)
|
||||
|
||||
if (answer && contract) await sendNewAnswerEmail(answer, contract)
|
||||
|
||||
return result
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
cleanUsername,
|
||||
} from '../../common/util/clean-username'
|
||||
import { sendWelcomeEmail } from './emails'
|
||||
import { isWhitelisted } from '../../common/access'
|
||||
|
||||
export const createUser = functions
|
||||
.runWith({ minInstances: 1 })
|
||||
|
@ -32,6 +33,9 @@ export const createUser = functions
|
|||
const fbUser = await admin.auth().getUser(userId)
|
||||
|
||||
const email = fbUser.email
|
||||
if (!isWhitelisted(email)) {
|
||||
return { status: 'error', message: `${email} is not whitelisted` }
|
||||
}
|
||||
const emailName = email?.replace(/@.*$/, '')
|
||||
|
||||
const rawName = fbUser.displayName || emailName || 'User' + randomString(4)
|
||||
|
|
561
functions/src/email-templates/market-answer-comment.html
Normal file
561
functions/src/email-templates/market-answer-comment.html
Normal file
|
@ -0,0 +1,561 @@
|
|||
<!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>Market comment</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"
|
||||
>
|
||||
<img
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
width="300"
|
||||
style="height: auto"
|
||||
alt="Manifold Markets"
|
||||
/>
|
||||
</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: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
Answer {{answerNumber}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 20px;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<span style="white-space: pre-line"
|
||||
>{{answer}}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
src="{{commentorAvatarUrl}}"
|
||||
width="30"
|
||||
height="30"
|
||||
style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
"
|
||||
alt=""
|
||||
/>
|
||||
<span style="font-weight: bold"
|
||||
>{{commentorName}}</span
|
||||
>
|
||||
{{betDescription}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<span style="white-space: pre-line"
|
||||
>{{comment}}</span
|
||||
>
|
||||
</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 comment</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="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
"
|
||||
>unsubscribe</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>
|
499
functions/src/email-templates/market-answer.html
Normal file
499
functions/src/email-templates/market-answer.html
Normal file
|
@ -0,0 +1,499 @@
|
|||
<!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>Market answer</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"
|
||||
>
|
||||
<img
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
width="300"
|
||||
style="height: auto"
|
||||
alt="Manifold Markets"
|
||||
/>
|
||||
</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: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
font-weight: bold;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
src="{{avatarUrl}}"
|
||||
width="30"
|
||||
height="30"
|
||||
style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
"
|
||||
alt=""
|
||||
/>
|
||||
{{name}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<span style="white-space: pre-line"
|
||||
>{{answer}}</span
|
||||
>
|
||||
</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 answer</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="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
"
|
||||
>unsubscribe</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>
|
647
functions/src/email-templates/market-close.html
Normal file
647
functions/src/email-templates/market-close.html
Normal file
|
@ -0,0 +1,647 @@
|
|||
<!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>Market closed</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 40px 0;
|
||||
text-align: left;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<img
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
width="300"
|
||||
style="height: auto"
|
||||
alt="Manifold Markets"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 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 6px 0;
|
||||
text-align: left;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
You asked
|
||||
</td>
|
||||
</tr>
|
||||
<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: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<a
|
||||
href="{{url}}"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
'Lucida Grande', sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 24px;
|
||||
color: #000;
|
||||
line-height: 1.2em;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
margin: 0 0 0 0;
|
||||
color: #4337c9;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
"
|
||||
>
|
||||
{{question}}</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<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: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 0px;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<h2
|
||||
class="aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
'Lucida Grande', sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 24px;
|
||||
color: #000;
|
||||
line-height: 1.2em;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin: 10px 0 0;
|
||||
"
|
||||
align="center"
|
||||
>
|
||||
Market closed
|
||||
</h2>
|
||||
</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;
|
||||
"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
Hi {{name}},
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
A market you created has closed. It's attracted
|
||||
<span style="font-weight: bold">{{pool}}</span> in
|
||||
bets — congrats!
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
Resolve your market to earn {{creatorFee}}% of the
|
||||
profits as the creator commission.
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
Thanks,
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
Manifold Team
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td style="padding: 10px 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="{{url}}"
|
||||
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="https://us-central1-mantic-markets.cloudfunctions.net/unsubscribe?id={{userId}}&type=market-resolve"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
"
|
||||
>unsubscribe</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/market-comment.html
Normal file
501
functions/src/email-templates/market-comment.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>Market comment</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"
|
||||
>
|
||||
<img
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
width="300"
|
||||
style="height: auto"
|
||||
alt="Manifold Markets"
|
||||
/>
|
||||
</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: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
src="{{commentorAvatarUrl}}"
|
||||
width="30"
|
||||
height="30"
|
||||
style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
"
|
||||
alt=""
|
||||
/>
|
||||
<span style="font-weight: bold"
|
||||
>{{commentorName}}</span
|
||||
>
|
||||
{{betDescription}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<span style="white-space: pre-line"
|
||||
>{{comment}}</span
|
||||
>
|
||||
</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 comment</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="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
"
|
||||
>unsubscribe</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>
|
669
functions/src/email-templates/market-resolved.html
Normal file
669
functions/src/email-templates/market-resolved.html
Normal file
|
@ -0,0 +1,669 @@
|
|||
<!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>Market resolved</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 40px 0;
|
||||
text-align: left;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<img
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
width="300"
|
||||
style="height: auto"
|
||||
alt="Manifold Markets"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 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 6px 0;
|
||||
text-align: left;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
{{creatorName}} asked
|
||||
</td>
|
||||
</tr>
|
||||
<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: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<a
|
||||
href="{{url}}"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
'Lucida Grande', sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 24px;
|
||||
color: #000;
|
||||
line-height: 1.2em;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
margin: 0 0 0 0;
|
||||
color: #4337c9;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
"
|
||||
>
|
||||
{{question}}</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<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: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 0px;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<h2
|
||||
class="aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
'Lucida Grande', sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 24px;
|
||||
color: #000;
|
||||
line-height: 1.2em;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin: 10px 0 0;
|
||||
"
|
||||
align="center"
|
||||
>
|
||||
Resolved {{outcome}}
|
||||
</h2>
|
||||
</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;
|
||||
"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
Dear {{name}},
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
A market you bet in has been resolved!
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
Your investment was
|
||||
<span style="font-weight: bold"
|
||||
>M$ {{investment}}</span
|
||||
>.
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
Your payout is
|
||||
<span style="font-weight: bold"
|
||||
>M$ {{payout}}</span
|
||||
>.
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
Thanks,
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
Manifold Team
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td style="padding: 10px 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="{{url}}"
|
||||
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="https://us-central1-mantic-markets.cloudfunctions.net/unsubscribe?id={{userId}}&type=market-resolved"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
"
|
||||
>unsubscribe</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>
|
|
@ -1,11 +1,14 @@
|
|||
import _ = require('lodash')
|
||||
import { Answer } from '../../common/answer'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { getProbability } from '../../common/calculate'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { CREATOR_FEE } from '../../common/fees'
|
||||
import { PrivateUser, User } from '../../common/user'
|
||||
import { formatMoney, formatPercent } from '../../common/util/format'
|
||||
import { sendTemplateEmail, sendTextEmail } from './send-email'
|
||||
import { getPrivateUser, getUser } from './utils'
|
||||
import { getPrivateUser, getUser, isProd } from './utils'
|
||||
|
||||
type market_resolved_template = {
|
||||
userId: string
|
||||
|
@ -13,13 +16,14 @@ type market_resolved_template = {
|
|||
creatorName: string
|
||||
question: string
|
||||
outcome: string
|
||||
investment: string
|
||||
payout: string
|
||||
url: string
|
||||
}
|
||||
|
||||
const toDisplayResolution = (
|
||||
outcome: string,
|
||||
prob: number,
|
||||
prob?: number,
|
||||
resolutions?: { [outcome: string]: number }
|
||||
) => {
|
||||
if (outcome === 'MKT' && resolutions) return 'MULTI'
|
||||
|
@ -28,7 +32,7 @@ const toDisplayResolution = (
|
|||
YES: 'YES',
|
||||
NO: 'NO',
|
||||
CANCEL: 'N/A',
|
||||
MKT: formatPercent(prob),
|
||||
MKT: formatPercent(prob ?? 0),
|
||||
}[outcome]
|
||||
|
||||
return display === undefined ? `#${outcome}` : display
|
||||
|
@ -36,6 +40,7 @@ const toDisplayResolution = (
|
|||
|
||||
export const sendMarketResolutionEmail = async (
|
||||
userId: string,
|
||||
investment: number,
|
||||
payout: number,
|
||||
creator: User,
|
||||
contract: Contract,
|
||||
|
@ -66,6 +71,7 @@ export const sendMarketResolutionEmail = async (
|
|||
creatorName: creator.name,
|
||||
question: contract.question,
|
||||
outcome,
|
||||
investment: `${Math.round(investment)}`,
|
||||
payout: `${Math.round(payout)}`,
|
||||
url: `https://manifold.markets/${creator.username}/${contract.slug}`,
|
||||
}
|
||||
|
@ -138,3 +144,119 @@ export const sendMarketCloseEmail = async (
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const sendNewCommentEmail = async (
|
||||
userId: string,
|
||||
commentCreator: User,
|
||||
contract: Contract,
|
||||
comment: Comment,
|
||||
bet: Bet,
|
||||
answer?: Answer
|
||||
) => {
|
||||
const privateUser = await getPrivateUser(userId)
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser.unsubscribedFromCommentEmails
|
||||
)
|
||||
return
|
||||
|
||||
const { question, creatorUsername, slug } = contract
|
||||
const marketUrl = `https://manifold.markets/${creatorUsername}/${slug}`
|
||||
|
||||
const unsubscribeUrl = `https://us-central1-${
|
||||
isProd ? 'mantic-markets' : 'dev-mantic-markets'
|
||||
}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-comment`
|
||||
|
||||
const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator
|
||||
const { text } = comment
|
||||
|
||||
const { amount, sale, outcome } = bet
|
||||
let betDescription = `${sale ? 'sold' : 'bought'} M$ ${Math.round(amount)}`
|
||||
|
||||
const subject = `Comment on ${question}`
|
||||
const from = `${commentorName} <info@manifold.markets>`
|
||||
|
||||
if (contract.outcomeType === 'FREE_RESPONSE') {
|
||||
const answerText = answer?.text ?? ''
|
||||
const answerNumber = `#${answer?.id ?? ''}`
|
||||
|
||||
await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
'market-answer-comment',
|
||||
{
|
||||
answer: answerText,
|
||||
answerNumber,
|
||||
commentorName,
|
||||
commentorAvatarUrl: commentorAvatarUrl ?? '',
|
||||
comment: text,
|
||||
marketUrl,
|
||||
unsubscribeUrl,
|
||||
betDescription,
|
||||
},
|
||||
{ from }
|
||||
)
|
||||
} else {
|
||||
betDescription = `${betDescription} of ${toDisplayResolution(outcome)}`
|
||||
|
||||
await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
'market-comment',
|
||||
{
|
||||
commentorName,
|
||||
commentorAvatarUrl: commentorAvatarUrl ?? '',
|
||||
comment: text,
|
||||
marketUrl,
|
||||
unsubscribeUrl,
|
||||
betDescription,
|
||||
},
|
||||
{ from }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const sendNewAnswerEmail = async (
|
||||
answer: Answer,
|
||||
contract: Contract
|
||||
) => {
|
||||
// Send to just the creator for now.
|
||||
const { creatorId: userId } = contract
|
||||
|
||||
// Don't send the creator's own answers.
|
||||
if (answer.userId === userId) return
|
||||
|
||||
const privateUser = await getPrivateUser(userId)
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser.unsubscribedFromAnswerEmails
|
||||
)
|
||||
return
|
||||
|
||||
const { question, creatorUsername, slug } = contract
|
||||
const { name, avatarUrl, text } = answer
|
||||
|
||||
const marketUrl = `https://manifold.markets/${creatorUsername}/${slug}`
|
||||
const unsubscribeUrl = `https://us-central1-${
|
||||
isProd ? 'mantic-markets' : 'dev-mantic-markets'
|
||||
}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-answer`
|
||||
|
||||
const subject = `New answer on ${question}`
|
||||
const from = `${name} <info@manifold.markets>`
|
||||
|
||||
await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
'market-answer',
|
||||
{
|
||||
name,
|
||||
avatarUrl: avatarUrl ?? '',
|
||||
answer: text,
|
||||
marketUrl,
|
||||
unsubscribeUrl,
|
||||
},
|
||||
{ from }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ export * from './create-contract'
|
|||
export * from './create-user'
|
||||
export * from './create-fold'
|
||||
export * from './create-answer'
|
||||
export * from './on-create-comment'
|
||||
export * from './on-fold-follow'
|
||||
export * from './on-fold-delete'
|
||||
export * from './unsubscribe'
|
||||
|
|
60
functions/src/on-create-comment.ts
Normal file
60
functions/src/on-create-comment.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
import { getContract, getUser, getValues } from './utils'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { sendNewCommentEmail } from './emails'
|
||||
import { Bet } from '../../common/bet'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const onCreateComment = functions.firestore
|
||||
.document('contracts/{contractId}/comments/{commentId}')
|
||||
.onCreate(async (change, context) => {
|
||||
const { contractId } = context.params as {
|
||||
contractId: string
|
||||
}
|
||||
|
||||
const contract = await getContract(contractId)
|
||||
if (!contract) return
|
||||
|
||||
const comment = change.data() as Comment
|
||||
|
||||
const commentCreator = await getUser(comment.userId)
|
||||
if (!commentCreator) return
|
||||
|
||||
const betSnapshot = await firestore
|
||||
.collection('contracts')
|
||||
.doc(contractId)
|
||||
.collection('bets')
|
||||
.doc(comment.betId)
|
||||
.get()
|
||||
const bet = betSnapshot.data() as Bet
|
||||
|
||||
const answer =
|
||||
contract.answers &&
|
||||
contract.answers.find((answer) => answer.id === bet.outcome)
|
||||
|
||||
const comments = await getValues<Comment>(
|
||||
firestore.collection('contracts').doc(contractId).collection('comments')
|
||||
)
|
||||
|
||||
const recipientUserIds = _.uniq([
|
||||
contract.creatorId,
|
||||
...comments.map((comment) => comment.userId),
|
||||
]).filter((id) => id !== comment.userId)
|
||||
|
||||
await Promise.all(
|
||||
recipientUserIds.map((userId) =>
|
||||
sendNewCommentEmail(
|
||||
userId,
|
||||
commentCreator,
|
||||
contract,
|
||||
comment,
|
||||
bet,
|
||||
answer
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
|
@ -7,8 +7,11 @@ import {
|
|||
getNewBinaryCpmmBetInfo,
|
||||
getNewBinaryDpmBetInfo,
|
||||
getNewMultiBetInfo,
|
||||
getLoanAmount,
|
||||
} from '../../common/new-bet'
|
||||
import { removeUndefinedProps } from '../../common/util/object'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { getValues } from './utils'
|
||||
|
||||
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||
async (
|
||||
|
@ -51,6 +54,11 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
if (closeTime && Date.now() > closeTime)
|
||||
return { status: 'error', message: 'Trading is closed' }
|
||||
|
||||
const yourBetsSnap = await transaction.get(
|
||||
contractDoc.collection('bets').where('userId', '==', userId)
|
||||
)
|
||||
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
||||
|
||||
if (outcomeType === 'FREE_RESPONSE') {
|
||||
const answerSnap = await transaction.get(
|
||||
contractDoc.collection('answers').doc(outcome)
|
||||
|
@ -63,6 +71,8 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
.collection(`contracts/${contractId}/bets`)
|
||||
.doc()
|
||||
|
||||
const loanAmount = getLoanAmount(yourBets, amount)
|
||||
|
||||
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
||||
outcomeType === 'BINARY'
|
||||
? mechanism === 'dpm-2'
|
||||
|
@ -71,6 +81,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
outcome as 'YES' | 'NO',
|
||||
amount,
|
||||
contract,
|
||||
loanAmount,
|
||||
newBetDoc.id
|
||||
)
|
||||
: (getNewBinaryCpmmBetInfo(
|
||||
|
@ -78,6 +89,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
outcome as 'YES' | 'NO',
|
||||
amount,
|
||||
contract,
|
||||
loanAmount,
|
||||
newBetDoc.id
|
||||
) as any)
|
||||
: getNewMultiBetInfo(
|
||||
|
@ -85,6 +97,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
outcome,
|
||||
amount,
|
||||
contract as any,
|
||||
loanAmount,
|
||||
newBetDoc.id
|
||||
)
|
||||
|
||||
|
|
|
@ -7,7 +7,12 @@ import { User } from '../../common/user'
|
|||
import { Bet } from '../../common/bet'
|
||||
import { getUser, payUser } from './utils'
|
||||
import { sendMarketResolutionEmail } from './emails'
|
||||
import { getPayouts, getPayoutsMultiOutcome } from '../../common/payouts'
|
||||
import {
|
||||
getLoanPayouts,
|
||||
getPayouts,
|
||||
getPayoutsMultiOutcome,
|
||||
} from '../../common/payouts'
|
||||
import { removeUndefinedProps } from '../../common/util/object'
|
||||
|
||||
export const resolveMarket = functions
|
||||
.runWith({ minInstances: 1 })
|
||||
|
@ -31,7 +36,7 @@ export const resolveMarket = functions
|
|||
if (!contractSnap.exists)
|
||||
return { status: 'error', message: 'Invalid contract' }
|
||||
const contract = contractSnap.data() as Contract
|
||||
const { creatorId, outcomeType } = contract
|
||||
const { creatorId, outcomeType, closeTime } = contract
|
||||
|
||||
if (outcomeType === 'BINARY') {
|
||||
if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome))
|
||||
|
@ -68,15 +73,21 @@ export const resolveMarket = functions
|
|||
const resolutionProbability =
|
||||
probabilityInt !== undefined ? probabilityInt / 100 : undefined
|
||||
|
||||
await contractDoc.update({
|
||||
const resolutionTime = Date.now()
|
||||
const newCloseTime = closeTime
|
||||
? Math.min(closeTime, resolutionTime)
|
||||
: closeTime
|
||||
|
||||
await contractDoc.update(
|
||||
removeUndefinedProps({
|
||||
isResolved: true,
|
||||
resolution: outcome,
|
||||
resolutionTime: Date.now(),
|
||||
...(resolutionProbability === undefined
|
||||
? {}
|
||||
: { resolutionProbability }),
|
||||
...(resolutions === undefined ? {} : { resolutions }),
|
||||
resolutionTime,
|
||||
closeTime: newCloseTime,
|
||||
resolutionProbability,
|
||||
resolutions,
|
||||
})
|
||||
)
|
||||
|
||||
console.log('contract ', contractId, 'resolved to:', outcome)
|
||||
|
||||
|
@ -92,13 +103,23 @@ export const resolveMarket = functions
|
|||
? getPayoutsMultiOutcome(resolutions, contract as any, openBets)
|
||||
: getPayouts(outcome, contract, openBets, resolutionProbability)
|
||||
|
||||
const loanPayouts = getLoanPayouts(openBets)
|
||||
|
||||
console.log('payouts:', payouts)
|
||||
|
||||
const groups = _.groupBy(payouts, (payout) => payout.userId)
|
||||
const groups = _.groupBy(
|
||||
[...payouts, ...loanPayouts],
|
||||
(payout) => payout.userId
|
||||
)
|
||||
const userPayouts = _.mapValues(groups, (group) =>
|
||||
_.sumBy(group, (g) => g.payout)
|
||||
)
|
||||
|
||||
const groupsWithoutLoans = _.groupBy(payouts, (payout) => payout.userId)
|
||||
const userPayoutsWithoutLoans = _.mapValues(groupsWithoutLoans, (group) =>
|
||||
_.sumBy(group, (g) => g.payout)
|
||||
)
|
||||
|
||||
const payoutPromises = Object.entries(userPayouts).map(
|
||||
([userId, payout]) => payUser(userId, payout)
|
||||
)
|
||||
|
@ -109,7 +130,7 @@ export const resolveMarket = functions
|
|||
|
||||
await sendResolutionEmails(
|
||||
openBets,
|
||||
userPayouts,
|
||||
userPayoutsWithoutLoans,
|
||||
creator,
|
||||
contract,
|
||||
outcome,
|
||||
|
@ -134,14 +155,24 @@ const sendResolutionEmails = async (
|
|||
_.uniq(openBets.map(({ userId }) => userId)),
|
||||
Object.keys(userPayouts)
|
||||
)
|
||||
const investedByUser = _.mapValues(
|
||||
_.groupBy(openBets, (bet) => bet.userId),
|
||||
(bets) => _.sumBy(bets, (bet) => bet.amount)
|
||||
)
|
||||
const emailPayouts = [
|
||||
...Object.entries(userPayouts),
|
||||
...nonWinners.map((userId) => [userId, 0] as const),
|
||||
]
|
||||
].map(([userId, payout]) => ({
|
||||
userId,
|
||||
investment: investedByUser[userId],
|
||||
payout,
|
||||
}))
|
||||
|
||||
await Promise.all(
|
||||
emailPayouts.map(([userId, payout]) =>
|
||||
emailPayouts.map(({ userId, investment, payout }) =>
|
||||
sendMarketResolutionEmail(
|
||||
userId,
|
||||
investment,
|
||||
payout,
|
||||
creator,
|
||||
contract,
|
||||
|
|
44
functions/src/scripts/remove-answer-ante.ts
Normal file
44
functions/src/scripts/remove-answer-ante.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('james')
|
||||
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { Contract } from '../../../common/contract'
|
||||
import { getValues } from '../utils'
|
||||
|
||||
async function removeAnswerAnte() {
|
||||
const firestore = admin.firestore()
|
||||
console.log('Removing isAnte from bets on answers')
|
||||
|
||||
const contracts = await getValues<Contract>(
|
||||
firestore
|
||||
.collection('contracts')
|
||||
.where('outcomeType', '==', 'FREE_RESPONSE')
|
||||
)
|
||||
|
||||
console.log('Loaded', contracts, 'contracts')
|
||||
|
||||
for (const contract of contracts) {
|
||||
const betsSnapshot = await firestore
|
||||
.collection('contracts')
|
||||
.doc(contract.id)
|
||||
.collection('bets')
|
||||
.get()
|
||||
|
||||
console.log('updating', contract.question)
|
||||
|
||||
for (const doc of betsSnapshot.docs) {
|
||||
const bet = doc.data() as Bet
|
||||
if (bet.isAnte && bet.outcome !== '0') {
|
||||
console.log('updating', bet.outcome)
|
||||
await doc.ref.update('isAnte', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
removeAnswerAnte().then(() => process.exit())
|
||||
}
|
|
@ -24,10 +24,11 @@ export const sendTemplateEmail = (
|
|||
to: string,
|
||||
subject: string,
|
||||
templateId: string,
|
||||
templateData: Record<string, string>
|
||||
templateData: Record<string, string>,
|
||||
options?: { from: string }
|
||||
) => {
|
||||
const data = {
|
||||
from: 'Manifold Markets <info@manifold.markets>',
|
||||
from: options?.from ?? 'Manifold Markets <info@manifold.markets>',
|
||||
to,
|
||||
subject,
|
||||
template: templateId,
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as functions from 'firebase-functions'
|
|||
import * as admin from 'firebase-admin'
|
||||
import Stripe from 'stripe'
|
||||
|
||||
import { payUser } from './utils'
|
||||
import { isProd, payUser } from './utils'
|
||||
|
||||
export type StripeTransaction = {
|
||||
userId: string
|
||||
|
@ -18,8 +18,7 @@ const stripe = new Stripe(functions.config().stripe.apikey, {
|
|||
})
|
||||
|
||||
// manage at https://dashboard.stripe.com/test/products?active=true
|
||||
const manticDollarStripePrice =
|
||||
admin.instanceId().app.options.projectId === 'mantic-markets'
|
||||
const manticDollarStripePrice = isProd
|
||||
? {
|
||||
500: 'price_1KFQXcGdoFKoCJW770gTNBrm',
|
||||
1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65',
|
||||
|
|
|
@ -1,30 +1,47 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
import * as _ from 'lodash'
|
||||
import { getPrivateUser } from './utils'
|
||||
import { getUser } from './utils'
|
||||
import { PrivateUser } from '../../common/user'
|
||||
|
||||
export const unsubscribe = functions
|
||||
.runWith({ minInstances: 1 })
|
||||
.https.onRequest(async (req, res) => {
|
||||
let id = req.query.id as string
|
||||
if (!id) return
|
||||
const { id, type } = req.query as { id: string; type: string }
|
||||
if (!id || !type) return
|
||||
|
||||
let privateUser = await getPrivateUser(id)
|
||||
const user = await getUser(id)
|
||||
|
||||
if (privateUser) {
|
||||
let { username } = privateUser
|
||||
if (user) {
|
||||
const { name } = user
|
||||
|
||||
const update: Partial<PrivateUser> = {
|
||||
...(type === 'market-resolve' && {
|
||||
unsubscribedFromResolutionEmails: true,
|
||||
}),
|
||||
...(type === 'market-comment' && {
|
||||
unsubscribedFromCommentEmails: true,
|
||||
}),
|
||||
...(type === 'market-answer' && {
|
||||
unsubscribedFromAnswerEmails: true,
|
||||
}),
|
||||
}
|
||||
|
||||
await firestore.collection('private-users').doc(id).update(update)
|
||||
|
||||
if (type === 'market-resolve')
|
||||
res.send(
|
||||
username +
|
||||
', you have been unsubscribed from market resolution emails on Manifold Markets.'
|
||||
`${name}, you have been unsubscribed from market resolution emails on Manifold Markets.`
|
||||
)
|
||||
else if (type === 'market-comment')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market comment emails on Manifold Markets.`
|
||||
)
|
||||
else if (type === 'market-answer')
|
||||
res.send(
|
||||
`${name}, you have been unsubscribed from market answer emails on Manifold Markets.`
|
||||
)
|
||||
else res.send(`${name}, you have been unsubscribed.`)
|
||||
} else {
|
||||
res.send('This user is not currently subscribed or does not exist.')
|
||||
}
|
||||
|
|
|
@ -52,7 +52,8 @@ const computeInvestmentValue = async (
|
|||
if (!contract || contract.isResolved) return 0
|
||||
if (bet.sale || bet.isSold) return 0
|
||||
|
||||
return calculatePayout(contract, bet, 'MKT')
|
||||
const payout = calculatePayout(contract, bet, 'MKT')
|
||||
return payout - (bet.loanAmount ?? 0)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@ import * as admin from 'firebase-admin'
|
|||
import { Contract } from '../../common/contract'
|
||||
import { PrivateUser, User } from '../../common/user'
|
||||
|
||||
export const isProd =
|
||||
admin.instanceId().app.options.projectId === 'mantic-markets'
|
||||
|
||||
export const getValue = async <T>(collection: string, doc: string) => {
|
||||
const snap = await admin.firestore().collection(collection).doc(doc).get()
|
||||
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import clsx from 'clsx'
|
||||
import _ from 'lodash'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import { formatMoney } from '../../common/util/format'
|
||||
import { AddFundsButton } from './add-funds-button'
|
||||
import { Col } from './layout/col'
|
||||
import { Row } from './layout/row'
|
||||
import { useUserContractBets } from '../hooks/use-user-bets'
|
||||
import { MAX_LOAN_PER_CONTRACT } from '../../common/bet'
|
||||
import { InfoTooltip } from './info-tooltip'
|
||||
import { Spacer } from './layout/spacer'
|
||||
|
||||
export function AmountInput(props: {
|
||||
amount: number | undefined
|
||||
onChange: (newAmount: number | undefined) => void
|
||||
error: string | undefined
|
||||
setError: (error: string | undefined) => void
|
||||
contractId: string | undefined
|
||||
minimumAmount?: number
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
|
@ -22,6 +27,7 @@ export function AmountInput(props: {
|
|||
onChange,
|
||||
error,
|
||||
setError,
|
||||
contractId,
|
||||
disabled,
|
||||
className,
|
||||
inputClassName,
|
||||
|
@ -31,10 +37,24 @@ export function AmountInput(props: {
|
|||
|
||||
const user = useUser()
|
||||
|
||||
const userBets = useUserContractBets(user?.id, contractId) ?? []
|
||||
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
|
||||
|
||||
const loanAmount = Math.min(
|
||||
amount ?? 0,
|
||||
MAX_LOAN_PER_CONTRACT - prevLoanAmount
|
||||
)
|
||||
|
||||
const onAmountChange = (str: string) => {
|
||||
if (str.includes('-')) {
|
||||
onChange(undefined)
|
||||
return
|
||||
}
|
||||
const amount = parseInt(str.replace(/[^\d]/, ''))
|
||||
|
||||
if (str && isNaN(amount)) return
|
||||
if (amount >= 10 ** 9) return
|
||||
|
||||
onChange(str ? amount : undefined)
|
||||
|
||||
|
@ -47,7 +67,8 @@ export function AmountInput(props: {
|
|||
}
|
||||
}
|
||||
|
||||
const remainingBalance = Math.max(0, (user?.balance ?? 0) - (amount ?? 0))
|
||||
const amountNetLoan = (amount ?? 0) - loanAmount
|
||||
const remainingBalance = Math.max(0, (user?.balance ?? 0) - amountNetLoan)
|
||||
|
||||
return (
|
||||
<Col className={className}>
|
||||
|
@ -68,19 +89,34 @@ export function AmountInput(props: {
|
|||
onChange={(e) => onAmountChange(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
{error && (
|
||||
<div className="mr-auto mt-4 self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
|
||||
<div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{user && (
|
||||
<Col className="mt-3 text-sm">
|
||||
<div className="mb-2 whitespace-nowrap text-gray-500">
|
||||
Remaining balance
|
||||
</div>
|
||||
<Row className="gap-4">
|
||||
<div>{formatMoney(Math.floor(remainingBalance))}</div>
|
||||
{user.balance !== 1000 && <AddFundsButton />}
|
||||
<Col className="gap-3 text-sm">
|
||||
{contractId && (
|
||||
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||
<Row className="items-center gap-2">
|
||||
Amount loaned{' '}
|
||||
<InfoTooltip
|
||||
text={`In every market, you get an interest-free loan on the first ${formatMoney(
|
||||
MAX_LOAN_PER_CONTRACT
|
||||
)}.`}
|
||||
/>
|
||||
</Row>
|
||||
<span className="text-neutral">{formatMoney(loanAmount)}</span>{' '}
|
||||
</Row>
|
||||
)}
|
||||
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||
Remaining balance{' '}
|
||||
<span className="text-neutral">
|
||||
{formatMoney(Math.floor(remainingBalance))}
|
||||
</span>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
|
|
|
@ -30,8 +30,9 @@ export function AnswerBetPanel(props: {
|
|||
answer: Answer
|
||||
contract: Contract
|
||||
closePanel: () => void
|
||||
className?: string
|
||||
}) {
|
||||
const { answer, contract, closePanel } = props
|
||||
const { answer, contract, closePanel, className } = props
|
||||
const { id: answerId } = answer
|
||||
|
||||
const user = useUser()
|
||||
|
@ -97,7 +98,7 @@ export function AnswerBetPanel(props: {
|
|||
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
||||
|
||||
return (
|
||||
<Col className="items-start px-2 pb-2 pt-4 sm:pt-0">
|
||||
<Col className={clsx('px-2 pb-2 pt-4 sm:pt-0', className)}>
|
||||
<Row className="self-stretch items-center justify-between">
|
||||
<div className="text-xl">Buy this answer</div>
|
||||
|
||||
|
@ -114,21 +115,21 @@ export function AnswerBetPanel(props: {
|
|||
setError={setError}
|
||||
disabled={isSubmitting}
|
||||
inputRef={inputRef}
|
||||
contractId={contract.id}
|
||||
/>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
|
||||
<Col className="gap-3 mt-3 w-full">
|
||||
<Row className="justify-between items-center text-sm">
|
||||
<div className="text-gray-500">Probability</div>
|
||||
<Row>
|
||||
<div>{formatPercent(initialProb)}</div>
|
||||
<div className="mx-2">→</div>
|
||||
<div>{formatPercent(resultProb)}</div>
|
||||
</Row>
|
||||
</Row>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
||||
Payout if chosen
|
||||
<Row className="justify-between items-start text-sm gap-2">
|
||||
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
|
||||
<div>Payout if chosen</div>
|
||||
<InfoTooltip
|
||||
text={`Current payout for ${formatWithCommas(
|
||||
shares
|
||||
|
@ -137,17 +138,21 @@ export function AnswerBetPanel(props: {
|
|||
)} shares`}
|
||||
/>
|
||||
</Row>
|
||||
<div>
|
||||
<Row className="flex-wrap justify-end items-end gap-2">
|
||||
<span className="whitespace-nowrap">
|
||||
{formatMoney(currentPayout)}
|
||||
<span>(+{currentReturnPercent})</span>
|
||||
</div>
|
||||
</span>
|
||||
<span>(+{currentReturnPercent})</span>
|
||||
</Row>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<Spacer h={6} />
|
||||
|
||||
{user ? (
|
||||
<button
|
||||
className={clsx(
|
||||
'btn',
|
||||
'btn self-stretch',
|
||||
betDisabled ? 'btn-disabled' : 'btn-primary',
|
||||
isSubmitting ? 'loading' : ''
|
||||
)}
|
||||
|
@ -157,7 +162,7 @@ export function AnswerBetPanel(props: {
|
|||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn mt-4 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
||||
className="btn self-stretch whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
||||
onClick={firebaseLogin}
|
||||
>
|
||||
Sign in to trade!
|
||||
|
|
|
@ -8,13 +8,13 @@ import { Col } from '../layout/col'
|
|||
import { Row } from '../layout/row'
|
||||
import { Avatar } from '../avatar'
|
||||
import { SiteLink } from '../site-link'
|
||||
import { DateTimeTooltip } from '../datetime-tooltip'
|
||||
import dayjs from 'dayjs'
|
||||
import { BuyButton } from '../yes-no-selector'
|
||||
import { formatPercent } from '../../../common/util/format'
|
||||
import { getOutcomeProbability } from '../../../common/calculate'
|
||||
import { tradingAllowed } from '../../lib/firebase/contracts'
|
||||
import { AnswerBetPanel } from './answer-bet-panel'
|
||||
import { ContractFeed } from '../contract-feed'
|
||||
import { Linkify } from '../linkify'
|
||||
|
||||
export function AnswerItem(props: {
|
||||
answer: Answer
|
||||
|
@ -35,10 +35,9 @@ export function AnswerItem(props: {
|
|||
onDeselect,
|
||||
} = props
|
||||
const { resolution, resolutions, totalShares } = contract
|
||||
const { username, avatarUrl, name, createdTime, number, text } = answer
|
||||
const { username, avatarUrl, name, number, text } = answer
|
||||
const isChosen = chosenProb !== undefined
|
||||
|
||||
const createdDate = dayjs(createdTime).format('MMM D')
|
||||
const prob = getOutcomeProbability(totalShares, answer.id)
|
||||
const roundedProb = Math.round(prob * 100)
|
||||
const probPercent = formatPercent(prob)
|
||||
|
@ -47,42 +46,50 @@ export function AnswerItem(props: {
|
|||
|
||||
const [isBetting, setIsBetting] = useState(false)
|
||||
|
||||
const canBet = !isBetting && !showChoice && tradingAllowed(contract)
|
||||
|
||||
return (
|
||||
<Col
|
||||
<div
|
||||
className={clsx(
|
||||
'p-4 sm:flex-row rounded gap-4',
|
||||
'flex flex-col gap-4 rounded p-4 sm:flex-row',
|
||||
wasResolvedTo
|
||||
? resolution === 'MKT'
|
||||
? 'bg-blue-50 mb-2'
|
||||
: 'bg-green-50 mb-8'
|
||||
? 'mb-2 bg-blue-50'
|
||||
: 'mb-8 bg-green-50'
|
||||
: chosenProb === undefined
|
||||
? 'bg-gray-50'
|
||||
: showChoice === 'radio'
|
||||
? 'bg-green-50'
|
||||
: 'bg-blue-50'
|
||||
: 'bg-blue-50',
|
||||
canBet && 'cursor-pointer hover:bg-gray-100'
|
||||
)}
|
||||
onClick={() => canBet && setIsBetting(true)}
|
||||
>
|
||||
<Col className="gap-3 flex-1">
|
||||
<div className="whitespace-pre-line break-words">{text}</div>
|
||||
<Col className="flex-1 gap-3">
|
||||
<div className="whitespace-pre-line">
|
||||
<Linkify text={text} />
|
||||
</div>
|
||||
|
||||
<Row className="text-gray-500 text-sm gap-2 items-center">
|
||||
<Row className="items-center gap-2 text-sm text-gray-500">
|
||||
<SiteLink className="relative" href={`/${username}`}>
|
||||
<Row className="items-center gap-2">
|
||||
<Avatar avatarUrl={avatarUrl} size={6} />
|
||||
<div className="truncate">{name}</div>
|
||||
</Row>
|
||||
</SiteLink>
|
||||
|
||||
<div className="">•</div>
|
||||
|
||||
<div className="whitespace-nowrap">
|
||||
<DateTimeTooltip text="" time={contract.createdTime}>
|
||||
{createdDate}
|
||||
</DateTimeTooltip>
|
||||
</div>
|
||||
<div className="">•</div>
|
||||
{/* TODO: Show total pool? */}
|
||||
<div className="text-base">#{number}</div>
|
||||
</Row>
|
||||
|
||||
{isBetting && (
|
||||
<ContractFeed
|
||||
contract={contract}
|
||||
bets={[]}
|
||||
comments={[]}
|
||||
feedType="multi"
|
||||
outcome={answer.id}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{isBetting ? (
|
||||
|
@ -90,13 +97,14 @@ export function AnswerItem(props: {
|
|||
answer={answer}
|
||||
contract={contract}
|
||||
closePanel={() => setIsBetting(false)}
|
||||
className="sm:w-72"
|
||||
/>
|
||||
) : (
|
||||
<Row className="self-end sm:self-start items-center gap-4 justify-end">
|
||||
<Row className="items-center justify-end gap-4 self-end sm:self-start">
|
||||
{!wasResolvedTo &&
|
||||
(showChoice === 'checkbox' ? (
|
||||
<input
|
||||
className="input input-bordered text-2xl justify-self-end w-24"
|
||||
className="input input-bordered w-24 justify-self-end text-2xl"
|
||||
type="number"
|
||||
placeholder={`${roundedProb}`}
|
||||
maxLength={9}
|
||||
|
@ -121,7 +129,7 @@ export function AnswerItem(props: {
|
|||
))}
|
||||
{showChoice ? (
|
||||
<div className="form-control py-1">
|
||||
<label className="cursor-pointer label gap-3">
|
||||
<label className="label cursor-pointer gap-3">
|
||||
<span className="">Choose this answer</span>
|
||||
{showChoice === 'radio' && (
|
||||
<input
|
||||
|
@ -162,7 +170,7 @@ export function AnswerItem(props: {
|
|||
<>
|
||||
{tradingAllowed(contract) && (
|
||||
<BuyButton
|
||||
className="justify-end self-end flex-initial btn-md !px-8"
|
||||
className="btn-md flex-initial justify-end self-end !px-8"
|
||||
onClick={() => {
|
||||
setIsBetting(true)
|
||||
}}
|
||||
|
@ -188,6 +196,6 @@ export function AnswerItem(props: {
|
|||
)}
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
|
|||
<div />
|
||||
<Col
|
||||
className={clsx(
|
||||
'sm:flex-row gap-4',
|
||||
'sm:flex-row sm:items-end gap-4',
|
||||
text ? 'justify-between' : 'self-end'
|
||||
)}
|
||||
>
|
||||
|
@ -101,34 +101,42 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
|
|||
setError={setAmountError}
|
||||
minimumAmount={1}
|
||||
disabled={isSubmitting}
|
||||
contractId={contract.id}
|
||||
/>
|
||||
</Col>
|
||||
<Col className="gap-2 mt-1">
|
||||
<div className="text-sm text-gray-500">Implied probability</div>
|
||||
<Col className="gap-3">
|
||||
<Row className="justify-between items-center text-sm">
|
||||
<div className="text-gray-500">Probability</div>
|
||||
<Row>
|
||||
<div>{formatPercent(0)}</div>
|
||||
<div className="mx-2">→</div>
|
||||
<div>{formatPercent(resultProb)}</div>
|
||||
</Row>
|
||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
||||
Payout if chosen
|
||||
</Row>
|
||||
|
||||
<Row className="justify-between text-sm gap-2">
|
||||
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
|
||||
<div>Payout if chosen</div>
|
||||
<InfoTooltip
|
||||
text={`Current payout for ${formatWithCommas(
|
||||
shares
|
||||
)} / ${formatWithCommas(shares)} shares`}
|
||||
/>
|
||||
</Row>
|
||||
<div>
|
||||
<Row className="flex-wrap justify-end items-end gap-2">
|
||||
<span className="whitespace-nowrap">
|
||||
{formatMoney(currentPayout)}
|
||||
<span>(+{currentReturnPercent})</span>
|
||||
</div>
|
||||
</span>
|
||||
<span>(+{currentReturnPercent})</span>
|
||||
</Row>
|
||||
</Row>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
{user ? (
|
||||
<button
|
||||
className={clsx(
|
||||
'btn self-end mt-2',
|
||||
'btn mt-2',
|
||||
canSubmit ? 'btn-outline' : 'btn-disabled',
|
||||
isSubmitting && 'loading'
|
||||
)}
|
||||
|
|
|
@ -144,7 +144,6 @@ export function BetPanel(props: {
|
|||
text={panelTitle}
|
||||
/>
|
||||
|
||||
{/* <div className="mt-2 mb-1 text-sm text-gray-500">Outcome</div> */}
|
||||
<YesNoSelector
|
||||
className="mb-4"
|
||||
selected={betChoice}
|
||||
|
@ -160,45 +159,49 @@ export function BetPanel(props: {
|
|||
setError={setError}
|
||||
disabled={isSubmitting}
|
||||
inputRef={inputRef}
|
||||
contractId={contract.id}
|
||||
/>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
|
||||
<Col className="gap-3 mt-3 w-full">
|
||||
<Row className="justify-between items-center text-sm">
|
||||
<div className="text-gray-500">Probability</div>
|
||||
<Row>
|
||||
<div>{formatPercent(initialProb)}</div>
|
||||
<div className="mx-2">→</div>
|
||||
<div>{formatPercent(resultProb)}</div>
|
||||
</Row>
|
||||
</Row>
|
||||
|
||||
{betChoice && (
|
||||
<>
|
||||
<Spacer h={4} />
|
||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
||||
Payout if <OutcomeLabel outcome={betChoice} />
|
||||
<Row className="justify-between items-start text-sm gap-2">
|
||||
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
|
||||
<div>
|
||||
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||
</div>
|
||||
<InfoTooltip
|
||||
text={`Current payout for ${formatWithCommas(
|
||||
shares
|
||||
)} / ${formatWithCommas(
|
||||
shares +
|
||||
totalShares[betChoice] -
|
||||
(phantomShares ? phantomShares[betChoice] : 0)
|
||||
totalShares[betChoice ?? 'YES'] -
|
||||
(phantomShares ? phantomShares[betChoice ?? 'YES'] : 0)
|
||||
)} ${betChoice} shares`}
|
||||
/>
|
||||
</Row>
|
||||
<div>
|
||||
<Row className="flex-wrap justify-end items-end gap-2">
|
||||
<span className="whitespace-nowrap">
|
||||
{formatMoney(currentPayout)}
|
||||
<span>(+{currentReturnPercent})</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<span>(+{currentReturnPercent})</span>
|
||||
</Row>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<Spacer h={6} />
|
||||
<Spacer h={8} />
|
||||
|
||||
{user && (
|
||||
<button
|
||||
className={clsx(
|
||||
'btn',
|
||||
'btn flex-1',
|
||||
betDisabled
|
||||
? 'btn-disabled'
|
||||
: betChoice === 'YES'
|
||||
|
@ -213,7 +216,7 @@ export function BetPanel(props: {
|
|||
)}
|
||||
{user === null && (
|
||||
<button
|
||||
className="btn mt-4 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
||||
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
||||
onClick={firebaseLogin}
|
||||
>
|
||||
Sign in to trade!
|
||||
|
|
|
@ -37,7 +37,7 @@ import { filterDefined } from '../../common/util/array'
|
|||
import { LoadingIndicator } from './loading-indicator'
|
||||
import { SiteLink } from './site-link'
|
||||
|
||||
type BetSort = 'newest' | 'profit' | 'resolved' | 'value'
|
||||
type BetSort = 'newest' | 'profit' | 'settled' | 'value'
|
||||
|
||||
export function BetsList(props: { user: User }) {
|
||||
const { user } = props
|
||||
|
@ -82,7 +82,8 @@ export function BetsList(props: { user: User }) {
|
|||
if (bet.isSold || bet.sale) return 0
|
||||
|
||||
const contract = contracts.find((c) => c.id === contractId)
|
||||
return contract ? calculatePayout(contract, bet, 'MKT') : 0
|
||||
const payout = contract ? calculatePayout(contract, bet, 'MKT') : 0
|
||||
return payout - (bet.loanAmount ?? 0)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -106,23 +107,20 @@ export function BetsList(props: { user: User }) {
|
|||
contracts,
|
||||
(c) => -1 * Math.max(...contractBets[c.id].map((bet) => bet.createdTime))
|
||||
)
|
||||
else if (sort === 'resolved')
|
||||
else if (sort === 'settled')
|
||||
sortedContracts = _.sortBy(contracts, (c) => -1 * (c.resolutionTime ?? 0))
|
||||
|
||||
const [resolved, unresolved] = _.partition(
|
||||
const [settled, unsettled] = _.partition(
|
||||
sortedContracts,
|
||||
(c) => c.isResolved
|
||||
(c) => c.isResolved || contractsInvestment[c.id] === 0
|
||||
)
|
||||
|
||||
const displayedContracts = sort === 'resolved' ? resolved : unresolved
|
||||
const displayedContracts = sort === 'settled' ? settled : unsettled
|
||||
|
||||
const currentInvestment = _.sumBy(
|
||||
unresolved,
|
||||
(c) => contractsInvestment[c.id]
|
||||
)
|
||||
const currentInvestment = _.sumBy(unsettled, (c) => contractsInvestment[c.id])
|
||||
|
||||
const currentBetsValue = _.sumBy(
|
||||
unresolved,
|
||||
unsettled,
|
||||
(c) => contractsCurrentValue[c.id]
|
||||
)
|
||||
|
||||
|
@ -140,7 +138,7 @@ export function BetsList(props: { user: User }) {
|
|||
<Col>
|
||||
<div className="text-sm text-gray-500">Invested</div>
|
||||
<div className="text-lg">
|
||||
{formatMoney(currentBetsValue)}{' '}
|
||||
{formatMoney(currentInvestment)}{' '}
|
||||
<ProfitBadge profitPercent={investedProfit} />
|
||||
</div>
|
||||
</Col>
|
||||
|
@ -160,8 +158,8 @@ export function BetsList(props: { user: User }) {
|
|||
>
|
||||
<option value="value">By value</option>
|
||||
<option value="profit">By profit</option>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
<option value="newest">Most recent</option>
|
||||
<option value="settled">Resolved</option>
|
||||
</select>
|
||||
</Col>
|
||||
|
||||
|
@ -228,7 +226,7 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
|
|||
/>
|
||||
</Row>
|
||||
|
||||
<Row className="items-center gap-2 text-sm text-gray-500 flex-1">
|
||||
<Row className="flex-1 items-center gap-2 text-sm text-gray-500">
|
||||
{isBinary && (
|
||||
<>
|
||||
{resolution ? (
|
||||
|
@ -361,6 +359,8 @@ export function MyBetsSummary(props: {
|
|||
{formatMoney(expectation)}
|
||||
</div>
|
||||
</Col> */}
|
||||
{isBinary && (
|
||||
<>
|
||||
<Col>
|
||||
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||
Payout if <YesLabel />
|
||||
|
@ -377,6 +377,8 @@ export function MyBetsSummary(props: {
|
|||
{formatMoney(noWinnings)}
|
||||
</div>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
<Col>
|
||||
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||
{isBinary ? (
|
||||
|
@ -421,9 +423,10 @@ export function ContractBetsTable(props: {
|
|||
<thead>
|
||||
<tr className="p-2">
|
||||
<th></th>
|
||||
<th>{isResolved ? <>Payout</> : <>Sale price</>}</th>
|
||||
<th>Outcome</th>
|
||||
<th>Amount</th>
|
||||
<th>{isResolved ? <>Payout</> : <>Sale price</>}</th>
|
||||
{!isResolved && <th>Payout if chosen</th>}
|
||||
<th>Probability</th>
|
||||
<th>Shares</th>
|
||||
<th>Date</th>
|
||||
|
@ -455,6 +458,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
|||
shares,
|
||||
isSold,
|
||||
isAnte,
|
||||
loanAmount,
|
||||
} = bet
|
||||
|
||||
const { isResolved, closeTime } = contract
|
||||
|
@ -462,7 +466,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
|||
|
||||
const saleAmount = saleBet?.sale?.amount
|
||||
|
||||
const saleDisplay = bet.isAnte ? (
|
||||
const saleDisplay = isAnte ? (
|
||||
'ANTE'
|
||||
) : saleAmount !== undefined ? (
|
||||
<>{formatMoney(saleAmount)} (sold)</>
|
||||
|
@ -474,6 +478,11 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
|||
)
|
||||
)
|
||||
|
||||
const payoutIfChosenDisplay =
|
||||
bet.outcome === '0' && bet.isAnte
|
||||
? 'N/A'
|
||||
: formatMoney(calculatePayout(contract, bet, bet.outcome))
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className="text-neutral">
|
||||
|
@ -481,11 +490,15 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
|||
<SellButton contract={contract} bet={bet} />
|
||||
)}
|
||||
</td>
|
||||
<td>{saleDisplay}</td>
|
||||
<td>
|
||||
<OutcomeLabel outcome={outcome} />
|
||||
</td>
|
||||
<td>{formatMoney(amount)}</td>
|
||||
<td>
|
||||
{formatMoney(amount)}
|
||||
{loanAmount ? ` (${formatMoney(loanAmount ?? 0)} loan)` : ''}
|
||||
</td>
|
||||
<td>{saleDisplay}</td>
|
||||
{!isResolved && <td>{payoutIfChosenDisplay}</td>}
|
||||
<td>
|
||||
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
||||
</td>
|
||||
|
@ -502,17 +515,19 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
|||
}, [])
|
||||
|
||||
const { contract, bet } = props
|
||||
const { outcome, shares, loanAmount } = bet
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const initialProb = getOutcomeProbability(
|
||||
contract.totalShares,
|
||||
bet.outcome === 'NO' ? 'YES' : bet.outcome
|
||||
outcome === 'NO' ? 'YES' : outcome
|
||||
)
|
||||
|
||||
const outcomeProb = getProbabilityAfterSale(
|
||||
contract.totalShares,
|
||||
bet.outcome,
|
||||
bet.shares
|
||||
outcome,
|
||||
shares
|
||||
)
|
||||
|
||||
const saleAmount = calculateSaleAmount(contract, bet)
|
||||
|
@ -524,7 +539,7 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
|||
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
||||
label: 'Sell',
|
||||
}}
|
||||
submitBtn={{ className: 'btn-primary' }}
|
||||
submitBtn={{ className: 'btn-primary', label: 'Sell' }}
|
||||
onSubmit={async () => {
|
||||
setIsSubmitting(true)
|
||||
await sellBet({ contractId: contract.id, betId: bet.id })
|
||||
|
@ -532,15 +547,18 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
|||
}}
|
||||
>
|
||||
<div className="mb-4 text-2xl">
|
||||
Sell <OutcomeLabel outcome={bet.outcome} />
|
||||
Sell {formatWithCommas(shares)} shares of{' '}
|
||||
<OutcomeLabel outcome={outcome} /> for {formatMoney(saleAmount)}?
|
||||
</div>
|
||||
<div>
|
||||
Do you want to sell {formatWithCommas(bet.shares)} shares of{' '}
|
||||
<OutcomeLabel outcome={bet.outcome} /> for {formatMoney(saleAmount)}?
|
||||
{!!loanAmount && (
|
||||
<div className="mt-2">
|
||||
You will also pay back {formatMoney(loanAmount)} of your loan, for a
|
||||
net of {formatMoney(saleAmount - loanAmount)}.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 mb-1 text-sm text-gray-500">
|
||||
Implied probability: {formatPercent(initialProb)} →{' '}
|
||||
<div className="mt-2 mb-1 text-sm">
|
||||
Market probability: {formatPercent(initialProb)} →{' '}
|
||||
{formatPercent(outcomeProb)}
|
||||
</div>
|
||||
</ConfirmationButton>
|
||||
|
|
|
@ -19,6 +19,7 @@ import { fromNow } from '../lib/util/time'
|
|||
import { Avatar } from './avatar'
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { useState } from 'react'
|
||||
import { TweetButton } from './tweet-button'
|
||||
|
||||
export function ContractCard(props: {
|
||||
contract: Contract
|
||||
|
@ -149,7 +150,8 @@ function AbbrContractDetails(props: {
|
|||
) : showCloseTime ? (
|
||||
<Row className="gap-1">
|
||||
<ClockIcon className="h-5 w-5" />
|
||||
Closes {fromNow(closeTime || 0)}
|
||||
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
|
||||
{fromNow(closeTime || 0)}
|
||||
</Row>
|
||||
) : (
|
||||
<Row className="gap-1">
|
||||
|
@ -170,6 +172,8 @@ export function ContractDetails(props: {
|
|||
const { closeTime, creatorName, creatorUsername } = contract
|
||||
const { truePool, createdDate, resolvedDate } = contractMetrics(contract)
|
||||
|
||||
const tweetText = getTweetText(contract, !!isCreator)
|
||||
|
||||
return (
|
||||
<Col className="gap-2 text-sm text-gray-500 sm:flex-row sm:flex-wrap">
|
||||
<Row className="flex-wrap items-center gap-x-4 gap-y-2">
|
||||
|
@ -222,6 +226,8 @@ export function ContractDetails(props: {
|
|||
|
||||
<div className="whitespace-nowrap">{formatMoney(truePool)} pool</div>
|
||||
</Row>
|
||||
|
||||
<TweetButton className={'self-end'} tweetText={tweetText} />
|
||||
</Row>
|
||||
</Col>
|
||||
)
|
||||
|
@ -307,9 +313,28 @@ function EditableCloseDate(props: {
|
|||
className="btn btn-xs btn-ghost"
|
||||
onClick={() => setIsEditingCloseTime(true)}
|
||||
>
|
||||
<PencilIcon className="inline h-4 w-4 mr-2" /> Edit
|
||||
<PencilIcon className="mr-2 inline h-4 w-4" /> Edit
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const getTweetText = (contract: Contract, isCreator: boolean) => {
|
||||
const { question, creatorName, resolution, outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
const tweetQuestion = isCreator
|
||||
? question
|
||||
: `${question} Asked by ${creatorName}.`
|
||||
const tweetDescription = resolution
|
||||
? `Resolved ${resolution}!`
|
||||
: isBinary
|
||||
? `Currently ${getBinaryProbPercent(
|
||||
contract
|
||||
)} chance, place your bets here:`
|
||||
: `Submit your own answer:`
|
||||
const url = `https://manifold.markets${contractPath(contract)}`
|
||||
|
||||
return `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ import { useAdmin } from '../hooks/use-admin'
|
|||
function FeedComment(props: {
|
||||
activityItem: any
|
||||
moreHref: string
|
||||
feedType: 'activity' | 'market'
|
||||
feedType: FeedType
|
||||
}) {
|
||||
const { activityItem, moreHref, feedType } = props
|
||||
const { person, text, amount, outcome, createdTime } = activityItem
|
||||
|
@ -65,7 +65,8 @@ function FeedComment(props: {
|
|||
username={person.username}
|
||||
name={person.name}
|
||||
/>{' '}
|
||||
{bought} {money} of <OutcomeLabel outcome={outcome} />{' '}
|
||||
{bought} {money}
|
||||
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />
|
||||
<Timestamp time={createdTime} />
|
||||
</p>
|
||||
</div>
|
||||
|
@ -90,8 +91,8 @@ function Timestamp(props: { time: number }) {
|
|||
)
|
||||
}
|
||||
|
||||
function FeedBet(props: { activityItem: any }) {
|
||||
const { activityItem } = props
|
||||
function FeedBet(props: { activityItem: any; feedType: FeedType }) {
|
||||
const { activityItem, feedType } = props
|
||||
const { id, contractId, amount, outcome, createdTime } = activityItem
|
||||
const user = useUser()
|
||||
const isSelf = user?.id == activityItem.userId
|
||||
|
@ -122,8 +123,9 @@ function FeedBet(props: { activityItem: any }) {
|
|||
</div>
|
||||
<div className="min-w-0 flex-1 py-1.5">
|
||||
<div className="text-sm text-gray-500">
|
||||
<span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money} of{' '}
|
||||
<OutcomeLabel outcome={outcome} /> <Timestamp time={createdTime} />
|
||||
<span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money}
|
||||
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />
|
||||
<Timestamp time={createdTime} />
|
||||
{canComment && (
|
||||
// Allow user to comment in an textarea if they are the creator
|
||||
<div className="mt-2">
|
||||
|
@ -134,7 +136,7 @@ function FeedBet(props: { activityItem: any }) {
|
|||
placeholder="Add a comment..."
|
||||
rows={3}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && e.ctrlKey) {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
submitComment()
|
||||
}
|
||||
}}
|
||||
|
@ -179,7 +181,7 @@ function EditContract(props: {
|
|||
e.target.setSelectionRange(text.length, text.length)
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && e.ctrlKey) {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
onSave(text)
|
||||
}
|
||||
}}
|
||||
|
@ -305,15 +307,13 @@ function FeedQuestion(props: { contract: Contract }) {
|
|||
const { truePool } = contractMetrics(contract)
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
// Currently hidden on mobile; ideally we'd fit this in somewhere.
|
||||
const closeMessage =
|
||||
contract.isResolved || !contract.closeTime ? null : (
|
||||
<span className="float-right hidden text-gray-400 sm:inline">
|
||||
{formatMoney(truePool)} pool
|
||||
<>
|
||||
<span className="mx-2">•</span>
|
||||
{contract.closeTime > Date.now() ? 'Closes' : 'Closed'}
|
||||
<Timestamp time={contract.closeTime || 0} />
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -330,7 +330,11 @@ function FeedQuestion(props: { contract: Contract }) {
|
|||
username={creatorUsername}
|
||||
/>{' '}
|
||||
asked
|
||||
{/* Currently hidden on mobile; ideally we'd fit this in somewhere. */}
|
||||
<span className="float-right hidden text-gray-400 sm:inline">
|
||||
{formatMoney(truePool)} pool
|
||||
{closeMessage}
|
||||
</span>
|
||||
</div>
|
||||
<Col className="mb-4 items-start justify-between gap-2 sm:flex-row sm:gap-4">
|
||||
<SiteLink
|
||||
|
@ -380,6 +384,29 @@ function FeedDescription(props: { contract: Contract }) {
|
|||
)
|
||||
}
|
||||
|
||||
function FeedAnswer(props: { contract: Contract; outcome: string }) {
|
||||
const { contract, outcome } = props
|
||||
const answer = contract?.answers?.[Number(outcome) - 1]
|
||||
if (!answer) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Avatar username={answer.username} avatarUrl={answer.avatarUrl} />
|
||||
<div className="min-w-0 flex-1 py-1.5">
|
||||
<div className="text-sm text-gray-500">
|
||||
<UserLink
|
||||
className="text-gray-900"
|
||||
name={answer.name}
|
||||
username={answer.username}
|
||||
/>{' '}
|
||||
submitted answer <OutcomeLabel outcome={outcome} />{' '}
|
||||
<Timestamp time={contract.createdTime} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function OutcomeIcon(props: { outcome?: string }) {
|
||||
const { outcome } = props
|
||||
switch (outcome) {
|
||||
|
@ -538,8 +565,12 @@ function groupBets(
|
|||
return items as ActivityItem[]
|
||||
}
|
||||
|
||||
function BetGroupSpan(props: { bets: Bet[]; outcome: string }) {
|
||||
const { bets, outcome } = props
|
||||
function BetGroupSpan(props: {
|
||||
bets: Bet[]
|
||||
outcome: string
|
||||
feedType: FeedType
|
||||
}) {
|
||||
const { bets, outcome, feedType } = props
|
||||
|
||||
const numberTraders = _.uniqBy(bets, (b) => b.userId).length
|
||||
|
||||
|
@ -554,14 +585,14 @@ function BetGroupSpan(props: { bets: Bet[]; outcome: string }) {
|
|||
{buyTotal > 0 && <>bought {formatMoney(buyTotal)} </>}
|
||||
{sellTotal > 0 && <>sold {formatMoney(sellTotal)} </>}
|
||||
</JoinSpans>
|
||||
of <OutcomeLabel outcome={outcome} />
|
||||
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />{' '}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Make this expandable to show all grouped bets?
|
||||
function FeedBetGroup(props: { activityItem: any }) {
|
||||
const { activityItem } = props
|
||||
function FeedBetGroup(props: { activityItem: any; feedType: FeedType }) {
|
||||
const { activityItem, feedType } = props
|
||||
const bets: Bet[] = activityItem.bets
|
||||
|
||||
const betGroups = _.groupBy(bets, (bet) => bet.outcome)
|
||||
|
@ -583,7 +614,11 @@ function FeedBetGroup(props: { activityItem: any }) {
|
|||
<div className="text-sm text-gray-500">
|
||||
{outcomes.map((outcome, index) => (
|
||||
<Fragment key={outcome}>
|
||||
<BetGroupSpan outcome={outcome} bets={betGroups[outcome]} />
|
||||
<BetGroupSpan
|
||||
outcome={outcome}
|
||||
bets={betGroups[outcome]}
|
||||
feedType={feedType}
|
||||
/>
|
||||
{index !== outcomes.length - 1 && <br />}
|
||||
</Fragment>
|
||||
))}
|
||||
|
@ -621,6 +656,18 @@ function FeedExpand(props: { setExpanded: (expanded: boolean) => void }) {
|
|||
)
|
||||
}
|
||||
|
||||
// On 'multi' feeds, the outcome is redundant, so we hide it
|
||||
function MaybeOutcomeLabel(props: { outcome: string; feedType: FeedType }) {
|
||||
const { outcome, feedType } = props
|
||||
return feedType === 'multi' ? null : (
|
||||
<span>
|
||||
{' '}
|
||||
of <OutcomeLabel outcome={outcome} />
|
||||
{/* TODO: Link to the correct e.g. #23 */}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// Missing feed items:
|
||||
// - Bet sold?
|
||||
type ActivityItem = {
|
||||
|
@ -635,15 +682,23 @@ type ActivityItem = {
|
|||
| 'expand'
|
||||
}
|
||||
|
||||
type FeedType =
|
||||
// Main homepage/fold feed,
|
||||
| 'activity'
|
||||
// Comments feed on a market
|
||||
| 'market'
|
||||
// Grouped for a multi-category outcome
|
||||
| 'multi'
|
||||
|
||||
export function ContractFeed(props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
// Feed types: 'activity' = Activity feed, 'market' = Comments feed on a market
|
||||
feedType: 'activity' | 'market'
|
||||
feedType: FeedType
|
||||
outcome?: string // Which multi-category outcome to filter
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, feedType, betRowClassName } = props
|
||||
const { contract, feedType, outcome, betRowClassName } = props
|
||||
const { id, outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
|
@ -655,6 +710,10 @@ export function ContractFeed(props: {
|
|||
? bets.filter((bet) => !bet.isAnte)
|
||||
: bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
|
||||
|
||||
if (feedType === 'multi') {
|
||||
bets = bets.filter((bet) => bet.outcome === outcome)
|
||||
}
|
||||
|
||||
const comments = useComments(id) ?? props.comments
|
||||
|
||||
const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
|
||||
|
@ -669,6 +728,10 @@ export function ContractFeed(props: {
|
|||
if (contract.resolution) {
|
||||
allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
||||
}
|
||||
if (feedType === 'multi') {
|
||||
// Hack to add some more padding above the 'multi' feedType, by adding a null item
|
||||
allItems.unshift({ type: '', id: -1 })
|
||||
}
|
||||
|
||||
// If there are more than 5 items, only show the first, an expand item, and last 3
|
||||
let items = allItems
|
||||
|
@ -682,10 +745,9 @@ export function ContractFeed(props: {
|
|||
|
||||
return (
|
||||
<div className="flow-root pr-2 md:pr-0">
|
||||
<ul role="list" className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
|
||||
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
|
||||
{items.map((activityItem, activityItemIdx) => (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-8">
|
||||
<div key={activityItem.id} className="relative pb-8">
|
||||
{activityItemIdx !== items.length - 1 ? (
|
||||
<span
|
||||
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
||||
|
@ -694,11 +756,13 @@ export function ContractFeed(props: {
|
|||
) : null}
|
||||
<div className="relative flex items-start space-x-3">
|
||||
{activityItem.type === 'start' ? (
|
||||
feedType == 'activity' ? (
|
||||
feedType === 'activity' ? (
|
||||
<FeedQuestion contract={contract} />
|
||||
) : (
|
||||
) : feedType === 'market' ? (
|
||||
<FeedDescription contract={contract} />
|
||||
)
|
||||
) : feedType === 'multi' ? (
|
||||
<FeedAnswer contract={contract} outcome={outcome || '0'} />
|
||||
) : null
|
||||
) : activityItem.type === 'comment' ? (
|
||||
<FeedComment
|
||||
activityItem={activityItem}
|
||||
|
@ -706,9 +770,9 @@ export function ContractFeed(props: {
|
|||
feedType={feedType}
|
||||
/>
|
||||
) : activityItem.type === 'bet' ? (
|
||||
<FeedBet activityItem={activityItem} />
|
||||
<FeedBet activityItem={activityItem} feedType={feedType} />
|
||||
) : activityItem.type === 'betgroup' ? (
|
||||
<FeedBetGroup activityItem={activityItem} />
|
||||
<FeedBetGroup activityItem={activityItem} feedType={feedType} />
|
||||
) : activityItem.type === 'close' ? (
|
||||
<FeedClose contract={contract} />
|
||||
) : activityItem.type === 'resolve' ? (
|
||||
|
@ -718,9 +782,32 @@ export function ContractFeed(props: {
|
|||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{isBinary && tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContractSummaryFeed(props: {
|
||||
contract: Contract
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, betRowClassName } = props
|
||||
const { outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
return (
|
||||
<div className="flow-root pr-2 md:pr-0">
|
||||
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
|
||||
<div className="relative pb-8">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<FeedQuestion contract={contract} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isBinary && tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
||||
)}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import {
|
||||
Contract,
|
||||
deleteContract,
|
||||
contractPath,
|
||||
tradingAllowed,
|
||||
getBinaryProbPercent,
|
||||
} from '../lib/firebase/contracts'
|
||||
import { Col } from './layout/col'
|
||||
import { Spacer } from './layout/spacer'
|
||||
|
@ -15,7 +13,6 @@ import { Linkify } from './linkify'
|
|||
import clsx from 'clsx'
|
||||
import { ContractDetails, ResolutionOrChance } from './contract-card'
|
||||
import { ContractFeed } from './contract-feed'
|
||||
import { TweetButton } from './tweet-button'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { RevealableTagsInput, TagsInput } from './tags-input'
|
||||
|
@ -38,8 +35,6 @@ export const ContractOverview = (props: {
|
|||
const isCreator = user?.id === creatorId
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
const tweetText = getTweetText(contract, isCreator)
|
||||
|
||||
return (
|
||||
<Col className={clsx('mb-6', className)}>
|
||||
<Row className="justify-between gap-4 px-2">
|
||||
|
@ -92,11 +87,9 @@ export const ContractOverview = (props: {
|
|||
) : (
|
||||
<FoldTagList folds={folds} />
|
||||
)}
|
||||
<TweetButton tweetText={tweetText} />
|
||||
</Row>
|
||||
|
||||
<Col className="mt-6 gap-4 sm:hidden">
|
||||
<TweetButton className="self-end" tweetText={tweetText} />
|
||||
{folds.length === 0 ? (
|
||||
<TagsInput contract={contract} />
|
||||
) : (
|
||||
|
@ -136,22 +129,3 @@ export const ContractOverview = (props: {
|
|||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
const getTweetText = (contract: Contract, isCreator: boolean) => {
|
||||
const { question, creatorName, resolution, outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
const tweetQuestion = isCreator
|
||||
? question
|
||||
: `${question} Asked by ${creatorName}.`
|
||||
const tweetDescription = resolution
|
||||
? `Resolved ${resolution}!`
|
||||
: isBinary
|
||||
? `Currently ${getBinaryProbPercent(
|
||||
contract
|
||||
)} chance, place your bets here:`
|
||||
: `Submit your own answer:`
|
||||
const url = `https://manifold.markets${contractPath(contract)}`
|
||||
|
||||
return `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ export function ContractProbGraph(props: { contract: Contract; bets: Bet[] }) {
|
|||
format: (time) => formatTime(+time, lessThanAWeek),
|
||||
}}
|
||||
colors={{ datum: 'color' }}
|
||||
pointSize={10}
|
||||
pointSize={bets.length > 100 ? 0 : 10}
|
||||
pointBorderWidth={1}
|
||||
pointBorderColor="#fff"
|
||||
enableSlices="x"
|
||||
|
|
|
@ -205,16 +205,19 @@ export function SearchableGrid(props: {
|
|||
}) {
|
||||
const { contracts, query, setQuery, sort, setSort, byOneCreator } = props
|
||||
|
||||
const queryWords = query.toLowerCase().split(' ')
|
||||
function check(corpus: String) {
|
||||
return corpus.toLowerCase().includes(query.toLowerCase())
|
||||
return queryWords.every((word) => corpus.toLowerCase().includes(word))
|
||||
}
|
||||
|
||||
let matches = contracts.filter(
|
||||
(c) =>
|
||||
check(c.question) ||
|
||||
check(c.description) ||
|
||||
check(c.creatorName) ||
|
||||
check(c.creatorUsername) ||
|
||||
check(c.lowercaseTags.map((tag) => `#${tag}`).join(' '))
|
||||
check(c.lowercaseTags.map((tag) => `#${tag}`).join(' ')) ||
|
||||
check((c.answers ?? []).map((answer) => answer.text).join(' '))
|
||||
)
|
||||
|
||||
if (sort === 'newest' || sort === 'all') {
|
||||
|
@ -226,11 +229,13 @@ export function SearchableGrid(props: {
|
|||
)
|
||||
} else if (sort === 'oldest') {
|
||||
matches.sort((a, b) => a.createdTime - b.createdTime)
|
||||
} else if (sort === 'close-date') {
|
||||
} else if (sort === 'close-date' || sort === 'closed') {
|
||||
matches = _.sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours)
|
||||
matches = _.sortBy(matches, (contract) => contract.closeTime)
|
||||
// Hide contracts that have already closed
|
||||
matches = matches.filter(({ closeTime }) => (closeTime || 0) > Date.now())
|
||||
const hideClosed = sort === 'closed'
|
||||
matches = matches.filter(
|
||||
({ closeTime }) => closeTime && closeTime > Date.now() !== hideClosed
|
||||
)
|
||||
} else if (sort === 'most-traded') {
|
||||
matches.sort(
|
||||
(a, b) => contractMetrics(b).truePool - contractMetrics(a).truePool
|
||||
|
@ -269,6 +274,7 @@ export function SearchableGrid(props: {
|
|||
<option value="most-traded">Most traded</option>
|
||||
<option value="24-hour-vol">24h volume</option>
|
||||
<option value="close-date">Closing soon</option>
|
||||
<option value="closed">Closed</option>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="oldest">Oldest</option>
|
||||
|
||||
|
@ -286,7 +292,7 @@ export function SearchableGrid(props: {
|
|||
) : (
|
||||
<ContractsGrid
|
||||
contracts={matches}
|
||||
showCloseTime={sort == 'close-date'}
|
||||
showCloseTime={['close-date', 'closed'].includes(sort)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -101,7 +101,7 @@ export const FastFoldFollowing = (props: {
|
|||
]}
|
||||
/>
|
||||
|
||||
<Spacer h={10} />
|
||||
<Spacer h={5} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -34,10 +34,6 @@ function getNavigationOptions(
|
|||
name: 'Home',
|
||||
href: user ? '/home' : '/',
|
||||
},
|
||||
{
|
||||
name: `Your profile`,
|
||||
href: `/${user?.username}`,
|
||||
},
|
||||
...(mobile
|
||||
? [
|
||||
{
|
||||
|
@ -50,10 +46,18 @@ function getNavigationOptions(
|
|||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: `Your profile`,
|
||||
href: `/${user?.username}`,
|
||||
},
|
||||
{
|
||||
name: 'Your trades',
|
||||
href: '/trades',
|
||||
},
|
||||
{
|
||||
name: 'Add funds',
|
||||
href: '/add-funds',
|
||||
},
|
||||
{
|
||||
name: 'Leaderboards',
|
||||
href: '/leaderboards',
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import { useUser } from './use-user'
|
||||
import { isAdmin } from '../../common/access'
|
||||
import { usePrivateUser, useUser } from './use-user'
|
||||
|
||||
export const useAdmin = () => {
|
||||
const user = useUser()
|
||||
const adminIds = [
|
||||
'igi2zGXsfxYPgB0DJTXVJVmwCOr2', // Austin
|
||||
'5LZ4LgYuySdL1huCWe7bti02ghx2', // James
|
||||
'tlmGNz9kjXc2EteizMORes4qvWl2', // Stephen
|
||||
'IPTOzEqrpkWmEzh6hwvAyY9PqFb2', // Manifold
|
||||
]
|
||||
return adminIds.includes(user?.id || '')
|
||||
const privateUser = usePrivateUser(user?.id)
|
||||
return isAdmin(privateUser?.email || '')
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
|||
import { Contract } from '../../common/contract'
|
||||
import {
|
||||
Bet,
|
||||
getRecentBets,
|
||||
listenForBets,
|
||||
listenForRecentBets,
|
||||
withoutAnteBets,
|
||||
|
@ -36,3 +37,11 @@ export const useRecentBets = () => {
|
|||
useEffect(() => listenForRecentBets(setRecentBets), [])
|
||||
return recentBets
|
||||
}
|
||||
|
||||
export const useGetRecentBets = () => {
|
||||
const [recentBets, setRecentBets] = useState<Bet[] | undefined>()
|
||||
useEffect(() => {
|
||||
getRecentBets().then(setRecentBets)
|
||||
}, [])
|
||||
return recentBets
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@ import _ from 'lodash'
|
|||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Contract,
|
||||
listenForActiveContracts,
|
||||
listenForContracts,
|
||||
listenForHotContracts,
|
||||
listenForInactiveContracts,
|
||||
} from '../lib/firebase/contracts'
|
||||
import { listenForTaggedContracts } from '../lib/firebase/folds'
|
||||
|
||||
|
@ -17,6 +19,26 @@ export const useContracts = () => {
|
|||
return contracts
|
||||
}
|
||||
|
||||
export const useActiveContracts = () => {
|
||||
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
return listenForActiveContracts(setContracts)
|
||||
}, [])
|
||||
|
||||
return contracts
|
||||
}
|
||||
|
||||
export const useInactiveContracts = () => {
|
||||
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
return listenForInactiveContracts(setContracts)
|
||||
}, [])
|
||||
|
||||
return contracts
|
||||
}
|
||||
|
||||
export const useUpdatedContracts = (initialContracts: Contract[]) => {
|
||||
const [contracts, setContracts] = useState(initialContracts)
|
||||
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
import _ from 'lodash'
|
||||
import { useRef } from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
|
||||
import { Fold } from '../../common/fold'
|
||||
import { User } from '../../common/user'
|
||||
import { filterDefined } from '../../common/util/array'
|
||||
import { Bet, getRecentBets } from '../lib/firebase/bets'
|
||||
import { Comment, getRecentComments } from '../lib/firebase/comments'
|
||||
import { Contract, listAllContracts } from '../lib/firebase/contracts'
|
||||
import { Contract, getActiveContracts } from '../lib/firebase/contracts'
|
||||
import { listAllFolds } from '../lib/firebase/folds'
|
||||
import { findActiveContracts } from '../pages/activity'
|
||||
import { useRecentBets } from './use-bets'
|
||||
import { useRecentComments } from './use-comments'
|
||||
import { useContracts } from './use-contracts'
|
||||
import { useInactiveContracts } from './use-contracts'
|
||||
import { useFollowedFolds } from './use-fold'
|
||||
import { useUserBetContracts } from './use-user-bets'
|
||||
|
||||
// used in static props
|
||||
export const getAllContractInfo = async () => {
|
||||
let [contracts, folds] = await Promise.all([
|
||||
listAllContracts().catch((_) => []),
|
||||
getActiveContracts().catch((_) => []),
|
||||
listAllFolds().catch(() => []),
|
||||
])
|
||||
|
||||
|
@ -30,25 +28,15 @@ export const getAllContractInfo = async () => {
|
|||
return { contracts, recentBets, recentComments, folds }
|
||||
}
|
||||
|
||||
export const useActiveContracts = (
|
||||
props: {
|
||||
export const useFilterYourContracts = (
|
||||
user: User | undefined | null,
|
||||
folds: Fold[],
|
||||
contracts: Contract[]
|
||||
folds: Fold[]
|
||||
recentBets: Bet[]
|
||||
recentComments: Comment[]
|
||||
},
|
||||
user: User | undefined | null
|
||||
) => {
|
||||
const contracts = useContracts() ?? props.contracts
|
||||
const recentBets = useRecentBets() ?? props.recentBets
|
||||
const recentComments = useRecentComments() ?? props.recentComments
|
||||
|
||||
const followedFoldIds = useFollowedFolds(user)
|
||||
|
||||
const followedFolds = filterDefined(
|
||||
(followedFoldIds ?? []).map((id) =>
|
||||
props.folds.find((fold) => fold.id === id)
|
||||
)
|
||||
(followedFoldIds ?? []).map((id) => folds.find((fold) => fold.id === id))
|
||||
)
|
||||
|
||||
// Save the initial followed fold slugs.
|
||||
|
@ -67,23 +55,35 @@ export const useActiveContracts = (
|
|||
: undefined
|
||||
|
||||
// Show no contracts before your info is loaded.
|
||||
let feedContracts: Contract[] = []
|
||||
let yourContracts: Contract[] = []
|
||||
if (yourBetContracts && followedFoldIds) {
|
||||
// Show all contracts if no folds are followed.
|
||||
if (followedFoldIds.length === 0) feedContracts = contracts
|
||||
if (followedFoldIds.length === 0) yourContracts = contracts
|
||||
else
|
||||
feedContracts = contracts.filter(
|
||||
yourContracts = contracts.filter(
|
||||
(contract) =>
|
||||
contract.lowercaseTags.some((tag) => tagSet.has(tag)) ||
|
||||
yourBetContracts.has(contract.id)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
yourContracts,
|
||||
initialFollowedFoldSlugs,
|
||||
}
|
||||
}
|
||||
|
||||
export const useFindActiveContracts = (props: {
|
||||
contracts: Contract[]
|
||||
recentBets: Bet[]
|
||||
recentComments: Comment[]
|
||||
}) => {
|
||||
const { contracts, recentBets, recentComments } = props
|
||||
|
||||
const activeContracts = findActiveContracts(
|
||||
feedContracts,
|
||||
contracts,
|
||||
recentComments,
|
||||
recentBets,
|
||||
365
|
||||
recentBets
|
||||
)
|
||||
|
||||
const betsByContract = _.groupBy(recentBets, (bet) => bet.contractId)
|
||||
|
@ -105,6 +105,24 @@ export const useActiveContracts = (
|
|||
activeContracts,
|
||||
activeBets,
|
||||
activeComments,
|
||||
initialFollowedFoldSlugs,
|
||||
}
|
||||
}
|
||||
|
||||
export const useExploreContracts = (maxContracts = 75) => {
|
||||
const inactiveContracts = useInactiveContracts()
|
||||
|
||||
const contractsDict = _.fromPairs(
|
||||
(inactiveContracts ?? []).map((c) => [c.id, c])
|
||||
)
|
||||
|
||||
// Preserve random ordering once inactiveContracts loaded.
|
||||
const exploreContractIds = useMemo(
|
||||
() => _.shuffle(Object.keys(contractsDict)),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[!!inactiveContracts]
|
||||
).slice(0, maxContracts)
|
||||
|
||||
if (!inactiveContracts) return undefined
|
||||
|
||||
return filterDefined(exploreContractIds.map((id) => contractsDict[id]))
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import _ from 'lodash'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
export type Sort =
|
||||
| 'creator'
|
||||
|
@ -8,6 +10,7 @@ export type Sort =
|
|||
| 'most-traded'
|
||||
| '24-hour-vol'
|
||||
| 'close-date'
|
||||
| 'closed'
|
||||
| 'resolved'
|
||||
| 'all'
|
||||
|
||||
|
@ -24,19 +27,34 @@ export function useQueryAndSortParams(options?: { defaultSort: Sort }) {
|
|||
router.push(router, undefined, { shallow: true })
|
||||
}
|
||||
|
||||
const setQuery = (query: string | undefined) => {
|
||||
const [queryState, setQueryState] = useState(query)
|
||||
|
||||
useEffect(() => {
|
||||
setQueryState(query)
|
||||
}, [query])
|
||||
|
||||
// Debounce router query update.
|
||||
const pushQuery = useMemo(
|
||||
() =>
|
||||
_.debounce((query: string | undefined) => {
|
||||
if (query) {
|
||||
router.query.q = query
|
||||
} else {
|
||||
delete router.query.q
|
||||
}
|
||||
|
||||
router.push(router, undefined, { shallow: true })
|
||||
}, 500),
|
||||
[router]
|
||||
)
|
||||
|
||||
const setQuery = (query: string | undefined) => {
|
||||
setQueryState(query)
|
||||
pushQuery(query)
|
||||
}
|
||||
|
||||
return {
|
||||
sort: sort ?? options?.defaultSort ?? '24-hour-vol',
|
||||
query: query ?? '',
|
||||
query: queryState ?? '',
|
||||
setSort,
|
||||
setQuery,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import _ from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Bet, listenForUserBets } from '../lib/firebase/bets'
|
||||
import {
|
||||
Bet,
|
||||
listenForUserBets,
|
||||
listenForUserContractBets,
|
||||
} from '../lib/firebase/bets'
|
||||
|
||||
export const useUserBets = (userId: string | undefined) => {
|
||||
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
|
||||
|
@ -12,6 +16,20 @@ export const useUserBets = (userId: string | undefined) => {
|
|||
return bets
|
||||
}
|
||||
|
||||
export const useUserContractBets = (
|
||||
userId: string | undefined,
|
||||
contractId: string | undefined
|
||||
) => {
|
||||
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (userId && contractId)
|
||||
return listenForUserContractBets(userId, contractId, setBets)
|
||||
}, [userId, contractId])
|
||||
|
||||
return bets
|
||||
}
|
||||
|
||||
export const useUserBetContracts = (userId: string | undefined) => {
|
||||
const [contractIds, setContractIds] = useState<string[] | undefined>()
|
||||
|
||||
|
|
|
@ -74,6 +74,21 @@ export function listenForUserBets(
|
|||
})
|
||||
}
|
||||
|
||||
export function listenForUserContractBets(
|
||||
userId: string,
|
||||
contractId: string,
|
||||
setBets: (bets: Bet[]) => void
|
||||
) {
|
||||
const betsQuery = query(
|
||||
collection(db, 'contracts', contractId, 'bets'),
|
||||
where('userId', '==', userId)
|
||||
)
|
||||
return listenForValues<Bet>(betsQuery, (bets) => {
|
||||
bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime)
|
||||
setBets(bets)
|
||||
})
|
||||
}
|
||||
|
||||
export function withoutAnteBets(contract: Contract, bets?: Bet[]) {
|
||||
const { createdTime } = contract
|
||||
|
||||
|
|
|
@ -115,6 +115,41 @@ export function listenForContracts(
|
|||
return listenForValues<Contract>(q, setContracts)
|
||||
}
|
||||
|
||||
const activeContractsQuery = query(
|
||||
contractCollection,
|
||||
where('isResolved', '==', false),
|
||||
where('visibility', '==', 'public'),
|
||||
where('volume24Hours', '>', 0)
|
||||
)
|
||||
|
||||
export function getActiveContracts() {
|
||||
return getValues<Contract>(activeContractsQuery)
|
||||
}
|
||||
|
||||
export function listenForActiveContracts(
|
||||
setContracts: (contracts: Contract[]) => void
|
||||
) {
|
||||
return listenForValues<Contract>(activeContractsQuery, setContracts)
|
||||
}
|
||||
|
||||
const inactiveContractsQuery = query(
|
||||
contractCollection,
|
||||
where('isResolved', '==', false),
|
||||
where('closeTime', '>', Date.now()),
|
||||
where('visibility', '==', 'public'),
|
||||
where('volume24Hours', '==', 0)
|
||||
)
|
||||
|
||||
export function getInactiveContracts() {
|
||||
return getValues<Contract>(inactiveContractsQuery)
|
||||
}
|
||||
|
||||
export function listenForInactiveContracts(
|
||||
setContracts: (contracts: Contract[]) => void
|
||||
) {
|
||||
return listenForValues<Contract>(inactiveContractsQuery, setContracts)
|
||||
}
|
||||
|
||||
export function listenForContract(
|
||||
contractId: string,
|
||||
setContract: (contract: Contract | null) => void
|
||||
|
|
|
@ -54,13 +54,12 @@ export async function getFoldBySlug(slug: string) {
|
|||
}
|
||||
|
||||
function contractsByTagsQuery(tags: string[]) {
|
||||
// TODO: if tags.length > 10, execute multiple parallel queries
|
||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase()).slice(0, 10)
|
||||
|
||||
return query(
|
||||
contractCollection,
|
||||
where(
|
||||
'lowercaseTags',
|
||||
'array-contains-any',
|
||||
tags.map((tag) => tag.toLowerCase())
|
||||
)
|
||||
where('lowercaseTags', 'array-contains-any', lowercaseTags)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -74,7 +73,6 @@ export async function getFoldContracts(fold: Fold) {
|
|||
} = fold
|
||||
|
||||
const [tagsContracts, includedContracts] = await Promise.all([
|
||||
// TODO: if tags.length > 10, execute multiple parallel queries
|
||||
tags.length > 0 ? getValues<Contract>(contractsByTagsQuery(tags)) : [],
|
||||
|
||||
// TODO: if contractIds.length > 10, execute multiple parallel queries
|
||||
|
@ -163,9 +161,10 @@ export function listenForFollow(
|
|||
export async function getFoldsByTags(tags: string[]) {
|
||||
if (tags.length === 0) return []
|
||||
|
||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||
const folds = await getValues<Fold>(
|
||||
// TODO: split into multiple queries if tags.length > 10.
|
||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase()).slice(0, 10)
|
||||
|
||||
const folds = await getValues<Fold>(
|
||||
query(
|
||||
foldCollection,
|
||||
where('lowercaseTags', 'array-contains-any', lowercaseTags)
|
||||
|
@ -179,10 +178,10 @@ export function listenForFoldsWithTags(
|
|||
tags: string[],
|
||||
setFolds: (folds: Fold[]) => void
|
||||
) {
|
||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||
const q =
|
||||
// TODO: split into multiple queries if tags.length > 10.
|
||||
query(
|
||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase()).slice(0, 10)
|
||||
|
||||
const q = query(
|
||||
foldCollection,
|
||||
where('lowercaseTags', 'array-contains-any', lowercaseTags)
|
||||
)
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { getFirestore } from '@firebase/firestore'
|
||||
import { initializeApp, getApps, getApp } from 'firebase/app'
|
||||
|
||||
// Used to decide which Stripe instance to point to
|
||||
export const isProd = process.env.NEXT_PUBLIC_FIREBASE_ENV !== 'DEV'
|
||||
|
||||
const firebaseConfig = isProd
|
||||
? {
|
||||
const FIREBASE_CONFIGS = {
|
||||
PROD: {
|
||||
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
||||
authDomain: 'mantic-markets.firebaseapp.com',
|
||||
projectId: 'mantic-markets',
|
||||
|
@ -12,8 +13,8 @@ const firebaseConfig = isProd
|
|||
messagingSenderId: '128925704902',
|
||||
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
||||
measurementId: 'G-SSFK1Q138D',
|
||||
}
|
||||
: {
|
||||
},
|
||||
DEV: {
|
||||
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
||||
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
||||
projectId: 'dev-mantic-markets',
|
||||
|
@ -21,8 +22,20 @@ const firebaseConfig = isProd
|
|||
messagingSenderId: '134303100058',
|
||||
appId: '1:134303100058:web:27f9ea8b83347251f80323',
|
||||
measurementId: 'G-YJC9E37P37',
|
||||
},
|
||||
THEOREMONE: {
|
||||
apiKey: 'AIzaSyBSXL6Ys7InNHnCKSy-_E_luhh4Fkj4Z6M',
|
||||
authDomain: 'theoremone-manifold.firebaseapp.com',
|
||||
projectId: 'theoremone-manifold',
|
||||
storageBucket: 'theoremone-manifold.appspot.com',
|
||||
messagingSenderId: '698012149198',
|
||||
appId: '1:698012149198:web:b342af75662831aa84b79f',
|
||||
measurementId: 'G-Y3EZ1WNT6E',
|
||||
},
|
||||
}
|
||||
|
||||
const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
|
||||
// @ts-ignore
|
||||
const firebaseConfig = FIREBASE_CONFIGS[ENV]
|
||||
// Initialize Firebase
|
||||
export const app = getApps().length ? getApp() : initializeApp(firebaseConfig)
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { db } from './init'
|
||||
import {
|
||||
doc,
|
||||
getDoc,
|
||||
getDocs,
|
||||
onSnapshot,
|
||||
|
@ -22,7 +21,9 @@ export function listenForValue<T>(
|
|||
docRef: DocumentReference,
|
||||
setValue: (value: T | null) => void
|
||||
) {
|
||||
return onSnapshot(docRef, (snapshot) => {
|
||||
// Exclude cached snapshots so we only trigger on fresh data.
|
||||
// includeMetadataChanges ensures listener is called even when server data is the same as cached data.
|
||||
return onSnapshot(docRef, { includeMetadataChanges: true }, (snapshot) => {
|
||||
if (snapshot.metadata.fromCache) return
|
||||
|
||||
const value = snapshot.exists() ? (snapshot.data() as T) : null
|
||||
|
@ -34,7 +35,9 @@ export function listenForValues<T>(
|
|||
query: Query,
|
||||
setValues: (values: T[]) => void
|
||||
) {
|
||||
return onSnapshot(query, (snapshot) => {
|
||||
// Exclude cached snapshots so we only trigger on fresh data.
|
||||
// includeMetadataChanges ensures listener is called even when server data is the same as cached data.
|
||||
return onSnapshot(query, { includeMetadataChanges: true }, (snapshot) => {
|
||||
if (snapshot.metadata.fromCache) return
|
||||
|
||||
const values = snapshot.docs.map((doc) => doc.data() as T)
|
||||
|
|
|
@ -145,7 +145,7 @@ export default function ContractPage(props: {
|
|||
|
||||
<Col className="flex-1">
|
||||
{allowTrade && (
|
||||
<BetPanel className="hidden lg:inline" contract={contract} />
|
||||
<BetPanel className="hidden lg:flex" contract={contract} />
|
||||
)}
|
||||
{allowResolve && (
|
||||
<ResolutionPanel creator={user} contract={contract} />
|
||||
|
@ -177,12 +177,8 @@ function BetsSection(props: {
|
|||
return (
|
||||
<div>
|
||||
<Title className="px-2" text="Your trades" />
|
||||
{isBinary && (
|
||||
<>
|
||||
<MyBetsSummary className="px-2" contract={contract} bets={userBets} />
|
||||
<Spacer h={6} />
|
||||
</>
|
||||
)}
|
||||
<ContractBetsTable contract={contract} bets={userBets} />
|
||||
<Spacer h={12} />
|
||||
</div>
|
||||
|
|
|
@ -139,6 +139,15 @@ function Contents() {
|
|||
bettors that are correct more often will gain influence, leading to
|
||||
better-calibrated forecasts over time.
|
||||
</p>
|
||||
<p>
|
||||
Since our launch, we've seen hundreds of users trade each day, on over a
|
||||
thousand different markets! You can track the popularity of our platform
|
||||
at{' '}
|
||||
<a href="http://manifold.markets/analytics">
|
||||
http://manifold.markets/analytics
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<h3 id="how-are-markets-resolved-">How are markets resolved?</h3>
|
||||
<p>
|
||||
The creator of the prediction market decides the outcome and earns{' '}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import _ from 'lodash'
|
||||
import { ContractFeed } from '../components/contract-feed'
|
||||
import { ContractFeed, ContractSummaryFeed } from '../components/contract-feed'
|
||||
import { Page } from '../components/page'
|
||||
import { Contract } from '../lib/firebase/contracts'
|
||||
import { Comment } from '../lib/firebase/comments'
|
||||
import { Col } from '../components/layout/col'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { filterDefined } from '../../common/util/array'
|
||||
|
||||
const MAX_ACTIVE_CONTRACTS = 75
|
||||
const MAX_HOT_MARKETS = 10
|
||||
|
||||
// This does NOT include comment times, since those aren't part of the contract atm.
|
||||
// TODO: Maybe store last activity time directly in the contract?
|
||||
|
@ -25,12 +23,11 @@ function lastActivityTime(contract: Contract) {
|
|||
// - Comment on a market
|
||||
// - New market created
|
||||
// - Market resolved
|
||||
// - Markets with most betting in last 24 hours
|
||||
// - Bet on market
|
||||
export function findActiveContracts(
|
||||
allContracts: Contract[],
|
||||
recentComments: Comment[],
|
||||
recentBets: Bet[],
|
||||
daysAgo = 3
|
||||
recentBets: Bet[]
|
||||
) {
|
||||
const idToActivityTime = new Map<string, number>()
|
||||
function record(contractId: string, time: number) {
|
||||
|
@ -39,52 +36,38 @@ export function findActiveContracts(
|
|||
idToActivityTime.set(contractId, Math.max(oldTime ?? 0, time))
|
||||
}
|
||||
|
||||
let contracts: Contract[] = []
|
||||
const contractsById = new Map(allContracts.map((c) => [c.id, c]))
|
||||
|
||||
// Find contracts with activity in the last 3 days
|
||||
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
||||
for (const contract of allContracts || []) {
|
||||
if (lastActivityTime(contract) > Date.now() - daysAgo * DAY_IN_MS) {
|
||||
contracts.push(contract)
|
||||
// Record contract activity.
|
||||
for (const contract of allContracts) {
|
||||
record(contract.id, lastActivityTime(contract))
|
||||
}
|
||||
}
|
||||
|
||||
// Add every contract that had a recent comment, too
|
||||
const contractsById = new Map(allContracts.map((c) => [c.id, c]))
|
||||
for (const comment of recentComments) {
|
||||
const contract = contractsById.get(comment.contractId)
|
||||
if (contract) {
|
||||
contracts.push(contract)
|
||||
record(contract.id, comment.createdTime)
|
||||
}
|
||||
if (contract) record(contract.id, comment.createdTime)
|
||||
}
|
||||
|
||||
// Add recent top-trading contracts, ordered by last bet.
|
||||
// Add contracts by last bet time.
|
||||
const contractBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
||||
const contractTotalBets = _.mapValues(contractBets, (bets) =>
|
||||
_.sumBy(bets, (bet) => bet.amount)
|
||||
const contractMostRecentBet = _.mapValues(
|
||||
contractBets,
|
||||
(bets) => _.maxBy(bets, (bet) => bet.createdTime) as Bet
|
||||
)
|
||||
const sortedPairs = _.sortBy(
|
||||
_.toPairs(contractTotalBets),
|
||||
([_, total]) => -1 * total
|
||||
)
|
||||
const topTradedContracts = filterDefined(
|
||||
sortedPairs.map(([id]) => contractsById.get(id))
|
||||
).slice(0, MAX_HOT_MARKETS)
|
||||
|
||||
for (const contract of topTradedContracts) {
|
||||
const bet = recentBets.find((bet) => bet.contractId === contract.id)
|
||||
if (bet) {
|
||||
contracts.push(contract)
|
||||
record(contract.id, bet.createdTime)
|
||||
}
|
||||
for (const bet of Object.values(contractMostRecentBet)) {
|
||||
const contract = contractsById.get(bet.contractId)
|
||||
if (contract) record(contract.id, bet.createdTime)
|
||||
}
|
||||
|
||||
contracts = _.uniqBy(contracts, (c) => c.id)
|
||||
contracts = contracts.filter((contract) => contract.visibility === 'public')
|
||||
contracts = _.sortBy(contracts, (c) => -(idToActivityTime.get(c.id) ?? 0))
|
||||
return contracts.slice(0, MAX_ACTIVE_CONTRACTS)
|
||||
let activeContracts = allContracts.filter(
|
||||
(contract) => contract.visibility === 'public' && !contract.isResolved
|
||||
)
|
||||
activeContracts = _.sortBy(
|
||||
activeContracts,
|
||||
(c) => -(idToActivityTime.get(c.id) ?? 0)
|
||||
)
|
||||
return activeContracts.slice(0, MAX_ACTIVE_CONTRACTS)
|
||||
}
|
||||
|
||||
export function ActivityFeed(props: {
|
||||
|
@ -116,6 +99,24 @@ export function ActivityFeed(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export function SummaryActivityFeed(props: { contracts: Contract[] }) {
|
||||
const { contracts } = props
|
||||
|
||||
return (
|
||||
<Col className="items-center">
|
||||
<Col className="w-full max-w-3xl">
|
||||
<Col className="w-full divide-y divide-gray-300 self-center bg-white">
|
||||
{contracts.map((contract) => (
|
||||
<div key={contract.id} className="py-6 px-2 sm:px-4">
|
||||
<ContractSummaryFeed contract={contract} />
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ActivityPage() {
|
||||
return (
|
||||
<Page>
|
||||
|
|
|
@ -19,11 +19,11 @@ export default function AddFundsPage() {
|
|||
<SEO title="Add funds" description="Add funds" url="/add-funds" />
|
||||
|
||||
<Col className="items-center">
|
||||
<Col>
|
||||
<Title text="Get Manifold Dollars" />
|
||||
<Col className="bg-white rounded sm:shadow-md p-4 py-8 sm:p-8 h-full">
|
||||
<Title className="!mt-0" text="Get Manifold Dollars" />
|
||||
<img
|
||||
className="mt-6 block"
|
||||
src="/praying-mantis-light.svg"
|
||||
className="mb-6 block self-center -scale-x-100"
|
||||
src="/stylized-crane-black.png"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
|
@ -50,7 +50,7 @@ export default function AddFundsPage() {
|
|||
<form
|
||||
action={checkoutURL(user?.id || '', amountSelected)}
|
||||
method="POST"
|
||||
className="mt-12"
|
||||
className="mt-8"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
|
|
|
@ -26,6 +26,7 @@ export type LiteMarket = {
|
|||
volume24Hours: number
|
||||
isResolved: boolean
|
||||
resolution?: string
|
||||
resolutionTime?: number
|
||||
}
|
||||
|
||||
export type FullMarket = LiteMarket & {
|
||||
|
@ -54,6 +55,7 @@ export function toLiteMarket({
|
|||
volume24Hours,
|
||||
isResolved,
|
||||
resolution,
|
||||
resolutionTime,
|
||||
}: Contract): LiteMarket {
|
||||
return {
|
||||
id,
|
||||
|
@ -61,7 +63,10 @@ export function toLiteMarket({
|
|||
creatorName,
|
||||
createdTime,
|
||||
creatorAvatarUrl,
|
||||
closeTime,
|
||||
closeTime:
|
||||
resolutionTime && closeTime
|
||||
? Math.min(resolutionTime, closeTime)
|
||||
: closeTime,
|
||||
question,
|
||||
description,
|
||||
tags,
|
||||
|
@ -72,5 +77,6 @@ export function toLiteMarket({
|
|||
volume24Hours,
|
||||
isResolved,
|
||||
resolution,
|
||||
resolutionTime,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -248,6 +248,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
error={anteError}
|
||||
setError={setAnteError}
|
||||
disabled={isSubmitting}
|
||||
contractId={undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -5,12 +5,7 @@ import { Fold } from '../../../../common/fold'
|
|||
import { Comment } from '../../../../common/comment'
|
||||
import { Page } from '../../../components/page'
|
||||
import { Title } from '../../../components/title'
|
||||
import {
|
||||
Bet,
|
||||
getRecentContractBets,
|
||||
listAllBets,
|
||||
} from '../../../lib/firebase/bets'
|
||||
import { listAllComments } from '../../../lib/firebase/comments'
|
||||
import { Bet, listAllBets } from '../../../lib/firebase/bets'
|
||||
import { Contract } from '../../../lib/firebase/contracts'
|
||||
import {
|
||||
foldPath,
|
||||
|
@ -40,6 +35,7 @@ import FeedCreate from '../../../components/feed-create'
|
|||
import { SEO } from '../../../components/SEO'
|
||||
import { useTaggedContracts } from '../../../hooks/use-contracts'
|
||||
import { Linkify } from '../../../components/linkify'
|
||||
import { filterDefined } from '../../../../common/util/array'
|
||||
|
||||
export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||
const { slugs } = props.params
|
||||
|
@ -49,42 +45,21 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
|||
|
||||
const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : []
|
||||
|
||||
const betsPromise = Promise.all(
|
||||
const bets = await Promise.all(
|
||||
contracts.map((contract) => listAllBets(contract.id))
|
||||
)
|
||||
const betsByContract = _.fromPairs(contracts.map((c, i) => [c.id, bets[i]]))
|
||||
|
||||
const [contractComments, contractRecentBets] = await Promise.all([
|
||||
Promise.all(
|
||||
contracts.map((contract) => listAllComments(contract.id).catch((_) => []))
|
||||
),
|
||||
Promise.all(
|
||||
contracts.map((contract) =>
|
||||
getRecentContractBets(contract.id).catch((_) => [])
|
||||
)
|
||||
),
|
||||
])
|
||||
|
||||
let activeContracts = findActiveContracts(
|
||||
contracts,
|
||||
_.flatten(contractComments),
|
||||
_.flatten(contractRecentBets),
|
||||
365
|
||||
)
|
||||
let activeContracts = findActiveContracts(contracts, [], _.flatten(bets))
|
||||
const [resolved, unresolved] = _.partition(
|
||||
activeContracts,
|
||||
({ isResolved }) => isResolved
|
||||
)
|
||||
activeContracts = [...unresolved, ...resolved]
|
||||
|
||||
const activeContractBets = await Promise.all(
|
||||
activeContracts.map((contract) => listAllBets(contract.id).catch((_) => []))
|
||||
const activeContractBets = activeContracts.map(
|
||||
(contract) => betsByContract[contract.id] ?? []
|
||||
)
|
||||
const activeContractComments = activeContracts.map(
|
||||
(contract) =>
|
||||
contractComments[contracts.findIndex((c) => c.id === contract.id)]
|
||||
)
|
||||
|
||||
const bets = await betsPromise
|
||||
|
||||
const creatorScores = scoreCreators(contracts, bets)
|
||||
const traderScores = scoreTraders(contracts, bets)
|
||||
|
@ -102,7 +77,7 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
|||
contracts,
|
||||
activeContracts,
|
||||
activeContractBets,
|
||||
activeContractComments,
|
||||
activeContractComments: activeContracts.map(() => []),
|
||||
traderScores,
|
||||
topTraders,
|
||||
creatorScores,
|
||||
|
@ -169,9 +144,11 @@ export default function FoldPage(props: {
|
|||
taggedContracts.map((contract) => [contract.id, contract])
|
||||
)
|
||||
|
||||
const contracts = props.contracts.map((contract) => contractsMap[contract.id])
|
||||
const activeContracts = props.activeContracts.map(
|
||||
(contract) => contractsMap[contract.id]
|
||||
const contracts = filterDefined(
|
||||
props.contracts.map((contract) => contractsMap[contract.id])
|
||||
)
|
||||
const activeContracts = filterDefined(
|
||||
props.activeContracts.map((contract) => contractsMap[contract.id])
|
||||
)
|
||||
|
||||
if (fold === null || !foldSubpages.includes(page) || slugs[2]) {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import Router from 'next/router'
|
||||
import { SparklesIcon, GlobeAltIcon } from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import _ from 'lodash'
|
||||
|
||||
import { Contract } from '../lib/firebase/contracts'
|
||||
import { Page } from '../components/page'
|
||||
import { ActivityFeed } from './activity'
|
||||
import { ActivityFeed, SummaryActivityFeed } from './activity'
|
||||
import { Comment } from '../lib/firebase/comments'
|
||||
import { Bet } from '../lib/firebase/bets'
|
||||
import FeedCreate from '../components/feed-create'
|
||||
|
@ -13,12 +16,15 @@ import { useUser } from '../hooks/use-user'
|
|||
import { Fold } from '../../common/fold'
|
||||
import { LoadingIndicator } from '../components/loading-indicator'
|
||||
import { Row } from '../components/layout/row'
|
||||
import { SparklesIcon } from '@heroicons/react/solid'
|
||||
import { FastFoldFollowing } from '../components/fast-fold-following'
|
||||
import {
|
||||
getAllContractInfo,
|
||||
useActiveContracts,
|
||||
} from '../hooks/use-active-contracts'
|
||||
useExploreContracts,
|
||||
useFilterYourContracts,
|
||||
useFindActiveContracts,
|
||||
} from '../hooks/use-find-active-contracts'
|
||||
import { useGetRecentBets } from '../hooks/use-bets'
|
||||
import { useActiveContracts } from '../hooks/use-contracts'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const contractInfo = await getAllContractInfo()
|
||||
|
@ -35,14 +41,27 @@ const Home = (props: {
|
|||
recentBets: Bet[]
|
||||
recentComments: Comment[]
|
||||
}) => {
|
||||
const { folds, recentComments } = props
|
||||
const user = useUser()
|
||||
|
||||
const {
|
||||
activeContracts,
|
||||
activeBets,
|
||||
activeComments,
|
||||
initialFollowedFoldSlugs,
|
||||
} = useActiveContracts(props, user)
|
||||
const contracts = useActiveContracts() ?? props.contracts
|
||||
const { yourContracts, initialFollowedFoldSlugs } = useFilterYourContracts(
|
||||
user,
|
||||
folds,
|
||||
contracts
|
||||
)
|
||||
|
||||
const recentBets = useGetRecentBets()
|
||||
const { activeContracts, activeBets, activeComments } =
|
||||
useFindActiveContracts({
|
||||
contracts: yourContracts,
|
||||
recentBets: recentBets ?? [],
|
||||
recentComments,
|
||||
})
|
||||
|
||||
const exploreContracts = useExploreContracts()
|
||||
|
||||
const [feedMode, setFeedMode] = useState<'activity' | 'explore'>('activity')
|
||||
|
||||
if (user === null) {
|
||||
Router.replace('/')
|
||||
|
@ -64,14 +83,37 @@ const Home = (props: {
|
|||
/>
|
||||
)}
|
||||
|
||||
<Spacer h={5} />
|
||||
|
||||
<Col className="mx-3 mb-3 gap-2 text-sm text-gray-800 sm:flex-row">
|
||||
<Row className="gap-2">
|
||||
<div className="tabs">
|
||||
<div
|
||||
className={clsx(
|
||||
'tab gap-2',
|
||||
feedMode === 'activity' && 'tab-active'
|
||||
)}
|
||||
onClick={() => setFeedMode('activity')}
|
||||
>
|
||||
<SparklesIcon className="inline h-5 w-5" aria-hidden="true" />
|
||||
<span className="whitespace-nowrap">Recent activity</span>
|
||||
Recent activity
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'tab gap-2',
|
||||
feedMode === 'explore' && 'tab-active'
|
||||
)}
|
||||
onClick={() => setFeedMode('explore')}
|
||||
>
|
||||
<GlobeAltIcon className="inline h-5 w-5" aria-hidden="true" />
|
||||
Explore
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
{activeContracts ? (
|
||||
{feedMode === 'activity' &&
|
||||
(recentBets ? (
|
||||
<ActivityFeed
|
||||
contracts={activeContracts}
|
||||
contractBets={activeBets}
|
||||
|
@ -79,7 +121,14 @@ const Home = (props: {
|
|||
/>
|
||||
) : (
|
||||
<LoadingIndicator className="mt-4" />
|
||||
)}
|
||||
))}
|
||||
|
||||
{feedMode === 'explore' &&
|
||||
(exploreContracts ? (
|
||||
<SummaryActivityFeed contracts={exploreContracts} />
|
||||
) : (
|
||||
<LoadingIndicator className="mt-4" />
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
</Page>
|
||||
|
|
|
@ -245,6 +245,7 @@ ${TEST_VALUE}
|
|||
error={anteError}
|
||||
setError={setAnteError}
|
||||
disabled={isSubmitting}
|
||||
contractId={undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 55 KiB |
BIN
web/public/stylized-crane-black.png
Normal file
BIN
web/public/stylized-crane-black.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 683 KiB |
Loading…
Reference in New Issue
Block a user