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
|
contractId: string
|
||||||
|
|
||||||
amount: number // bet size; negative if SELL bet
|
amount: number // bet size; negative if SELL bet
|
||||||
|
loanAmount?: number
|
||||||
outcome: string
|
outcome: string
|
||||||
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
||||||
|
|
||||||
|
@ -21,3 +22,5 @@ export type Bet = {
|
||||||
|
|
||||||
createdTime: number
|
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 {
|
import {
|
||||||
calculateShares,
|
calculateShares,
|
||||||
getProbability,
|
getProbability,
|
||||||
|
@ -20,6 +21,7 @@ export const getNewBinaryCpmmBetInfo = (
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
amount: number,
|
amount: number,
|
||||||
contract: FullContract<CPMM, Binary>,
|
contract: FullContract<CPMM, Binary>,
|
||||||
|
loanAmount: number,
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
const { pool, k } = contract
|
const { pool, k } = contract
|
||||||
|
@ -32,7 +34,7 @@ export const getNewBinaryCpmmBetInfo = (
|
||||||
? [y - shares + amount, amount]
|
? [y - shares + amount, amount]
|
||||||
: [amount, n - shares + amount]
|
: [amount, n - shares + amount]
|
||||||
|
|
||||||
const newBalance = user.balance - amount
|
const newBalance = user.balance - (amount - loanAmount)
|
||||||
|
|
||||||
const probBefore = getCpmmProbability(pool)
|
const probBefore = getCpmmProbability(pool)
|
||||||
const newPool = { YES: newY, NO: newN }
|
const newPool = { YES: newY, NO: newN }
|
||||||
|
@ -45,6 +47,7 @@ export const getNewBinaryCpmmBetInfo = (
|
||||||
amount,
|
amount,
|
||||||
shares,
|
shares,
|
||||||
outcome,
|
outcome,
|
||||||
|
loanAmount,
|
||||||
probBefore,
|
probBefore,
|
||||||
probAfter,
|
probAfter,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
|
@ -58,6 +61,7 @@ export const getNewBinaryDpmBetInfo = (
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
amount: number,
|
amount: number,
|
||||||
contract: FullContract<DPM, Binary>,
|
contract: FullContract<DPM, Binary>,
|
||||||
|
loanAmount: number,
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
const { YES: yesPool, NO: noPool } = contract.pool
|
const { YES: yesPool, NO: noPool } = contract.pool
|
||||||
|
@ -91,6 +95,7 @@ export const getNewBinaryDpmBetInfo = (
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
|
loanAmount,
|
||||||
shares,
|
shares,
|
||||||
outcome,
|
outcome,
|
||||||
probBefore,
|
probBefore,
|
||||||
|
@ -98,7 +103,7 @@ export const getNewBinaryDpmBetInfo = (
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance - amount
|
const newBalance = user.balance - (amount - loanAmount)
|
||||||
|
|
||||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||||
}
|
}
|
||||||
|
@ -108,6 +113,7 @@ export const getNewMultiBetInfo = (
|
||||||
outcome: string,
|
outcome: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
contract: FullContract<DPM, Multi | FreeResponse>,
|
contract: FullContract<DPM, Multi | FreeResponse>,
|
||||||
|
loanAmount: number,
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
const { pool, totalShares, totalBets } = contract
|
const { pool, totalShares, totalBets } = contract
|
||||||
|
@ -131,6 +137,7 @@ export const getNewMultiBetInfo = (
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
|
loanAmount,
|
||||||
shares,
|
shares,
|
||||||
outcome,
|
outcome,
|
||||||
probBefore,
|
probBefore,
|
||||||
|
@ -138,7 +145,17 @@ export const getNewMultiBetInfo = (
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance - amount
|
const newBalance = user.balance - (amount - loanAmount)
|
||||||
|
|
||||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
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 }))
|
.map(({ userId, payout }) => ({ userId, payout }))
|
||||||
.concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
|
.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
|
const investments = bets
|
||||||
.filter((bet) => !bet.sale)
|
.filter((bet) => !bet.sale)
|
||||||
.map((bet) => {
|
.map((bet) => {
|
||||||
const { userId, amount } = bet
|
const { userId, amount, loanAmount } = bet
|
||||||
return { userId, payout: -amount }
|
const payout = -amount - (loanAmount ?? 0)
|
||||||
|
return { userId, payout }
|
||||||
})
|
})
|
||||||
|
|
||||||
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
|
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const getSellBetInfo = (
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
const { pool, totalShares, totalBets } = contract
|
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)
|
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 {
|
return {
|
||||||
newBet,
|
newBet,
|
||||||
|
|
|
@ -28,6 +28,8 @@ export type PrivateUser = {
|
||||||
|
|
||||||
email?: string
|
email?: string
|
||||||
unsubscribedFromResolutionEmails?: boolean
|
unsubscribedFromResolutionEmails?: boolean
|
||||||
|
unsubscribedFromCommentEmails?: boolean
|
||||||
|
unsubscribedFromAnswerEmails?: boolean
|
||||||
initialDeviceToken?: string
|
initialDeviceToken?: string
|
||||||
initialIpAddress?: string
|
initialIpAddress?: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"source": "functions"
|
"source": "functions"
|
||||||
},
|
},
|
||||||
"firestore": {
|
"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
|
# Compiled JavaScript files
|
||||||
lib/**/*.js
|
lib/**/*.js
|
||||||
lib/**/*.js.map
|
lib/**/*.js.map
|
||||||
|
|
|
@ -3,9 +3,11 @@ import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { getNewMultiBetInfo } from '../../common/new-bet'
|
import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet'
|
||||||
import { Answer } from '../../common/answer'
|
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(
|
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
async (
|
||||||
|
@ -28,7 +30,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
return { status: 'error', message: 'Invalid text' }
|
return { status: 'error', message: 'Invalid text' }
|
||||||
|
|
||||||
// Run as transaction to prevent race conditions.
|
// 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 userDoc = firestore.doc(`users/${userId}`)
|
||||||
const userSnap = await transaction.get(userDoc)
|
const userSnap = await transaction.get(userDoc)
|
||||||
if (!userSnap.exists)
|
if (!userSnap.exists)
|
||||||
|
@ -54,6 +56,11 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (closeTime && Date.now() > closeTime)
|
||||||
return { status: 'error', message: 'Trading is closed' }
|
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>(
|
const [lastAnswer] = await getValues<Answer>(
|
||||||
firestore
|
firestore
|
||||||
.collection(`contracts/${contractId}/answers`)
|
.collection(`contracts/${contractId}/answers`)
|
||||||
|
@ -91,8 +98,17 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
.collection(`contracts/${contractId}/bets`)
|
.collection(`contracts/${contractId}/bets`)
|
||||||
.doc()
|
.doc()
|
||||||
|
|
||||||
|
const loanAmount = getLoanAmount(yourBets, amount)
|
||||||
|
|
||||||
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
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.create(newBetDoc, newBet)
|
||||||
transaction.update(contractDoc, {
|
transaction.update(contractDoc, {
|
||||||
|
@ -103,8 +119,15 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
})
|
})
|
||||||
transaction.update(userDoc, { balance: newBalance })
|
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,
|
cleanUsername,
|
||||||
} from '../../common/util/clean-username'
|
} from '../../common/util/clean-username'
|
||||||
import { sendWelcomeEmail } from './emails'
|
import { sendWelcomeEmail } from './emails'
|
||||||
|
import { isWhitelisted } from '../../common/access'
|
||||||
|
|
||||||
export const createUser = functions
|
export const createUser = functions
|
||||||
.runWith({ minInstances: 1 })
|
.runWith({ minInstances: 1 })
|
||||||
|
@ -32,6 +33,9 @@ export const createUser = functions
|
||||||
const fbUser = await admin.auth().getUser(userId)
|
const fbUser = await admin.auth().getUser(userId)
|
||||||
|
|
||||||
const email = fbUser.email
|
const email = fbUser.email
|
||||||
|
if (!isWhitelisted(email)) {
|
||||||
|
return { status: 'error', message: `${email} is not whitelisted` }
|
||||||
|
}
|
||||||
const emailName = email?.replace(/@.*$/, '')
|
const emailName = email?.replace(/@.*$/, '')
|
||||||
|
|
||||||
const rawName = fbUser.displayName || emailName || 'User' + randomString(4)
|
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 _ = require('lodash')
|
||||||
|
import { Answer } from '../../common/answer'
|
||||||
|
import { Bet } from '../../common/bet'
|
||||||
import { getProbability } from '../../common/calculate'
|
import { getProbability } from '../../common/calculate'
|
||||||
|
import { Comment } from '../../common/comment'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { CREATOR_FEE } from '../../common/fees'
|
import { CREATOR_FEE } from '../../common/fees'
|
||||||
import { PrivateUser, User } from '../../common/user'
|
import { PrivateUser, User } from '../../common/user'
|
||||||
import { formatMoney, formatPercent } from '../../common/util/format'
|
import { formatMoney, formatPercent } from '../../common/util/format'
|
||||||
import { sendTemplateEmail, sendTextEmail } from './send-email'
|
import { sendTemplateEmail, sendTextEmail } from './send-email'
|
||||||
import { getPrivateUser, getUser } from './utils'
|
import { getPrivateUser, getUser, isProd } from './utils'
|
||||||
|
|
||||||
type market_resolved_template = {
|
type market_resolved_template = {
|
||||||
userId: string
|
userId: string
|
||||||
|
@ -13,13 +16,14 @@ type market_resolved_template = {
|
||||||
creatorName: string
|
creatorName: string
|
||||||
question: string
|
question: string
|
||||||
outcome: string
|
outcome: string
|
||||||
|
investment: string
|
||||||
payout: string
|
payout: string
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const toDisplayResolution = (
|
const toDisplayResolution = (
|
||||||
outcome: string,
|
outcome: string,
|
||||||
prob: number,
|
prob?: number,
|
||||||
resolutions?: { [outcome: string]: number }
|
resolutions?: { [outcome: string]: number }
|
||||||
) => {
|
) => {
|
||||||
if (outcome === 'MKT' && resolutions) return 'MULTI'
|
if (outcome === 'MKT' && resolutions) return 'MULTI'
|
||||||
|
@ -28,7 +32,7 @@ const toDisplayResolution = (
|
||||||
YES: 'YES',
|
YES: 'YES',
|
||||||
NO: 'NO',
|
NO: 'NO',
|
||||||
CANCEL: 'N/A',
|
CANCEL: 'N/A',
|
||||||
MKT: formatPercent(prob),
|
MKT: formatPercent(prob ?? 0),
|
||||||
}[outcome]
|
}[outcome]
|
||||||
|
|
||||||
return display === undefined ? `#${outcome}` : display
|
return display === undefined ? `#${outcome}` : display
|
||||||
|
@ -36,6 +40,7 @@ const toDisplayResolution = (
|
||||||
|
|
||||||
export const sendMarketResolutionEmail = async (
|
export const sendMarketResolutionEmail = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
|
investment: number,
|
||||||
payout: number,
|
payout: number,
|
||||||
creator: User,
|
creator: User,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
|
@ -66,6 +71,7 @@ export const sendMarketResolutionEmail = async (
|
||||||
creatorName: creator.name,
|
creatorName: creator.name,
|
||||||
question: contract.question,
|
question: contract.question,
|
||||||
outcome,
|
outcome,
|
||||||
|
investment: `${Math.round(investment)}`,
|
||||||
payout: `${Math.round(payout)}`,
|
payout: `${Math.round(payout)}`,
|
||||||
url: `https://manifold.markets/${creator.username}/${contract.slug}`,
|
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-user'
|
||||||
export * from './create-fold'
|
export * from './create-fold'
|
||||||
export * from './create-answer'
|
export * from './create-answer'
|
||||||
|
export * from './on-create-comment'
|
||||||
export * from './on-fold-follow'
|
export * from './on-fold-follow'
|
||||||
export * from './on-fold-delete'
|
export * from './on-fold-delete'
|
||||||
export * from './unsubscribe'
|
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,
|
getNewBinaryCpmmBetInfo,
|
||||||
getNewBinaryDpmBetInfo,
|
getNewBinaryDpmBetInfo,
|
||||||
getNewMultiBetInfo,
|
getNewMultiBetInfo,
|
||||||
|
getLoanAmount,
|
||||||
} from '../../common/new-bet'
|
} from '../../common/new-bet'
|
||||||
import { removeUndefinedProps } from '../../common/util/object'
|
import { removeUndefinedProps } from '../../common/util/object'
|
||||||
|
import { Bet } from '../../common/bet'
|
||||||
|
import { getValues } from './utils'
|
||||||
|
|
||||||
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
async (
|
||||||
|
@ -51,6 +54,11 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (closeTime && Date.now() > closeTime)
|
||||||
return { status: 'error', message: 'Trading is closed' }
|
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') {
|
if (outcomeType === 'FREE_RESPONSE') {
|
||||||
const answerSnap = await transaction.get(
|
const answerSnap = await transaction.get(
|
||||||
contractDoc.collection('answers').doc(outcome)
|
contractDoc.collection('answers').doc(outcome)
|
||||||
|
@ -63,6 +71,8 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
.collection(`contracts/${contractId}/bets`)
|
.collection(`contracts/${contractId}/bets`)
|
||||||
.doc()
|
.doc()
|
||||||
|
|
||||||
|
const loanAmount = getLoanAmount(yourBets, amount)
|
||||||
|
|
||||||
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
||||||
outcomeType === 'BINARY'
|
outcomeType === 'BINARY'
|
||||||
? mechanism === 'dpm-2'
|
? mechanism === 'dpm-2'
|
||||||
|
@ -71,6 +81,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
outcome as 'YES' | 'NO',
|
outcome as 'YES' | 'NO',
|
||||||
amount,
|
amount,
|
||||||
contract,
|
contract,
|
||||||
|
loanAmount,
|
||||||
newBetDoc.id
|
newBetDoc.id
|
||||||
)
|
)
|
||||||
: (getNewBinaryCpmmBetInfo(
|
: (getNewBinaryCpmmBetInfo(
|
||||||
|
@ -78,6 +89,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
outcome as 'YES' | 'NO',
|
outcome as 'YES' | 'NO',
|
||||||
amount,
|
amount,
|
||||||
contract,
|
contract,
|
||||||
|
loanAmount,
|
||||||
newBetDoc.id
|
newBetDoc.id
|
||||||
) as any)
|
) as any)
|
||||||
: getNewMultiBetInfo(
|
: getNewMultiBetInfo(
|
||||||
|
@ -85,6 +97,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
outcome,
|
outcome,
|
||||||
amount,
|
amount,
|
||||||
contract as any,
|
contract as any,
|
||||||
|
loanAmount,
|
||||||
newBetDoc.id
|
newBetDoc.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,12 @@ import { User } from '../../common/user'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { getUser, payUser } from './utils'
|
import { getUser, payUser } from './utils'
|
||||||
import { sendMarketResolutionEmail } from './emails'
|
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
|
export const resolveMarket = functions
|
||||||
.runWith({ minInstances: 1 })
|
.runWith({ minInstances: 1 })
|
||||||
|
@ -31,7 +36,7 @@ export const resolveMarket = functions
|
||||||
if (!contractSnap.exists)
|
if (!contractSnap.exists)
|
||||||
return { status: 'error', message: 'Invalid contract' }
|
return { status: 'error', message: 'Invalid contract' }
|
||||||
const contract = contractSnap.data() as Contract
|
const contract = contractSnap.data() as Contract
|
||||||
const { creatorId, outcomeType } = contract
|
const { creatorId, outcomeType, closeTime } = contract
|
||||||
|
|
||||||
if (outcomeType === 'BINARY') {
|
if (outcomeType === 'BINARY') {
|
||||||
if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome))
|
if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome))
|
||||||
|
@ -68,15 +73,21 @@ export const resolveMarket = functions
|
||||||
const resolutionProbability =
|
const resolutionProbability =
|
||||||
probabilityInt !== undefined ? probabilityInt / 100 : undefined
|
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,
|
isResolved: true,
|
||||||
resolution: outcome,
|
resolution: outcome,
|
||||||
resolutionTime: Date.now(),
|
resolutionTime,
|
||||||
...(resolutionProbability === undefined
|
closeTime: newCloseTime,
|
||||||
? {}
|
resolutionProbability,
|
||||||
: { resolutionProbability }),
|
resolutions,
|
||||||
...(resolutions === undefined ? {} : { resolutions }),
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
console.log('contract ', contractId, 'resolved to:', outcome)
|
console.log('contract ', contractId, 'resolved to:', outcome)
|
||||||
|
|
||||||
|
@ -92,13 +103,23 @@ export const resolveMarket = functions
|
||||||
? getPayoutsMultiOutcome(resolutions, contract as any, openBets)
|
? getPayoutsMultiOutcome(resolutions, contract as any, openBets)
|
||||||
: getPayouts(outcome, contract, openBets, resolutionProbability)
|
: getPayouts(outcome, contract, openBets, resolutionProbability)
|
||||||
|
|
||||||
|
const loanPayouts = getLoanPayouts(openBets)
|
||||||
|
|
||||||
console.log('payouts:', payouts)
|
console.log('payouts:', payouts)
|
||||||
|
|
||||||
const groups = _.groupBy(payouts, (payout) => payout.userId)
|
const groups = _.groupBy(
|
||||||
|
[...payouts, ...loanPayouts],
|
||||||
|
(payout) => payout.userId
|
||||||
|
)
|
||||||
const userPayouts = _.mapValues(groups, (group) =>
|
const userPayouts = _.mapValues(groups, (group) =>
|
||||||
_.sumBy(group, (g) => g.payout)
|
_.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(
|
const payoutPromises = Object.entries(userPayouts).map(
|
||||||
([userId, payout]) => payUser(userId, payout)
|
([userId, payout]) => payUser(userId, payout)
|
||||||
)
|
)
|
||||||
|
@ -109,7 +130,7 @@ export const resolveMarket = functions
|
||||||
|
|
||||||
await sendResolutionEmails(
|
await sendResolutionEmails(
|
||||||
openBets,
|
openBets,
|
||||||
userPayouts,
|
userPayoutsWithoutLoans,
|
||||||
creator,
|
creator,
|
||||||
contract,
|
contract,
|
||||||
outcome,
|
outcome,
|
||||||
|
@ -134,14 +155,24 @@ const sendResolutionEmails = async (
|
||||||
_.uniq(openBets.map(({ userId }) => userId)),
|
_.uniq(openBets.map(({ userId }) => userId)),
|
||||||
Object.keys(userPayouts)
|
Object.keys(userPayouts)
|
||||||
)
|
)
|
||||||
|
const investedByUser = _.mapValues(
|
||||||
|
_.groupBy(openBets, (bet) => bet.userId),
|
||||||
|
(bets) => _.sumBy(bets, (bet) => bet.amount)
|
||||||
|
)
|
||||||
const emailPayouts = [
|
const emailPayouts = [
|
||||||
...Object.entries(userPayouts),
|
...Object.entries(userPayouts),
|
||||||
...nonWinners.map((userId) => [userId, 0] as const),
|
...nonWinners.map((userId) => [userId, 0] as const),
|
||||||
]
|
].map(([userId, payout]) => ({
|
||||||
|
userId,
|
||||||
|
investment: investedByUser[userId],
|
||||||
|
payout,
|
||||||
|
}))
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
emailPayouts.map(([userId, payout]) =>
|
emailPayouts.map(({ userId, investment, payout }) =>
|
||||||
sendMarketResolutionEmail(
|
sendMarketResolutionEmail(
|
||||||
userId,
|
userId,
|
||||||
|
investment,
|
||||||
payout,
|
payout,
|
||||||
creator,
|
creator,
|
||||||
contract,
|
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,
|
to: string,
|
||||||
subject: string,
|
subject: string,
|
||||||
templateId: string,
|
templateId: string,
|
||||||
templateData: Record<string, string>
|
templateData: Record<string, string>,
|
||||||
|
options?: { from: string }
|
||||||
) => {
|
) => {
|
||||||
const data = {
|
const data = {
|
||||||
from: 'Manifold Markets <info@manifold.markets>',
|
from: options?.from ?? 'Manifold Markets <info@manifold.markets>',
|
||||||
to,
|
to,
|
||||||
subject,
|
subject,
|
||||||
template: templateId,
|
template: templateId,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import Stripe from 'stripe'
|
import Stripe from 'stripe'
|
||||||
|
|
||||||
import { payUser } from './utils'
|
import { isProd, payUser } from './utils'
|
||||||
|
|
||||||
export type StripeTransaction = {
|
export type StripeTransaction = {
|
||||||
userId: string
|
userId: string
|
||||||
|
@ -18,8 +18,7 @@ const stripe = new Stripe(functions.config().stripe.apikey, {
|
||||||
})
|
})
|
||||||
|
|
||||||
// manage at https://dashboard.stripe.com/test/products?active=true
|
// manage at https://dashboard.stripe.com/test/products?active=true
|
||||||
const manticDollarStripePrice =
|
const manticDollarStripePrice = isProd
|
||||||
admin.instanceId().app.options.projectId === 'mantic-markets'
|
|
||||||
? {
|
? {
|
||||||
500: 'price_1KFQXcGdoFKoCJW770gTNBrm',
|
500: 'price_1KFQXcGdoFKoCJW770gTNBrm',
|
||||||
1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65',
|
1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65',
|
||||||
|
|
|
@ -1,30 +1,47 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
import { getPrivateUser } from './utils'
|
import { getUser } from './utils'
|
||||||
import { PrivateUser } from '../../common/user'
|
import { PrivateUser } from '../../common/user'
|
||||||
|
|
||||||
export const unsubscribe = functions
|
export const unsubscribe = functions
|
||||||
.runWith({ minInstances: 1 })
|
.runWith({ minInstances: 1 })
|
||||||
.https.onRequest(async (req, res) => {
|
.https.onRequest(async (req, res) => {
|
||||||
let id = req.query.id as string
|
const { id, type } = req.query as { id: string; type: string }
|
||||||
if (!id) return
|
if (!id || !type) return
|
||||||
|
|
||||||
let privateUser = await getPrivateUser(id)
|
const user = await getUser(id)
|
||||||
|
|
||||||
if (privateUser) {
|
if (user) {
|
||||||
let { username } = privateUser
|
const { name } = user
|
||||||
|
|
||||||
const update: Partial<PrivateUser> = {
|
const update: Partial<PrivateUser> = {
|
||||||
|
...(type === 'market-resolve' && {
|
||||||
unsubscribedFromResolutionEmails: true,
|
unsubscribedFromResolutionEmails: true,
|
||||||
|
}),
|
||||||
|
...(type === 'market-comment' && {
|
||||||
|
unsubscribedFromCommentEmails: true,
|
||||||
|
}),
|
||||||
|
...(type === 'market-answer' && {
|
||||||
|
unsubscribedFromAnswerEmails: true,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
await firestore.collection('private-users').doc(id).update(update)
|
await firestore.collection('private-users').doc(id).update(update)
|
||||||
|
|
||||||
|
if (type === 'market-resolve')
|
||||||
res.send(
|
res.send(
|
||||||
username +
|
`${name}, you have been unsubscribed from market resolution emails on Manifold Markets.`
|
||||||
', 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 {
|
} else {
|
||||||
res.send('This user is not currently subscribed or does not exist.')
|
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 (!contract || contract.isResolved) return 0
|
||||||
if (bet.sale || bet.isSold) 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 { Contract } from '../../common/contract'
|
||||||
import { PrivateUser, User } from '../../common/user'
|
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) => {
|
export const getValue = async <T>(collection: string, doc: string) => {
|
||||||
const snap = await admin.firestore().collection(collection).doc(doc).get()
|
const snap = await admin.firestore().collection(collection).doc(doc).get()
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import _ from 'lodash'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { formatMoney } from '../../common/util/format'
|
import { formatMoney } from '../../common/util/format'
|
||||||
import { AddFundsButton } from './add-funds-button'
|
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
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: {
|
export function AmountInput(props: {
|
||||||
amount: number | undefined
|
amount: number | undefined
|
||||||
onChange: (newAmount: number | undefined) => void
|
onChange: (newAmount: number | undefined) => void
|
||||||
error: string | undefined
|
error: string | undefined
|
||||||
setError: (error: string | undefined) => void
|
setError: (error: string | undefined) => void
|
||||||
|
contractId: string | undefined
|
||||||
minimumAmount?: number
|
minimumAmount?: number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
@ -22,6 +27,7 @@ export function AmountInput(props: {
|
||||||
onChange,
|
onChange,
|
||||||
error,
|
error,
|
||||||
setError,
|
setError,
|
||||||
|
contractId,
|
||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
inputClassName,
|
inputClassName,
|
||||||
|
@ -31,10 +37,24 @@ export function AmountInput(props: {
|
||||||
|
|
||||||
const user = useUser()
|
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) => {
|
const onAmountChange = (str: string) => {
|
||||||
|
if (str.includes('-')) {
|
||||||
|
onChange(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
const amount = parseInt(str.replace(/[^\d]/, ''))
|
const amount = parseInt(str.replace(/[^\d]/, ''))
|
||||||
|
|
||||||
if (str && isNaN(amount)) return
|
if (str && isNaN(amount)) return
|
||||||
|
if (amount >= 10 ** 9) return
|
||||||
|
|
||||||
onChange(str ? amount : undefined)
|
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 (
|
return (
|
||||||
<Col className={className}>
|
<Col className={className}>
|
||||||
|
@ -68,19 +89,34 @@ export function AmountInput(props: {
|
||||||
onChange={(e) => onAmountChange(e.target.value)}
|
onChange={(e) => onAmountChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{user && (
|
{user && (
|
||||||
<Col className="mt-3 text-sm">
|
<Col className="gap-3 text-sm">
|
||||||
<div className="mb-2 whitespace-nowrap text-gray-500">
|
{contractId && (
|
||||||
Remaining balance
|
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||||
</div>
|
<Row className="items-center gap-2">
|
||||||
<Row className="gap-4">
|
Amount loaned{' '}
|
||||||
<div>{formatMoney(Math.floor(remainingBalance))}</div>
|
<InfoTooltip
|
||||||
{user.balance !== 1000 && <AddFundsButton />}
|
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>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -30,8 +30,9 @@ export function AnswerBetPanel(props: {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
contract: Contract
|
contract: Contract
|
||||||
closePanel: () => void
|
closePanel: () => void
|
||||||
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { answer, contract, closePanel } = props
|
const { answer, contract, closePanel, className } = props
|
||||||
const { id: answerId } = answer
|
const { id: answerId } = answer
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -97,7 +98,7 @@ export function AnswerBetPanel(props: {
|
||||||
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
||||||
|
|
||||||
return (
|
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">
|
<Row className="self-stretch items-center justify-between">
|
||||||
<div className="text-xl">Buy this answer</div>
|
<div className="text-xl">Buy this answer</div>
|
||||||
|
|
||||||
|
@ -114,21 +115,21 @@ export function AnswerBetPanel(props: {
|
||||||
setError={setError}
|
setError={setError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
|
contractId={contract.id}
|
||||||
/>
|
/>
|
||||||
|
<Col className="gap-3 mt-3 w-full">
|
||||||
<Spacer h={4} />
|
<Row className="justify-between items-center text-sm">
|
||||||
|
<div className="text-gray-500">Probability</div>
|
||||||
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
|
|
||||||
<Row>
|
<Row>
|
||||||
<div>{formatPercent(initialProb)}</div>
|
<div>{formatPercent(initialProb)}</div>
|
||||||
<div className="mx-2">→</div>
|
<div className="mx-2">→</div>
|
||||||
<div>{formatPercent(resultProb)}</div>
|
<div>{formatPercent(resultProb)}</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Row className="justify-between items-start text-sm gap-2">
|
||||||
|
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
|
||||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
<div>Payout if chosen</div>
|
||||||
Payout if chosen
|
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
text={`Current payout for ${formatWithCommas(
|
text={`Current payout for ${formatWithCommas(
|
||||||
shares
|
shares
|
||||||
|
@ -137,17 +138,21 @@ export function AnswerBetPanel(props: {
|
||||||
)} shares`}
|
)} shares`}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<div>
|
<Row className="flex-wrap justify-end items-end gap-2">
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
{formatMoney(currentPayout)}
|
{formatMoney(currentPayout)}
|
||||||
<span>(+{currentReturnPercent})</span>
|
</span>
|
||||||
</div>
|
<span>(+{currentReturnPercent})</span>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
{user ? (
|
{user ? (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn',
|
'btn self-stretch',
|
||||||
betDisabled ? 'btn-disabled' : 'btn-primary',
|
betDisabled ? 'btn-disabled' : 'btn-primary',
|
||||||
isSubmitting ? 'loading' : ''
|
isSubmitting ? 'loading' : ''
|
||||||
)}
|
)}
|
||||||
|
@ -157,7 +162,7 @@ export function AnswerBetPanel(props: {
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<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}
|
onClick={firebaseLogin}
|
||||||
>
|
>
|
||||||
Sign in to trade!
|
Sign in to trade!
|
||||||
|
|
|
@ -8,13 +8,13 @@ import { Col } from '../layout/col'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
import { SiteLink } from '../site-link'
|
import { SiteLink } from '../site-link'
|
||||||
import { DateTimeTooltip } from '../datetime-tooltip'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { BuyButton } from '../yes-no-selector'
|
import { BuyButton } from '../yes-no-selector'
|
||||||
import { formatPercent } from '../../../common/util/format'
|
import { formatPercent } from '../../../common/util/format'
|
||||||
import { getOutcomeProbability } from '../../../common/calculate'
|
import { getOutcomeProbability } from '../../../common/calculate'
|
||||||
import { tradingAllowed } from '../../lib/firebase/contracts'
|
import { tradingAllowed } from '../../lib/firebase/contracts'
|
||||||
import { AnswerBetPanel } from './answer-bet-panel'
|
import { AnswerBetPanel } from './answer-bet-panel'
|
||||||
|
import { ContractFeed } from '../contract-feed'
|
||||||
|
import { Linkify } from '../linkify'
|
||||||
|
|
||||||
export function AnswerItem(props: {
|
export function AnswerItem(props: {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
|
@ -35,10 +35,9 @@ export function AnswerItem(props: {
|
||||||
onDeselect,
|
onDeselect,
|
||||||
} = props
|
} = props
|
||||||
const { resolution, resolutions, totalShares } = contract
|
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 isChosen = chosenProb !== undefined
|
||||||
|
|
||||||
const createdDate = dayjs(createdTime).format('MMM D')
|
|
||||||
const prob = getOutcomeProbability(totalShares, answer.id)
|
const prob = getOutcomeProbability(totalShares, answer.id)
|
||||||
const roundedProb = Math.round(prob * 100)
|
const roundedProb = Math.round(prob * 100)
|
||||||
const probPercent = formatPercent(prob)
|
const probPercent = formatPercent(prob)
|
||||||
|
@ -47,42 +46,50 @@ export function AnswerItem(props: {
|
||||||
|
|
||||||
const [isBetting, setIsBetting] = useState(false)
|
const [isBetting, setIsBetting] = useState(false)
|
||||||
|
|
||||||
|
const canBet = !isBetting && !showChoice && tradingAllowed(contract)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'p-4 sm:flex-row rounded gap-4',
|
'flex flex-col gap-4 rounded p-4 sm:flex-row',
|
||||||
wasResolvedTo
|
wasResolvedTo
|
||||||
? resolution === 'MKT'
|
? resolution === 'MKT'
|
||||||
? 'bg-blue-50 mb-2'
|
? 'mb-2 bg-blue-50'
|
||||||
: 'bg-green-50 mb-8'
|
: 'mb-8 bg-green-50'
|
||||||
: chosenProb === undefined
|
: chosenProb === undefined
|
||||||
? 'bg-gray-50'
|
? 'bg-gray-50'
|
||||||
: showChoice === 'radio'
|
: showChoice === 'radio'
|
||||||
? 'bg-green-50'
|
? '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">
|
<Col className="flex-1 gap-3">
|
||||||
<div className="whitespace-pre-line break-words">{text}</div>
|
<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}`}>
|
<SiteLink className="relative" href={`/${username}`}>
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
<Avatar avatarUrl={avatarUrl} size={6} />
|
<Avatar avatarUrl={avatarUrl} size={6} />
|
||||||
<div className="truncate">{name}</div>
|
<div className="truncate">{name}</div>
|
||||||
</Row>
|
</Row>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
|
{/* TODO: Show total pool? */}
|
||||||
<div className="">•</div>
|
|
||||||
|
|
||||||
<div className="whitespace-nowrap">
|
|
||||||
<DateTimeTooltip text="" time={contract.createdTime}>
|
|
||||||
{createdDate}
|
|
||||||
</DateTimeTooltip>
|
|
||||||
</div>
|
|
||||||
<div className="">•</div>
|
|
||||||
<div className="text-base">#{number}</div>
|
<div className="text-base">#{number}</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
{isBetting && (
|
||||||
|
<ContractFeed
|
||||||
|
contract={contract}
|
||||||
|
bets={[]}
|
||||||
|
comments={[]}
|
||||||
|
feedType="multi"
|
||||||
|
outcome={answer.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{isBetting ? (
|
{isBetting ? (
|
||||||
|
@ -90,13 +97,14 @@ export function AnswerItem(props: {
|
||||||
answer={answer}
|
answer={answer}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
closePanel={() => setIsBetting(false)}
|
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 &&
|
{!wasResolvedTo &&
|
||||||
(showChoice === 'checkbox' ? (
|
(showChoice === 'checkbox' ? (
|
||||||
<input
|
<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"
|
type="number"
|
||||||
placeholder={`${roundedProb}`}
|
placeholder={`${roundedProb}`}
|
||||||
maxLength={9}
|
maxLength={9}
|
||||||
|
@ -121,7 +129,7 @@ export function AnswerItem(props: {
|
||||||
))}
|
))}
|
||||||
{showChoice ? (
|
{showChoice ? (
|
||||||
<div className="form-control py-1">
|
<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>
|
<span className="">Choose this answer</span>
|
||||||
{showChoice === 'radio' && (
|
{showChoice === 'radio' && (
|
||||||
<input
|
<input
|
||||||
|
@ -162,7 +170,7 @@ export function AnswerItem(props: {
|
||||||
<>
|
<>
|
||||||
{tradingAllowed(contract) && (
|
{tradingAllowed(contract) && (
|
||||||
<BuyButton
|
<BuyButton
|
||||||
className="justify-end self-end flex-initial btn-md !px-8"
|
className="btn-md flex-initial justify-end self-end !px-8"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsBetting(true)
|
setIsBetting(true)
|
||||||
}}
|
}}
|
||||||
|
@ -188,6 +196,6 @@ export function AnswerItem(props: {
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
|
||||||
<div />
|
<div />
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'sm:flex-row gap-4',
|
'sm:flex-row sm:items-end gap-4',
|
||||||
text ? 'justify-between' : 'self-end'
|
text ? 'justify-between' : 'self-end'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -101,34 +101,42 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
|
||||||
setError={setAmountError}
|
setError={setAmountError}
|
||||||
minimumAmount={1}
|
minimumAmount={1}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
contractId={contract.id}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="gap-2 mt-1">
|
<Col className="gap-3">
|
||||||
<div className="text-sm text-gray-500">Implied probability</div>
|
<Row className="justify-between items-center text-sm">
|
||||||
|
<div className="text-gray-500">Probability</div>
|
||||||
<Row>
|
<Row>
|
||||||
<div>{formatPercent(0)}</div>
|
<div>{formatPercent(0)}</div>
|
||||||
<div className="mx-2">→</div>
|
<div className="mx-2">→</div>
|
||||||
<div>{formatPercent(resultProb)}</div>
|
<div>{formatPercent(resultProb)}</div>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
</Row>
|
||||||
Payout if chosen
|
|
||||||
|
<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
|
<InfoTooltip
|
||||||
text={`Current payout for ${formatWithCommas(
|
text={`Current payout for ${formatWithCommas(
|
||||||
shares
|
shares
|
||||||
)} / ${formatWithCommas(shares)} shares`}
|
)} / ${formatWithCommas(shares)} shares`}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<div>
|
<Row className="flex-wrap justify-end items-end gap-2">
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
{formatMoney(currentPayout)}
|
{formatMoney(currentPayout)}
|
||||||
<span>(+{currentReturnPercent})</span>
|
</span>
|
||||||
</div>
|
<span>(+{currentReturnPercent})</span>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{user ? (
|
{user ? (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn self-end mt-2',
|
'btn mt-2',
|
||||||
canSubmit ? 'btn-outline' : 'btn-disabled',
|
canSubmit ? 'btn-outline' : 'btn-disabled',
|
||||||
isSubmitting && 'loading'
|
isSubmitting && 'loading'
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -144,7 +144,6 @@ export function BetPanel(props: {
|
||||||
text={panelTitle}
|
text={panelTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <div className="mt-2 mb-1 text-sm text-gray-500">Outcome</div> */}
|
|
||||||
<YesNoSelector
|
<YesNoSelector
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
selected={betChoice}
|
selected={betChoice}
|
||||||
|
@ -160,45 +159,49 @@ export function BetPanel(props: {
|
||||||
setError={setError}
|
setError={setError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
|
contractId={contract.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Col className="gap-3 mt-3 w-full">
|
||||||
|
<Row className="justify-between items-center text-sm">
|
||||||
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
|
<div className="text-gray-500">Probability</div>
|
||||||
<Row>
|
<Row>
|
||||||
<div>{formatPercent(initialProb)}</div>
|
<div>{formatPercent(initialProb)}</div>
|
||||||
<div className="mx-2">→</div>
|
<div className="mx-2">→</div>
|
||||||
<div>{formatPercent(resultProb)}</div>
|
<div>{formatPercent(resultProb)}</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
</Row>
|
||||||
|
|
||||||
{betChoice && (
|
<Row className="justify-between items-start text-sm gap-2">
|
||||||
<>
|
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
|
||||||
<Spacer h={4} />
|
<div>
|
||||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||||
Payout if <OutcomeLabel outcome={betChoice} />
|
</div>
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
text={`Current payout for ${formatWithCommas(
|
text={`Current payout for ${formatWithCommas(
|
||||||
shares
|
shares
|
||||||
)} / ${formatWithCommas(
|
)} / ${formatWithCommas(
|
||||||
shares +
|
shares +
|
||||||
totalShares[betChoice] -
|
totalShares[betChoice ?? 'YES'] -
|
||||||
(phantomShares ? phantomShares[betChoice] : 0)
|
(phantomShares ? phantomShares[betChoice ?? 'YES'] : 0)
|
||||||
)} ${betChoice} shares`}
|
)} ${betChoice} shares`}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<div>
|
<Row className="flex-wrap justify-end items-end gap-2">
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
{formatMoney(currentPayout)}
|
{formatMoney(currentPayout)}
|
||||||
<span>(+{currentReturnPercent})</span>
|
</span>
|
||||||
</div>
|
<span>(+{currentReturnPercent})</span>
|
||||||
</>
|
</Row>
|
||||||
)}
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={8} />
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn',
|
'btn flex-1',
|
||||||
betDisabled
|
betDisabled
|
||||||
? 'btn-disabled'
|
? 'btn-disabled'
|
||||||
: betChoice === 'YES'
|
: betChoice === 'YES'
|
||||||
|
@ -213,7 +216,7 @@ export function BetPanel(props: {
|
||||||
)}
|
)}
|
||||||
{user === null && (
|
{user === null && (
|
||||||
<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 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}
|
onClick={firebaseLogin}
|
||||||
>
|
>
|
||||||
Sign in to trade!
|
Sign in to trade!
|
||||||
|
|
|
@ -37,7 +37,7 @@ import { filterDefined } from '../../common/util/array'
|
||||||
import { LoadingIndicator } from './loading-indicator'
|
import { LoadingIndicator } from './loading-indicator'
|
||||||
import { SiteLink } from './site-link'
|
import { SiteLink } from './site-link'
|
||||||
|
|
||||||
type BetSort = 'newest' | 'profit' | 'resolved' | 'value'
|
type BetSort = 'newest' | 'profit' | 'settled' | 'value'
|
||||||
|
|
||||||
export function BetsList(props: { user: User }) {
|
export function BetsList(props: { user: User }) {
|
||||||
const { user } = props
|
const { user } = props
|
||||||
|
@ -82,7 +82,8 @@ export function BetsList(props: { user: User }) {
|
||||||
if (bet.isSold || bet.sale) return 0
|
if (bet.isSold || bet.sale) return 0
|
||||||
|
|
||||||
const contract = contracts.find((c) => c.id === contractId)
|
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,
|
contracts,
|
||||||
(c) => -1 * Math.max(...contractBets[c.id].map((bet) => bet.createdTime))
|
(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))
|
sortedContracts = _.sortBy(contracts, (c) => -1 * (c.resolutionTime ?? 0))
|
||||||
|
|
||||||
const [resolved, unresolved] = _.partition(
|
const [settled, unsettled] = _.partition(
|
||||||
sortedContracts,
|
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(
|
const currentInvestment = _.sumBy(unsettled, (c) => contractsInvestment[c.id])
|
||||||
unresolved,
|
|
||||||
(c) => contractsInvestment[c.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
const currentBetsValue = _.sumBy(
|
const currentBetsValue = _.sumBy(
|
||||||
unresolved,
|
unsettled,
|
||||||
(c) => contractsCurrentValue[c.id]
|
(c) => contractsCurrentValue[c.id]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -140,7 +138,7 @@ export function BetsList(props: { user: User }) {
|
||||||
<Col>
|
<Col>
|
||||||
<div className="text-sm text-gray-500">Invested</div>
|
<div className="text-sm text-gray-500">Invested</div>
|
||||||
<div className="text-lg">
|
<div className="text-lg">
|
||||||
{formatMoney(currentBetsValue)}{' '}
|
{formatMoney(currentInvestment)}{' '}
|
||||||
<ProfitBadge profitPercent={investedProfit} />
|
<ProfitBadge profitPercent={investedProfit} />
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -160,8 +158,8 @@ export function BetsList(props: { user: User }) {
|
||||||
>
|
>
|
||||||
<option value="value">By value</option>
|
<option value="value">By value</option>
|
||||||
<option value="profit">By profit</option>
|
<option value="profit">By profit</option>
|
||||||
<option value="newest">Newest</option>
|
<option value="newest">Most recent</option>
|
||||||
<option value="resolved">Resolved</option>
|
<option value="settled">Resolved</option>
|
||||||
</select>
|
</select>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
@ -228,7 +226,7 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
|
||||||
/>
|
/>
|
||||||
</Row>
|
</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 && (
|
{isBinary && (
|
||||||
<>
|
<>
|
||||||
{resolution ? (
|
{resolution ? (
|
||||||
|
@ -361,6 +359,8 @@ export function MyBetsSummary(props: {
|
||||||
{formatMoney(expectation)}
|
{formatMoney(expectation)}
|
||||||
</div>
|
</div>
|
||||||
</Col> */}
|
</Col> */}
|
||||||
|
{isBinary && (
|
||||||
|
<>
|
||||||
<Col>
|
<Col>
|
||||||
<div className="whitespace-nowrap text-sm text-gray-500">
|
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||||
Payout if <YesLabel />
|
Payout if <YesLabel />
|
||||||
|
@ -377,6 +377,8 @@ export function MyBetsSummary(props: {
|
||||||
{formatMoney(noWinnings)}
|
{formatMoney(noWinnings)}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Col>
|
<Col>
|
||||||
<div className="whitespace-nowrap text-sm text-gray-500">
|
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||||
{isBinary ? (
|
{isBinary ? (
|
||||||
|
@ -421,9 +423,10 @@ export function ContractBetsTable(props: {
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="p-2">
|
<tr className="p-2">
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>{isResolved ? <>Payout</> : <>Sale price</>}</th>
|
|
||||||
<th>Outcome</th>
|
<th>Outcome</th>
|
||||||
<th>Amount</th>
|
<th>Amount</th>
|
||||||
|
<th>{isResolved ? <>Payout</> : <>Sale price</>}</th>
|
||||||
|
{!isResolved && <th>Payout if chosen</th>}
|
||||||
<th>Probability</th>
|
<th>Probability</th>
|
||||||
<th>Shares</th>
|
<th>Shares</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
|
@ -455,6 +458,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
shares,
|
shares,
|
||||||
isSold,
|
isSold,
|
||||||
isAnte,
|
isAnte,
|
||||||
|
loanAmount,
|
||||||
} = bet
|
} = bet
|
||||||
|
|
||||||
const { isResolved, closeTime } = contract
|
const { isResolved, closeTime } = contract
|
||||||
|
@ -462,7 +466,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
|
|
||||||
const saleAmount = saleBet?.sale?.amount
|
const saleAmount = saleBet?.sale?.amount
|
||||||
|
|
||||||
const saleDisplay = bet.isAnte ? (
|
const saleDisplay = isAnte ? (
|
||||||
'ANTE'
|
'ANTE'
|
||||||
) : saleAmount !== undefined ? (
|
) : saleAmount !== undefined ? (
|
||||||
<>{formatMoney(saleAmount)} (sold)</>
|
<>{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 (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="text-neutral">
|
<td className="text-neutral">
|
||||||
|
@ -481,11 +490,15 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
<SellButton contract={contract} bet={bet} />
|
<SellButton contract={contract} bet={bet} />
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{saleDisplay}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<OutcomeLabel outcome={outcome} />
|
<OutcomeLabel outcome={outcome} />
|
||||||
</td>
|
</td>
|
||||||
<td>{formatMoney(amount)}</td>
|
<td>
|
||||||
|
{formatMoney(amount)}
|
||||||
|
{loanAmount ? ` (${formatMoney(loanAmount ?? 0)} loan)` : ''}
|
||||||
|
</td>
|
||||||
|
<td>{saleDisplay}</td>
|
||||||
|
{!isResolved && <td>{payoutIfChosenDisplay}</td>}
|
||||||
<td>
|
<td>
|
||||||
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
||||||
</td>
|
</td>
|
||||||
|
@ -502,17 +515,19 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { contract, bet } = props
|
const { contract, bet } = props
|
||||||
|
const { outcome, shares, loanAmount } = bet
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
const initialProb = getOutcomeProbability(
|
const initialProb = getOutcomeProbability(
|
||||||
contract.totalShares,
|
contract.totalShares,
|
||||||
bet.outcome === 'NO' ? 'YES' : bet.outcome
|
outcome === 'NO' ? 'YES' : outcome
|
||||||
)
|
)
|
||||||
|
|
||||||
const outcomeProb = getProbabilityAfterSale(
|
const outcomeProb = getProbabilityAfterSale(
|
||||||
contract.totalShares,
|
contract.totalShares,
|
||||||
bet.outcome,
|
outcome,
|
||||||
bet.shares
|
shares
|
||||||
)
|
)
|
||||||
|
|
||||||
const saleAmount = calculateSaleAmount(contract, bet)
|
const saleAmount = calculateSaleAmount(contract, bet)
|
||||||
|
@ -524,7 +539,7 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
||||||
label: 'Sell',
|
label: 'Sell',
|
||||||
}}
|
}}
|
||||||
submitBtn={{ className: 'btn-primary' }}
|
submitBtn={{ className: 'btn-primary', label: 'Sell' }}
|
||||||
onSubmit={async () => {
|
onSubmit={async () => {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
await sellBet({ contractId: contract.id, betId: bet.id })
|
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">
|
<div className="mb-4 text-2xl">
|
||||||
Sell <OutcomeLabel outcome={bet.outcome} />
|
Sell {formatWithCommas(shares)} shares of{' '}
|
||||||
|
<OutcomeLabel outcome={outcome} /> for {formatMoney(saleAmount)}?
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{!!loanAmount && (
|
||||||
Do you want to sell {formatWithCommas(bet.shares)} shares of{' '}
|
<div className="mt-2">
|
||||||
<OutcomeLabel outcome={bet.outcome} /> for {formatMoney(saleAmount)}?
|
You will also pay back {formatMoney(loanAmount)} of your loan, for a
|
||||||
|
net of {formatMoney(saleAmount - loanAmount)}.
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-2 mb-1 text-sm text-gray-500">
|
<div className="mt-2 mb-1 text-sm">
|
||||||
Implied probability: {formatPercent(initialProb)} →{' '}
|
Market probability: {formatPercent(initialProb)} →{' '}
|
||||||
{formatPercent(outcomeProb)}
|
{formatPercent(outcomeProb)}
|
||||||
</div>
|
</div>
|
||||||
</ConfirmationButton>
|
</ConfirmationButton>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { fromNow } from '../lib/util/time'
|
||||||
import { Avatar } from './avatar'
|
import { Avatar } from './avatar'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { TweetButton } from './tweet-button'
|
||||||
|
|
||||||
export function ContractCard(props: {
|
export function ContractCard(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -149,7 +150,8 @@ function AbbrContractDetails(props: {
|
||||||
) : showCloseTime ? (
|
) : showCloseTime ? (
|
||||||
<Row className="gap-1">
|
<Row className="gap-1">
|
||||||
<ClockIcon className="h-5 w-5" />
|
<ClockIcon className="h-5 w-5" />
|
||||||
Closes {fromNow(closeTime || 0)}
|
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
|
||||||
|
{fromNow(closeTime || 0)}
|
||||||
</Row>
|
</Row>
|
||||||
) : (
|
) : (
|
||||||
<Row className="gap-1">
|
<Row className="gap-1">
|
||||||
|
@ -170,6 +172,8 @@ export function ContractDetails(props: {
|
||||||
const { closeTime, creatorName, creatorUsername } = contract
|
const { closeTime, creatorName, creatorUsername } = contract
|
||||||
const { truePool, createdDate, resolvedDate } = contractMetrics(contract)
|
const { truePool, createdDate, resolvedDate } = contractMetrics(contract)
|
||||||
|
|
||||||
|
const tweetText = getTweetText(contract, !!isCreator)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-2 text-sm text-gray-500 sm:flex-row sm:flex-wrap">
|
<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">
|
<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>
|
<div className="whitespace-nowrap">{formatMoney(truePool)} pool</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
<TweetButton className={'self-end'} tweetText={tweetText} />
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
@ -307,9 +313,28 @@ function EditableCloseDate(props: {
|
||||||
className="btn btn-xs btn-ghost"
|
className="btn btn-xs btn-ghost"
|
||||||
onClick={() => setIsEditingCloseTime(true)}
|
onClick={() => setIsEditingCloseTime(true)}
|
||||||
>
|
>
|
||||||
<PencilIcon className="inline h-4 w-4 mr-2" /> Edit
|
<PencilIcon className="mr-2 inline h-4 w-4" /> Edit
|
||||||
</button>
|
</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: {
|
function FeedComment(props: {
|
||||||
activityItem: any
|
activityItem: any
|
||||||
moreHref: string
|
moreHref: string
|
||||||
feedType: 'activity' | 'market'
|
feedType: FeedType
|
||||||
}) {
|
}) {
|
||||||
const { activityItem, moreHref, feedType } = props
|
const { activityItem, moreHref, feedType } = props
|
||||||
const { person, text, amount, outcome, createdTime } = activityItem
|
const { person, text, amount, outcome, createdTime } = activityItem
|
||||||
|
@ -65,7 +65,8 @@ function FeedComment(props: {
|
||||||
username={person.username}
|
username={person.username}
|
||||||
name={person.name}
|
name={person.name}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{bought} {money} of <OutcomeLabel outcome={outcome} />{' '}
|
{bought} {money}
|
||||||
|
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />
|
||||||
<Timestamp time={createdTime} />
|
<Timestamp time={createdTime} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,8 +91,8 @@ function Timestamp(props: { time: number }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FeedBet(props: { activityItem: any }) {
|
function FeedBet(props: { activityItem: any; feedType: FeedType }) {
|
||||||
const { activityItem } = props
|
const { activityItem, feedType } = props
|
||||||
const { id, contractId, amount, outcome, createdTime } = activityItem
|
const { id, contractId, amount, outcome, createdTime } = activityItem
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const isSelf = user?.id == activityItem.userId
|
const isSelf = user?.id == activityItem.userId
|
||||||
|
@ -122,8 +123,9 @@ function FeedBet(props: { activityItem: any }) {
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1 py-1.5">
|
<div className="min-w-0 flex-1 py-1.5">
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
<span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money} of{' '}
|
<span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money}
|
||||||
<OutcomeLabel outcome={outcome} /> <Timestamp time={createdTime} />
|
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />
|
||||||
|
<Timestamp time={createdTime} />
|
||||||
{canComment && (
|
{canComment && (
|
||||||
// Allow user to comment in an textarea if they are the creator
|
// Allow user to comment in an textarea if they are the creator
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
|
@ -134,7 +136,7 @@ function FeedBet(props: { activityItem: any }) {
|
||||||
placeholder="Add a comment..."
|
placeholder="Add a comment..."
|
||||||
rows={3}
|
rows={3}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && e.ctrlKey) {
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||||
submitComment()
|
submitComment()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -179,7 +181,7 @@ function EditContract(props: {
|
||||||
e.target.setSelectionRange(text.length, text.length)
|
e.target.setSelectionRange(text.length, text.length)
|
||||||
}
|
}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && e.ctrlKey) {
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||||
onSave(text)
|
onSave(text)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -305,15 +307,13 @@ function FeedQuestion(props: { contract: Contract }) {
|
||||||
const { truePool } = contractMetrics(contract)
|
const { truePool } = contractMetrics(contract)
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
// Currently hidden on mobile; ideally we'd fit this in somewhere.
|
|
||||||
const closeMessage =
|
const closeMessage =
|
||||||
contract.isResolved || !contract.closeTime ? null : (
|
contract.isResolved || !contract.closeTime ? null : (
|
||||||
<span className="float-right hidden text-gray-400 sm:inline">
|
<>
|
||||||
{formatMoney(truePool)} pool
|
|
||||||
<span className="mx-2">•</span>
|
<span className="mx-2">•</span>
|
||||||
{contract.closeTime > Date.now() ? 'Closes' : 'Closed'}
|
{contract.closeTime > Date.now() ? 'Closes' : 'Closed'}
|
||||||
<Timestamp time={contract.closeTime || 0} />
|
<Timestamp time={contract.closeTime || 0} />
|
||||||
</span>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -330,7 +330,11 @@ function FeedQuestion(props: { contract: Contract }) {
|
||||||
username={creatorUsername}
|
username={creatorUsername}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
asked
|
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}
|
{closeMessage}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Col className="mb-4 items-start justify-between gap-2 sm:flex-row sm:gap-4">
|
<Col className="mb-4 items-start justify-between gap-2 sm:flex-row sm:gap-4">
|
||||||
<SiteLink
|
<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 }) {
|
function OutcomeIcon(props: { outcome?: string }) {
|
||||||
const { outcome } = props
|
const { outcome } = props
|
||||||
switch (outcome) {
|
switch (outcome) {
|
||||||
|
@ -538,8 +565,12 @@ function groupBets(
|
||||||
return items as ActivityItem[]
|
return items as ActivityItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function BetGroupSpan(props: { bets: Bet[]; outcome: string }) {
|
function BetGroupSpan(props: {
|
||||||
const { bets, outcome } = props
|
bets: Bet[]
|
||||||
|
outcome: string
|
||||||
|
feedType: FeedType
|
||||||
|
}) {
|
||||||
|
const { bets, outcome, feedType } = props
|
||||||
|
|
||||||
const numberTraders = _.uniqBy(bets, (b) => b.userId).length
|
const numberTraders = _.uniqBy(bets, (b) => b.userId).length
|
||||||
|
|
||||||
|
@ -554,14 +585,14 @@ function BetGroupSpan(props: { bets: Bet[]; outcome: string }) {
|
||||||
{buyTotal > 0 && <>bought {formatMoney(buyTotal)} </>}
|
{buyTotal > 0 && <>bought {formatMoney(buyTotal)} </>}
|
||||||
{sellTotal > 0 && <>sold {formatMoney(sellTotal)} </>}
|
{sellTotal > 0 && <>sold {formatMoney(sellTotal)} </>}
|
||||||
</JoinSpans>
|
</JoinSpans>
|
||||||
of <OutcomeLabel outcome={outcome} />
|
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />{' '}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make this expandable to show all grouped bets?
|
// TODO: Make this expandable to show all grouped bets?
|
||||||
function FeedBetGroup(props: { activityItem: any }) {
|
function FeedBetGroup(props: { activityItem: any; feedType: FeedType }) {
|
||||||
const { activityItem } = props
|
const { activityItem, feedType } = props
|
||||||
const bets: Bet[] = activityItem.bets
|
const bets: Bet[] = activityItem.bets
|
||||||
|
|
||||||
const betGroups = _.groupBy(bets, (bet) => bet.outcome)
|
const betGroups = _.groupBy(bets, (bet) => bet.outcome)
|
||||||
|
@ -583,7 +614,11 @@ function FeedBetGroup(props: { activityItem: any }) {
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{outcomes.map((outcome, index) => (
|
{outcomes.map((outcome, index) => (
|
||||||
<Fragment key={outcome}>
|
<Fragment key={outcome}>
|
||||||
<BetGroupSpan outcome={outcome} bets={betGroups[outcome]} />
|
<BetGroupSpan
|
||||||
|
outcome={outcome}
|
||||||
|
bets={betGroups[outcome]}
|
||||||
|
feedType={feedType}
|
||||||
|
/>
|
||||||
{index !== outcomes.length - 1 && <br />}
|
{index !== outcomes.length - 1 && <br />}
|
||||||
</Fragment>
|
</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:
|
// Missing feed items:
|
||||||
// - Bet sold?
|
// - Bet sold?
|
||||||
type ActivityItem = {
|
type ActivityItem = {
|
||||||
|
@ -635,15 +682,23 @@ type ActivityItem = {
|
||||||
| 'expand'
|
| '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: {
|
export function ContractFeed(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
// Feed types: 'activity' = Activity feed, 'market' = Comments feed on a market
|
feedType: FeedType
|
||||||
feedType: 'activity' | 'market'
|
outcome?: string // Which multi-category outcome to filter
|
||||||
betRowClassName?: string
|
betRowClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, feedType, betRowClassName } = props
|
const { contract, feedType, outcome, betRowClassName } = props
|
||||||
const { id, outcomeType } = contract
|
const { id, outcomeType } = contract
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
|
@ -655,6 +710,10 @@ export function ContractFeed(props: {
|
||||||
? bets.filter((bet) => !bet.isAnte)
|
? bets.filter((bet) => !bet.isAnte)
|
||||||
: bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
|
: 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 comments = useComments(id) ?? props.comments
|
||||||
|
|
||||||
const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
|
const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
|
||||||
|
@ -669,6 +728,10 @@ export function ContractFeed(props: {
|
||||||
if (contract.resolution) {
|
if (contract.resolution) {
|
||||||
allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
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
|
// If there are more than 5 items, only show the first, an expand item, and last 3
|
||||||
let items = allItems
|
let items = allItems
|
||||||
|
@ -682,10 +745,9 @@ export function ContractFeed(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flow-root pr-2 md:pr-0">
|
<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) => (
|
{items.map((activityItem, activityItemIdx) => (
|
||||||
<li key={activityItem.id}>
|
<div key={activityItem.id} className="relative pb-8">
|
||||||
<div className="relative pb-8">
|
|
||||||
{activityItemIdx !== items.length - 1 ? (
|
{activityItemIdx !== items.length - 1 ? (
|
||||||
<span
|
<span
|
||||||
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
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}
|
) : null}
|
||||||
<div className="relative flex items-start space-x-3">
|
<div className="relative flex items-start space-x-3">
|
||||||
{activityItem.type === 'start' ? (
|
{activityItem.type === 'start' ? (
|
||||||
feedType == 'activity' ? (
|
feedType === 'activity' ? (
|
||||||
<FeedQuestion contract={contract} />
|
<FeedQuestion contract={contract} />
|
||||||
) : (
|
) : feedType === 'market' ? (
|
||||||
<FeedDescription contract={contract} />
|
<FeedDescription contract={contract} />
|
||||||
)
|
) : feedType === 'multi' ? (
|
||||||
|
<FeedAnswer contract={contract} outcome={outcome || '0'} />
|
||||||
|
) : null
|
||||||
) : activityItem.type === 'comment' ? (
|
) : activityItem.type === 'comment' ? (
|
||||||
<FeedComment
|
<FeedComment
|
||||||
activityItem={activityItem}
|
activityItem={activityItem}
|
||||||
|
@ -706,9 +770,9 @@ export function ContractFeed(props: {
|
||||||
feedType={feedType}
|
feedType={feedType}
|
||||||
/>
|
/>
|
||||||
) : activityItem.type === 'bet' ? (
|
) : activityItem.type === 'bet' ? (
|
||||||
<FeedBet activityItem={activityItem} />
|
<FeedBet activityItem={activityItem} feedType={feedType} />
|
||||||
) : activityItem.type === 'betgroup' ? (
|
) : activityItem.type === 'betgroup' ? (
|
||||||
<FeedBetGroup activityItem={activityItem} />
|
<FeedBetGroup activityItem={activityItem} feedType={feedType} />
|
||||||
) : activityItem.type === 'close' ? (
|
) : activityItem.type === 'close' ? (
|
||||||
<FeedClose contract={contract} />
|
<FeedClose contract={contract} />
|
||||||
) : activityItem.type === 'resolve' ? (
|
) : activityItem.type === 'resolve' ? (
|
||||||
|
@ -718,9 +782,32 @@ export function ContractFeed(props: {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</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) && (
|
{isBinary && tradingAllowed(contract) && (
|
||||||
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
deleteContract,
|
deleteContract,
|
||||||
contractPath,
|
|
||||||
tradingAllowed,
|
tradingAllowed,
|
||||||
getBinaryProbPercent,
|
|
||||||
} from '../lib/firebase/contracts'
|
} from '../lib/firebase/contracts'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
|
@ -15,7 +13,6 @@ import { Linkify } from './linkify'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { ContractDetails, ResolutionOrChance } from './contract-card'
|
import { ContractDetails, ResolutionOrChance } from './contract-card'
|
||||||
import { ContractFeed } from './contract-feed'
|
import { ContractFeed } from './contract-feed'
|
||||||
import { TweetButton } from './tweet-button'
|
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { Comment } from '../../common/comment'
|
import { Comment } from '../../common/comment'
|
||||||
import { RevealableTagsInput, TagsInput } from './tags-input'
|
import { RevealableTagsInput, TagsInput } from './tags-input'
|
||||||
|
@ -38,8 +35,6 @@ export const ContractOverview = (props: {
|
||||||
const isCreator = user?.id === creatorId
|
const isCreator = user?.id === creatorId
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
const tweetText = getTweetText(contract, isCreator)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={clsx('mb-6', className)}>
|
<Col className={clsx('mb-6', className)}>
|
||||||
<Row className="justify-between gap-4 px-2">
|
<Row className="justify-between gap-4 px-2">
|
||||||
|
@ -92,11 +87,9 @@ export const ContractOverview = (props: {
|
||||||
) : (
|
) : (
|
||||||
<FoldTagList folds={folds} />
|
<FoldTagList folds={folds} />
|
||||||
)}
|
)}
|
||||||
<TweetButton tweetText={tweetText} />
|
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Col className="mt-6 gap-4 sm:hidden">
|
<Col className="mt-6 gap-4 sm:hidden">
|
||||||
<TweetButton className="self-end" tweetText={tweetText} />
|
|
||||||
{folds.length === 0 ? (
|
{folds.length === 0 ? (
|
||||||
<TagsInput contract={contract} />
|
<TagsInput contract={contract} />
|
||||||
) : (
|
) : (
|
||||||
|
@ -136,22 +129,3 @@ export const ContractOverview = (props: {
|
||||||
</Col>
|
</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),
|
format: (time) => formatTime(+time, lessThanAWeek),
|
||||||
}}
|
}}
|
||||||
colors={{ datum: 'color' }}
|
colors={{ datum: 'color' }}
|
||||||
pointSize={10}
|
pointSize={bets.length > 100 ? 0 : 10}
|
||||||
pointBorderWidth={1}
|
pointBorderWidth={1}
|
||||||
pointBorderColor="#fff"
|
pointBorderColor="#fff"
|
||||||
enableSlices="x"
|
enableSlices="x"
|
||||||
|
|
|
@ -205,16 +205,19 @@ export function SearchableGrid(props: {
|
||||||
}) {
|
}) {
|
||||||
const { contracts, query, setQuery, sort, setSort, byOneCreator } = props
|
const { contracts, query, setQuery, sort, setSort, byOneCreator } = props
|
||||||
|
|
||||||
|
const queryWords = query.toLowerCase().split(' ')
|
||||||
function check(corpus: String) {
|
function check(corpus: String) {
|
||||||
return corpus.toLowerCase().includes(query.toLowerCase())
|
return queryWords.every((word) => corpus.toLowerCase().includes(word))
|
||||||
}
|
}
|
||||||
|
|
||||||
let matches = contracts.filter(
|
let matches = contracts.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
check(c.question) ||
|
check(c.question) ||
|
||||||
check(c.description) ||
|
check(c.description) ||
|
||||||
check(c.creatorName) ||
|
check(c.creatorName) ||
|
||||||
check(c.creatorUsername) ||
|
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') {
|
if (sort === 'newest' || sort === 'all') {
|
||||||
|
@ -226,11 +229,13 @@ export function SearchableGrid(props: {
|
||||||
)
|
)
|
||||||
} else if (sort === 'oldest') {
|
} else if (sort === 'oldest') {
|
||||||
matches.sort((a, b) => a.createdTime - b.createdTime)
|
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, ({ volume24Hours }) => -1 * volume24Hours)
|
||||||
matches = _.sortBy(matches, (contract) => contract.closeTime)
|
matches = _.sortBy(matches, (contract) => contract.closeTime)
|
||||||
// Hide contracts that have already closed
|
const hideClosed = sort === 'closed'
|
||||||
matches = matches.filter(({ closeTime }) => (closeTime || 0) > Date.now())
|
matches = matches.filter(
|
||||||
|
({ closeTime }) => closeTime && closeTime > Date.now() !== hideClosed
|
||||||
|
)
|
||||||
} else if (sort === 'most-traded') {
|
} else if (sort === 'most-traded') {
|
||||||
matches.sort(
|
matches.sort(
|
||||||
(a, b) => contractMetrics(b).truePool - contractMetrics(a).truePool
|
(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="most-traded">Most traded</option>
|
||||||
<option value="24-hour-vol">24h volume</option>
|
<option value="24-hour-vol">24h volume</option>
|
||||||
<option value="close-date">Closing soon</option>
|
<option value="close-date">Closing soon</option>
|
||||||
|
<option value="closed">Closed</option>
|
||||||
<option value="newest">Newest</option>
|
<option value="newest">Newest</option>
|
||||||
<option value="oldest">Oldest</option>
|
<option value="oldest">Oldest</option>
|
||||||
|
|
||||||
|
@ -286,7 +292,7 @@ export function SearchableGrid(props: {
|
||||||
) : (
|
) : (
|
||||||
<ContractsGrid
|
<ContractsGrid
|
||||||
contracts={matches}
|
contracts={matches}
|
||||||
showCloseTime={sort == 'close-date'}
|
showCloseTime={['close-date', 'closed'].includes(sort)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -101,7 +101,7 @@ export const FastFoldFollowing = (props: {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spacer h={10} />
|
<Spacer h={5} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,10 +34,6 @@ function getNavigationOptions(
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
href: user ? '/home' : '/',
|
href: user ? '/home' : '/',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: `Your profile`,
|
|
||||||
href: `/${user?.username}`,
|
|
||||||
},
|
|
||||||
...(mobile
|
...(mobile
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
@ -50,10 +46,18 @@ function getNavigationOptions(
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
{
|
||||||
|
name: `Your profile`,
|
||||||
|
href: `/${user?.username}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Your trades',
|
name: 'Your trades',
|
||||||
href: '/trades',
|
href: '/trades',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Add funds',
|
||||||
|
href: '/add-funds',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Leaderboards',
|
name: 'Leaderboards',
|
||||||
href: '/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 = () => {
|
export const useAdmin = () => {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const adminIds = [
|
const privateUser = usePrivateUser(user?.id)
|
||||||
'igi2zGXsfxYPgB0DJTXVJVmwCOr2', // Austin
|
return isAdmin(privateUser?.email || '')
|
||||||
'5LZ4LgYuySdL1huCWe7bti02ghx2', // James
|
|
||||||
'tlmGNz9kjXc2EteizMORes4qvWl2', // Stephen
|
|
||||||
'IPTOzEqrpkWmEzh6hwvAyY9PqFb2', // Manifold
|
|
||||||
]
|
|
||||||
return adminIds.includes(user?.id || '')
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import {
|
import {
|
||||||
Bet,
|
Bet,
|
||||||
|
getRecentBets,
|
||||||
listenForBets,
|
listenForBets,
|
||||||
listenForRecentBets,
|
listenForRecentBets,
|
||||||
withoutAnteBets,
|
withoutAnteBets,
|
||||||
|
@ -36,3 +37,11 @@ export const useRecentBets = () => {
|
||||||
useEffect(() => listenForRecentBets(setRecentBets), [])
|
useEffect(() => listenForRecentBets(setRecentBets), [])
|
||||||
return recentBets
|
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 { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
|
listenForActiveContracts,
|
||||||
listenForContracts,
|
listenForContracts,
|
||||||
listenForHotContracts,
|
listenForHotContracts,
|
||||||
|
listenForInactiveContracts,
|
||||||
} from '../lib/firebase/contracts'
|
} from '../lib/firebase/contracts'
|
||||||
import { listenForTaggedContracts } from '../lib/firebase/folds'
|
import { listenForTaggedContracts } from '../lib/firebase/folds'
|
||||||
|
|
||||||
|
@ -17,6 +19,26 @@ export const useContracts = () => {
|
||||||
return contracts
|
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[]) => {
|
export const useUpdatedContracts = (initialContracts: Contract[]) => {
|
||||||
const [contracts, setContracts] = useState(initialContracts)
|
const [contracts, setContracts] = useState(initialContracts)
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
|
|
||||||
import { Fold } from '../../common/fold'
|
import { Fold } from '../../common/fold'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { filterDefined } from '../../common/util/array'
|
import { filterDefined } from '../../common/util/array'
|
||||||
import { Bet, getRecentBets } from '../lib/firebase/bets'
|
import { Bet, getRecentBets } from '../lib/firebase/bets'
|
||||||
import { Comment, getRecentComments } from '../lib/firebase/comments'
|
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 { listAllFolds } from '../lib/firebase/folds'
|
||||||
import { findActiveContracts } from '../pages/activity'
|
import { findActiveContracts } from '../pages/activity'
|
||||||
import { useRecentBets } from './use-bets'
|
import { useInactiveContracts } from './use-contracts'
|
||||||
import { useRecentComments } from './use-comments'
|
|
||||||
import { useContracts } from './use-contracts'
|
|
||||||
import { useFollowedFolds } from './use-fold'
|
import { useFollowedFolds } from './use-fold'
|
||||||
import { useUserBetContracts } from './use-user-bets'
|
import { useUserBetContracts } from './use-user-bets'
|
||||||
|
|
||||||
// used in static props
|
// used in static props
|
||||||
export const getAllContractInfo = async () => {
|
export const getAllContractInfo = async () => {
|
||||||
let [contracts, folds] = await Promise.all([
|
let [contracts, folds] = await Promise.all([
|
||||||
listAllContracts().catch((_) => []),
|
getActiveContracts().catch((_) => []),
|
||||||
listAllFolds().catch(() => []),
|
listAllFolds().catch(() => []),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -30,25 +28,15 @@ export const getAllContractInfo = async () => {
|
||||||
return { contracts, recentBets, recentComments, folds }
|
return { contracts, recentBets, recentComments, folds }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useActiveContracts = (
|
export const useFilterYourContracts = (
|
||||||
props: {
|
user: User | undefined | null,
|
||||||
|
folds: Fold[],
|
||||||
contracts: Contract[]
|
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 followedFoldIds = useFollowedFolds(user)
|
||||||
|
|
||||||
const followedFolds = filterDefined(
|
const followedFolds = filterDefined(
|
||||||
(followedFoldIds ?? []).map((id) =>
|
(followedFoldIds ?? []).map((id) => folds.find((fold) => fold.id === id))
|
||||||
props.folds.find((fold) => fold.id === id)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Save the initial followed fold slugs.
|
// Save the initial followed fold slugs.
|
||||||
|
@ -67,23 +55,35 @@ export const useActiveContracts = (
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
// Show no contracts before your info is loaded.
|
// Show no contracts before your info is loaded.
|
||||||
let feedContracts: Contract[] = []
|
let yourContracts: Contract[] = []
|
||||||
if (yourBetContracts && followedFoldIds) {
|
if (yourBetContracts && followedFoldIds) {
|
||||||
// Show all contracts if no folds are followed.
|
// Show all contracts if no folds are followed.
|
||||||
if (followedFoldIds.length === 0) feedContracts = contracts
|
if (followedFoldIds.length === 0) yourContracts = contracts
|
||||||
else
|
else
|
||||||
feedContracts = contracts.filter(
|
yourContracts = contracts.filter(
|
||||||
(contract) =>
|
(contract) =>
|
||||||
contract.lowercaseTags.some((tag) => tagSet.has(tag)) ||
|
contract.lowercaseTags.some((tag) => tagSet.has(tag)) ||
|
||||||
yourBetContracts.has(contract.id)
|
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(
|
const activeContracts = findActiveContracts(
|
||||||
feedContracts,
|
contracts,
|
||||||
recentComments,
|
recentComments,
|
||||||
recentBets,
|
recentBets
|
||||||
365
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const betsByContract = _.groupBy(recentBets, (bet) => bet.contractId)
|
const betsByContract = _.groupBy(recentBets, (bet) => bet.contractId)
|
||||||
|
@ -105,6 +105,24 @@ export const useActiveContracts = (
|
||||||
activeContracts,
|
activeContracts,
|
||||||
activeBets,
|
activeBets,
|
||||||
activeComments,
|
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 { useRouter } from 'next/router'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
export type Sort =
|
export type Sort =
|
||||||
| 'creator'
|
| 'creator'
|
||||||
|
@ -8,6 +10,7 @@ export type Sort =
|
||||||
| 'most-traded'
|
| 'most-traded'
|
||||||
| '24-hour-vol'
|
| '24-hour-vol'
|
||||||
| 'close-date'
|
| 'close-date'
|
||||||
|
| 'closed'
|
||||||
| 'resolved'
|
| 'resolved'
|
||||||
| 'all'
|
| 'all'
|
||||||
|
|
||||||
|
@ -24,19 +27,34 @@ export function useQueryAndSortParams(options?: { defaultSort: Sort }) {
|
||||||
router.push(router, undefined, { shallow: true })
|
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) {
|
if (query) {
|
||||||
router.query.q = query
|
router.query.q = query
|
||||||
} else {
|
} else {
|
||||||
delete router.query.q
|
delete router.query.q
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push(router, undefined, { shallow: true })
|
router.push(router, undefined, { shallow: true })
|
||||||
|
}, 500),
|
||||||
|
[router]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setQuery = (query: string | undefined) => {
|
||||||
|
setQueryState(query)
|
||||||
|
pushQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sort: sort ?? options?.defaultSort ?? '24-hour-vol',
|
sort: sort ?? options?.defaultSort ?? '24-hour-vol',
|
||||||
query: query ?? '',
|
query: queryState ?? '',
|
||||||
setSort,
|
setSort,
|
||||||
setQuery,
|
setQuery,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { useEffect, useState } from 'react'
|
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) => {
|
export const useUserBets = (userId: string | undefined) => {
|
||||||
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
|
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
|
||||||
|
@ -12,6 +16,20 @@ export const useUserBets = (userId: string | undefined) => {
|
||||||
return bets
|
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) => {
|
export const useUserBetContracts = (userId: string | undefined) => {
|
||||||
const [contractIds, setContractIds] = useState<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[]) {
|
export function withoutAnteBets(contract: Contract, bets?: Bet[]) {
|
||||||
const { createdTime } = contract
|
const { createdTime } = contract
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,41 @@ export function listenForContracts(
|
||||||
return listenForValues<Contract>(q, setContracts)
|
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(
|
export function listenForContract(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
setContract: (contract: Contract | null) => void
|
setContract: (contract: Contract | null) => void
|
||||||
|
|
|
@ -54,13 +54,12 @@ export async function getFoldBySlug(slug: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function contractsByTagsQuery(tags: 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(
|
return query(
|
||||||
contractCollection,
|
contractCollection,
|
||||||
where(
|
where('lowercaseTags', 'array-contains-any', lowercaseTags)
|
||||||
'lowercaseTags',
|
|
||||||
'array-contains-any',
|
|
||||||
tags.map((tag) => tag.toLowerCase())
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +73,6 @@ export async function getFoldContracts(fold: Fold) {
|
||||||
} = fold
|
} = fold
|
||||||
|
|
||||||
const [tagsContracts, includedContracts] = await Promise.all([
|
const [tagsContracts, includedContracts] = await Promise.all([
|
||||||
// TODO: if tags.length > 10, execute multiple parallel queries
|
|
||||||
tags.length > 0 ? getValues<Contract>(contractsByTagsQuery(tags)) : [],
|
tags.length > 0 ? getValues<Contract>(contractsByTagsQuery(tags)) : [],
|
||||||
|
|
||||||
// TODO: if contractIds.length > 10, execute multiple parallel queries
|
// TODO: if contractIds.length > 10, execute multiple parallel queries
|
||||||
|
@ -163,9 +161,10 @@ export function listenForFollow(
|
||||||
export async function getFoldsByTags(tags: string[]) {
|
export async function getFoldsByTags(tags: string[]) {
|
||||||
if (tags.length === 0) return []
|
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.
|
// 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(
|
query(
|
||||||
foldCollection,
|
foldCollection,
|
||||||
where('lowercaseTags', 'array-contains-any', lowercaseTags)
|
where('lowercaseTags', 'array-contains-any', lowercaseTags)
|
||||||
|
@ -179,10 +178,10 @@ export function listenForFoldsWithTags(
|
||||||
tags: string[],
|
tags: string[],
|
||||||
setFolds: (folds: Fold[]) => void
|
setFolds: (folds: Fold[]) => void
|
||||||
) {
|
) {
|
||||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
|
||||||
const q =
|
|
||||||
// TODO: split into multiple queries if tags.length > 10.
|
// TODO: split into multiple queries if tags.length > 10.
|
||||||
query(
|
const lowercaseTags = tags.map((tag) => tag.toLowerCase()).slice(0, 10)
|
||||||
|
|
||||||
|
const q = query(
|
||||||
foldCollection,
|
foldCollection,
|
||||||
where('lowercaseTags', 'array-contains-any', lowercaseTags)
|
where('lowercaseTags', 'array-contains-any', lowercaseTags)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { getFirestore } from '@firebase/firestore'
|
import { getFirestore } from '@firebase/firestore'
|
||||||
import { initializeApp, getApps, getApp } from 'firebase/app'
|
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'
|
export const isProd = process.env.NEXT_PUBLIC_FIREBASE_ENV !== 'DEV'
|
||||||
|
|
||||||
const firebaseConfig = isProd
|
const FIREBASE_CONFIGS = {
|
||||||
? {
|
PROD: {
|
||||||
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
||||||
authDomain: 'mantic-markets.firebaseapp.com',
|
authDomain: 'mantic-markets.firebaseapp.com',
|
||||||
projectId: 'mantic-markets',
|
projectId: 'mantic-markets',
|
||||||
|
@ -12,8 +13,8 @@ const firebaseConfig = isProd
|
||||||
messagingSenderId: '128925704902',
|
messagingSenderId: '128925704902',
|
||||||
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
||||||
measurementId: 'G-SSFK1Q138D',
|
measurementId: 'G-SSFK1Q138D',
|
||||||
}
|
},
|
||||||
: {
|
DEV: {
|
||||||
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
||||||
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
||||||
projectId: 'dev-mantic-markets',
|
projectId: 'dev-mantic-markets',
|
||||||
|
@ -21,8 +22,20 @@ const firebaseConfig = isProd
|
||||||
messagingSenderId: '134303100058',
|
messagingSenderId: '134303100058',
|
||||||
appId: '1:134303100058:web:27f9ea8b83347251f80323',
|
appId: '1:134303100058:web:27f9ea8b83347251f80323',
|
||||||
measurementId: 'G-YJC9E37P37',
|
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
|
// Initialize Firebase
|
||||||
export const app = getApps().length ? getApp() : initializeApp(firebaseConfig)
|
export const app = getApps().length ? getApp() : initializeApp(firebaseConfig)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
import {
|
import {
|
||||||
doc,
|
|
||||||
getDoc,
|
getDoc,
|
||||||
getDocs,
|
getDocs,
|
||||||
onSnapshot,
|
onSnapshot,
|
||||||
|
@ -22,7 +21,9 @@ export function listenForValue<T>(
|
||||||
docRef: DocumentReference,
|
docRef: DocumentReference,
|
||||||
setValue: (value: T | null) => void
|
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
|
if (snapshot.metadata.fromCache) return
|
||||||
|
|
||||||
const value = snapshot.exists() ? (snapshot.data() as T) : null
|
const value = snapshot.exists() ? (snapshot.data() as T) : null
|
||||||
|
@ -34,7 +35,9 @@ export function listenForValues<T>(
|
||||||
query: Query,
|
query: Query,
|
||||||
setValues: (values: T[]) => void
|
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
|
if (snapshot.metadata.fromCache) return
|
||||||
|
|
||||||
const values = snapshot.docs.map((doc) => doc.data() as T)
|
const values = snapshot.docs.map((doc) => doc.data() as T)
|
||||||
|
|
|
@ -145,7 +145,7 @@ export default function ContractPage(props: {
|
||||||
|
|
||||||
<Col className="flex-1">
|
<Col className="flex-1">
|
||||||
{allowTrade && (
|
{allowTrade && (
|
||||||
<BetPanel className="hidden lg:inline" contract={contract} />
|
<BetPanel className="hidden lg:flex" contract={contract} />
|
||||||
)}
|
)}
|
||||||
{allowResolve && (
|
{allowResolve && (
|
||||||
<ResolutionPanel creator={user} contract={contract} />
|
<ResolutionPanel creator={user} contract={contract} />
|
||||||
|
@ -177,12 +177,8 @@ function BetsSection(props: {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Title className="px-2" text="Your trades" />
|
<Title className="px-2" text="Your trades" />
|
||||||
{isBinary && (
|
|
||||||
<>
|
|
||||||
<MyBetsSummary className="px-2" contract={contract} bets={userBets} />
|
<MyBetsSummary className="px-2" contract={contract} bets={userBets} />
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<ContractBetsTable contract={contract} bets={userBets} />
|
<ContractBetsTable contract={contract} bets={userBets} />
|
||||||
<Spacer h={12} />
|
<Spacer h={12} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -139,6 +139,15 @@ function Contents() {
|
||||||
bettors that are correct more often will gain influence, leading to
|
bettors that are correct more often will gain influence, leading to
|
||||||
better-calibrated forecasts over time.
|
better-calibrated forecasts over time.
|
||||||
</p>
|
</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>
|
<h3 id="how-are-markets-resolved-">How are markets resolved?</h3>
|
||||||
<p>
|
<p>
|
||||||
The creator of the prediction market decides the outcome and earns{' '}
|
The creator of the prediction market decides the outcome and earns{' '}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { ContractFeed } from '../components/contract-feed'
|
import { ContractFeed, ContractSummaryFeed } from '../components/contract-feed'
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
import { Contract } from '../lib/firebase/contracts'
|
import { Contract } from '../lib/firebase/contracts'
|
||||||
import { Comment } from '../lib/firebase/comments'
|
import { Comment } from '../lib/firebase/comments'
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from '../components/layout/col'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { filterDefined } from '../../common/util/array'
|
|
||||||
|
|
||||||
const MAX_ACTIVE_CONTRACTS = 75
|
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.
|
// 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?
|
// TODO: Maybe store last activity time directly in the contract?
|
||||||
|
@ -25,12 +23,11 @@ function lastActivityTime(contract: Contract) {
|
||||||
// - Comment on a market
|
// - Comment on a market
|
||||||
// - New market created
|
// - New market created
|
||||||
// - Market resolved
|
// - Market resolved
|
||||||
// - Markets with most betting in last 24 hours
|
// - Bet on market
|
||||||
export function findActiveContracts(
|
export function findActiveContracts(
|
||||||
allContracts: Contract[],
|
allContracts: Contract[],
|
||||||
recentComments: Comment[],
|
recentComments: Comment[],
|
||||||
recentBets: Bet[],
|
recentBets: Bet[]
|
||||||
daysAgo = 3
|
|
||||||
) {
|
) {
|
||||||
const idToActivityTime = new Map<string, number>()
|
const idToActivityTime = new Map<string, number>()
|
||||||
function record(contractId: string, time: number) {
|
function record(contractId: string, time: number) {
|
||||||
|
@ -39,52 +36,38 @@ export function findActiveContracts(
|
||||||
idToActivityTime.set(contractId, Math.max(oldTime ?? 0, time))
|
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
|
// Record contract activity.
|
||||||
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
for (const contract of allContracts) {
|
||||||
for (const contract of allContracts || []) {
|
|
||||||
if (lastActivityTime(contract) > Date.now() - daysAgo * DAY_IN_MS) {
|
|
||||||
contracts.push(contract)
|
|
||||||
record(contract.id, lastActivityTime(contract))
|
record(contract.id, lastActivityTime(contract))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Add every contract that had a recent comment, too
|
// Add every contract that had a recent comment, too
|
||||||
const contractsById = new Map(allContracts.map((c) => [c.id, c]))
|
|
||||||
for (const comment of recentComments) {
|
for (const comment of recentComments) {
|
||||||
const contract = contractsById.get(comment.contractId)
|
const contract = contractsById.get(comment.contractId)
|
||||||
if (contract) {
|
if (contract) record(contract.id, comment.createdTime)
|
||||||
contracts.push(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 contractBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
||||||
const contractTotalBets = _.mapValues(contractBets, (bets) =>
|
const contractMostRecentBet = _.mapValues(
|
||||||
_.sumBy(bets, (bet) => bet.amount)
|
contractBets,
|
||||||
|
(bets) => _.maxBy(bets, (bet) => bet.createdTime) as Bet
|
||||||
)
|
)
|
||||||
const sortedPairs = _.sortBy(
|
for (const bet of Object.values(contractMostRecentBet)) {
|
||||||
_.toPairs(contractTotalBets),
|
const contract = contractsById.get(bet.contractId)
|
||||||
([_, total]) => -1 * total
|
if (contract) record(contract.id, bet.createdTime)
|
||||||
)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contracts = _.uniqBy(contracts, (c) => c.id)
|
let activeContracts = allContracts.filter(
|
||||||
contracts = contracts.filter((contract) => contract.visibility === 'public')
|
(contract) => contract.visibility === 'public' && !contract.isResolved
|
||||||
contracts = _.sortBy(contracts, (c) => -(idToActivityTime.get(c.id) ?? 0))
|
)
|
||||||
return contracts.slice(0, MAX_ACTIVE_CONTRACTS)
|
activeContracts = _.sortBy(
|
||||||
|
activeContracts,
|
||||||
|
(c) => -(idToActivityTime.get(c.id) ?? 0)
|
||||||
|
)
|
||||||
|
return activeContracts.slice(0, MAX_ACTIVE_CONTRACTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActivityFeed(props: {
|
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() {
|
export default function ActivityPage() {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
|
|
@ -19,11 +19,11 @@ export default function AddFundsPage() {
|
||||||
<SEO title="Add funds" description="Add funds" url="/add-funds" />
|
<SEO title="Add funds" description="Add funds" url="/add-funds" />
|
||||||
|
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col>
|
<Col className="bg-white rounded sm:shadow-md p-4 py-8 sm:p-8 h-full">
|
||||||
<Title text="Get Manifold Dollars" />
|
<Title className="!mt-0" text="Get Manifold Dollars" />
|
||||||
<img
|
<img
|
||||||
className="mt-6 block"
|
className="mb-6 block self-center -scale-x-100"
|
||||||
src="/praying-mantis-light.svg"
|
src="/stylized-crane-black.png"
|
||||||
width={200}
|
width={200}
|
||||||
height={200}
|
height={200}
|
||||||
/>
|
/>
|
||||||
|
@ -50,7 +50,7 @@ export default function AddFundsPage() {
|
||||||
<form
|
<form
|
||||||
action={checkoutURL(user?.id || '', amountSelected)}
|
action={checkoutURL(user?.id || '', amountSelected)}
|
||||||
method="POST"
|
method="POST"
|
||||||
className="mt-12"
|
className="mt-8"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
@ -26,6 +26,7 @@ export type LiteMarket = {
|
||||||
volume24Hours: number
|
volume24Hours: number
|
||||||
isResolved: boolean
|
isResolved: boolean
|
||||||
resolution?: string
|
resolution?: string
|
||||||
|
resolutionTime?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FullMarket = LiteMarket & {
|
export type FullMarket = LiteMarket & {
|
||||||
|
@ -54,6 +55,7 @@ export function toLiteMarket({
|
||||||
volume24Hours,
|
volume24Hours,
|
||||||
isResolved,
|
isResolved,
|
||||||
resolution,
|
resolution,
|
||||||
|
resolutionTime,
|
||||||
}: Contract): LiteMarket {
|
}: Contract): LiteMarket {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
@ -61,7 +63,10 @@ export function toLiteMarket({
|
||||||
creatorName,
|
creatorName,
|
||||||
createdTime,
|
createdTime,
|
||||||
creatorAvatarUrl,
|
creatorAvatarUrl,
|
||||||
closeTime,
|
closeTime:
|
||||||
|
resolutionTime && closeTime
|
||||||
|
? Math.min(resolutionTime, closeTime)
|
||||||
|
: closeTime,
|
||||||
question,
|
question,
|
||||||
description,
|
description,
|
||||||
tags,
|
tags,
|
||||||
|
@ -72,5 +77,6 @@ export function toLiteMarket({
|
||||||
volume24Hours,
|
volume24Hours,
|
||||||
isResolved,
|
isResolved,
|
||||||
resolution,
|
resolution,
|
||||||
|
resolutionTime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,6 +248,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
error={anteError}
|
error={anteError}
|
||||||
setError={setAnteError}
|
setError={setAnteError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
contractId={undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,7 @@ import { Fold } from '../../../../common/fold'
|
||||||
import { Comment } from '../../../../common/comment'
|
import { Comment } from '../../../../common/comment'
|
||||||
import { Page } from '../../../components/page'
|
import { Page } from '../../../components/page'
|
||||||
import { Title } from '../../../components/title'
|
import { Title } from '../../../components/title'
|
||||||
import {
|
import { Bet, listAllBets } from '../../../lib/firebase/bets'
|
||||||
Bet,
|
|
||||||
getRecentContractBets,
|
|
||||||
listAllBets,
|
|
||||||
} from '../../../lib/firebase/bets'
|
|
||||||
import { listAllComments } from '../../../lib/firebase/comments'
|
|
||||||
import { Contract } from '../../../lib/firebase/contracts'
|
import { Contract } from '../../../lib/firebase/contracts'
|
||||||
import {
|
import {
|
||||||
foldPath,
|
foldPath,
|
||||||
|
@ -40,6 +35,7 @@ import FeedCreate from '../../../components/feed-create'
|
||||||
import { SEO } from '../../../components/SEO'
|
import { SEO } from '../../../components/SEO'
|
||||||
import { useTaggedContracts } from '../../../hooks/use-contracts'
|
import { useTaggedContracts } from '../../../hooks/use-contracts'
|
||||||
import { Linkify } from '../../../components/linkify'
|
import { Linkify } from '../../../components/linkify'
|
||||||
|
import { filterDefined } from '../../../../common/util/array'
|
||||||
|
|
||||||
export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||||
const { slugs } = props.params
|
const { slugs } = props.params
|
||||||
|
@ -49,42 +45,21 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||||
|
|
||||||
const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : []
|
const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : []
|
||||||
|
|
||||||
const betsPromise = Promise.all(
|
const bets = await Promise.all(
|
||||||
contracts.map((contract) => listAllBets(contract.id))
|
contracts.map((contract) => listAllBets(contract.id))
|
||||||
)
|
)
|
||||||
|
const betsByContract = _.fromPairs(contracts.map((c, i) => [c.id, bets[i]]))
|
||||||
|
|
||||||
const [contractComments, contractRecentBets] = await Promise.all([
|
let activeContracts = findActiveContracts(contracts, [], _.flatten(bets))
|
||||||
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
|
|
||||||
)
|
|
||||||
const [resolved, unresolved] = _.partition(
|
const [resolved, unresolved] = _.partition(
|
||||||
activeContracts,
|
activeContracts,
|
||||||
({ isResolved }) => isResolved
|
({ isResolved }) => isResolved
|
||||||
)
|
)
|
||||||
activeContracts = [...unresolved, ...resolved]
|
activeContracts = [...unresolved, ...resolved]
|
||||||
|
|
||||||
const activeContractBets = await Promise.all(
|
const activeContractBets = activeContracts.map(
|
||||||
activeContracts.map((contract) => listAllBets(contract.id).catch((_) => []))
|
(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 creatorScores = scoreCreators(contracts, bets)
|
||||||
const traderScores = scoreTraders(contracts, bets)
|
const traderScores = scoreTraders(contracts, bets)
|
||||||
|
@ -102,7 +77,7 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||||
contracts,
|
contracts,
|
||||||
activeContracts,
|
activeContracts,
|
||||||
activeContractBets,
|
activeContractBets,
|
||||||
activeContractComments,
|
activeContractComments: activeContracts.map(() => []),
|
||||||
traderScores,
|
traderScores,
|
||||||
topTraders,
|
topTraders,
|
||||||
creatorScores,
|
creatorScores,
|
||||||
|
@ -169,9 +144,11 @@ export default function FoldPage(props: {
|
||||||
taggedContracts.map((contract) => [contract.id, contract])
|
taggedContracts.map((contract) => [contract.id, contract])
|
||||||
)
|
)
|
||||||
|
|
||||||
const contracts = props.contracts.map((contract) => contractsMap[contract.id])
|
const contracts = filterDefined(
|
||||||
const activeContracts = props.activeContracts.map(
|
props.contracts.map((contract) => contractsMap[contract.id])
|
||||||
(contract) => contractsMap[contract.id]
|
)
|
||||||
|
const activeContracts = filterDefined(
|
||||||
|
props.activeContracts.map((contract) => contractsMap[contract.id])
|
||||||
)
|
)
|
||||||
|
|
||||||
if (fold === null || !foldSubpages.includes(page) || slugs[2]) {
|
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 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 { Contract } from '../lib/firebase/contracts'
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
import { ActivityFeed } from './activity'
|
import { ActivityFeed, SummaryActivityFeed } from './activity'
|
||||||
import { Comment } from '../lib/firebase/comments'
|
import { Comment } from '../lib/firebase/comments'
|
||||||
import { Bet } from '../lib/firebase/bets'
|
import { Bet } from '../lib/firebase/bets'
|
||||||
import FeedCreate from '../components/feed-create'
|
import FeedCreate from '../components/feed-create'
|
||||||
|
@ -13,12 +16,15 @@ import { useUser } from '../hooks/use-user'
|
||||||
import { Fold } from '../../common/fold'
|
import { Fold } from '../../common/fold'
|
||||||
import { LoadingIndicator } from '../components/loading-indicator'
|
import { LoadingIndicator } from '../components/loading-indicator'
|
||||||
import { Row } from '../components/layout/row'
|
import { Row } from '../components/layout/row'
|
||||||
import { SparklesIcon } from '@heroicons/react/solid'
|
|
||||||
import { FastFoldFollowing } from '../components/fast-fold-following'
|
import { FastFoldFollowing } from '../components/fast-fold-following'
|
||||||
import {
|
import {
|
||||||
getAllContractInfo,
|
getAllContractInfo,
|
||||||
useActiveContracts,
|
useExploreContracts,
|
||||||
} from '../hooks/use-active-contracts'
|
useFilterYourContracts,
|
||||||
|
useFindActiveContracts,
|
||||||
|
} from '../hooks/use-find-active-contracts'
|
||||||
|
import { useGetRecentBets } from '../hooks/use-bets'
|
||||||
|
import { useActiveContracts } from '../hooks/use-contracts'
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const contractInfo = await getAllContractInfo()
|
const contractInfo = await getAllContractInfo()
|
||||||
|
@ -35,14 +41,27 @@ const Home = (props: {
|
||||||
recentBets: Bet[]
|
recentBets: Bet[]
|
||||||
recentComments: Comment[]
|
recentComments: Comment[]
|
||||||
}) => {
|
}) => {
|
||||||
|
const { folds, recentComments } = props
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const {
|
const contracts = useActiveContracts() ?? props.contracts
|
||||||
activeContracts,
|
const { yourContracts, initialFollowedFoldSlugs } = useFilterYourContracts(
|
||||||
activeBets,
|
user,
|
||||||
activeComments,
|
folds,
|
||||||
initialFollowedFoldSlugs,
|
contracts
|
||||||
} = useActiveContracts(props, user)
|
)
|
||||||
|
|
||||||
|
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) {
|
if (user === null) {
|
||||||
Router.replace('/')
|
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">
|
<Col className="mx-3 mb-3 gap-2 text-sm text-gray-800 sm:flex-row">
|
||||||
<Row className="gap-2">
|
<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" />
|
<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>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{activeContracts ? (
|
{feedMode === 'activity' &&
|
||||||
|
(recentBets ? (
|
||||||
<ActivityFeed
|
<ActivityFeed
|
||||||
contracts={activeContracts}
|
contracts={activeContracts}
|
||||||
contractBets={activeBets}
|
contractBets={activeBets}
|
||||||
|
@ -79,7 +121,14 @@ const Home = (props: {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LoadingIndicator className="mt-4" />
|
<LoadingIndicator className="mt-4" />
|
||||||
)}
|
))}
|
||||||
|
|
||||||
|
{feedMode === 'explore' &&
|
||||||
|
(exploreContracts ? (
|
||||||
|
<SummaryActivityFeed contracts={exploreContracts} />
|
||||||
|
) : (
|
||||||
|
<LoadingIndicator className="mt-4" />
|
||||||
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -245,6 +245,7 @@ ${TEST_VALUE}
|
||||||
error={anteError}
|
error={anteError}
|
||||||
setError={setAnteError}
|
setError={setAnteError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
contractId={undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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